Attribute VB_Name = "TileMath"

Option Explicit

'Neighbours
'0 -> (+1,  0)
'1 -> (+1, -1)
'2 -> ( 0, -1)
'3 -> (-1, -1)
'4 -> (-1,  0)
'5 -> (-1, +1)
'6 -> ( 0, +1)
'7 -> (+1, +1)

Public Enum Match
    Match_false
    Match_true
    Match_any
End Enum

Public Type NeighbourhoodBoolean
    N(0 To 7) As Boolean
    TrueCount As Byte
End Type

Public Type NeighbourhoodMatch
    N(0 To 7) As Match
    TrueMin As Byte
    TrueMax As Byte
    NumberOfTests As Byte
End Type

Public Type TileVector
    X As Long
    Z As Long
End Type

Public Type TileRect
    MinX As Long
    MaxX As Long
    MinZ As Long
    MaxZ As Long
End Type

Public Function MakeSafeTileRect(MinX As Long, MaxX As Long, MinZ As Long, MaxZ As Long) As TileRect
    'Insert the values bounded by the level size
    MakeSafeTileRect.MinX = Max_Long(0, MinX)
    MakeSafeTileRect.MaxX = Min_Long(MapSizeX - 1, MaxX)
    MakeSafeTileRect.MinZ = Max_Long(0, MinZ)
    MakeSafeTileRect.MaxZ = Min_Long(MapSizeZ - 1, MaxZ)
End Function

Public Function TileRect_Overlap(A As TileRect, B As TileRect) As Boolean
    If A.MaxX < B.MinX - 1 Then
        TileRect_Overlap = False
    ElseIf B.MaxX < A.MinX - 1 Then
        TileRect_Overlap = False
    ElseIf A.MaxZ < B.MinZ - 1 Then
        TileRect_Overlap = False
    ElseIf B.MaxZ < A.MinZ - 1 Then
        TileRect_Overlap = False
    Else
        TileRect_Overlap = True
    End If
End Function

Public Function TileRect_Inside(A As TileRect, ByVal X As Long, ByVal Z As Long) As Boolean
    TileRect_Inside = (A.MinX <= X And X <= A.MaxX And A.MinZ <= Z And Z <= A.MaxZ)
End Function

Public Function MakeTileVector(X As Long, Z As Long) As TileVector
    MakeTileVector.X = X
    MakeTileVector.Z = Z
End Function

Public Function IsNMEqual(A As NeighbourhoodMatch, B As NeighbourhoodMatch) As Boolean
    IsNMEqual = (A.N(0) = B.N(0) _
             And A.N(1) = B.N(1) _
             And A.N(2) = B.N(2) _
             And A.N(3) = B.N(3) _
             And A.N(4) = B.N(4) _
             And A.N(5) = B.N(5) _
             And A.N(6) = B.N(6) _
             And A.N(7) = B.N(7))
End Function

Public Function MatchTruth(M As Match, ByVal B As Boolean) As Boolean
    If (B = False And M = Match_true) Or (B = True And M = Match_false) Then
        MatchTruth = False
    Else
        MatchTruth = True
    End If
End Function

'Returns the first matching offset of 0, 1, 2 or 3 for 0, 90, 180 or 270 degrees
'Returns 255 if there is no match
Public Function Neighbourhood_Match(B As NeighbourhoodBoolean, M As NeighbourhoodMatch) As Byte
    Static I As Integer
    Static J As Integer
    Static Offset As Integer
    Static CurrentMatch As Boolean
    
    'Fast test
    If B.TrueCount < M.TrueMin Or B.TrueCount > M.TrueMax Then
        'Can't match
        Neighbourhood_Match = 255
        Exit Function
    End If
    
    'Slow test
    For Offset = 0 To M.NumberOfTests - 1
        'See if B matches the pattern M with the current offset.
        CurrentMatch = True
        For I = 0 To 7
            J = (I + (Offset * 2)) Mod 8
            If MatchTruth(M.N(I), B.N(J)) = False Then
                CurrentMatch = False
            End If
        Next I
        
        'Return the direction if it matched
        If CurrentMatch Then
            Neighbourhood_Match = Offset
            Exit Function
        End If
    Next Offset
    
    'No match found
    Neighbourhood_Match = 255
End Function

'The default constructor for NeighbourhoodMatch
Public Function MakeNM(N0 As Match, N1 As Match, N2 As Match, N3 As Match, N4 As Match, N5 As Match, N6 As Match, N7 As Match) As NeighbourhoodMatch
    Static I As Integer
    
    'Insert the values
    MakeNM.N(0) = N0
    MakeNM.N(1) = N1
    MakeNM.N(2) = N2
    MakeNM.N(3) = N3
    MakeNM.N(4) = N4
    MakeNM.N(5) = N5
    MakeNM.N(6) = N6
    MakeNM.N(7) = N7
    
    'Count the minimum and maximum number of true values that can match this pattern
    For I = 0 To 7
        If MakeNM.N(I) = Match_true Then
            MakeNM.TrueMin = MakeNM.TrueMin + 1
            MakeNM.TrueMax = MakeNM.TrueMax + 1
        ElseIf MakeNM.N(I) = Match_any Then
            MakeNM.TrueMax = MakeNM.TrueMax + 1
        End If
    Next I
    
    'Test the symmetry to set the number of required tests
    If N0 = N4 And N1 = N5 And N2 = N6 And N3 = N7 Then
        If N0 = N2 And N1 = N3 And N2 = N4 And N3 = N5 Then
            'We have 90 degree symmetry
            MakeNM.NumberOfTests = 1
        Else
            'We have 180 degree symmetry
            MakeNM.NumberOfTests = 2
        End If
    Else
        'We have 360 degree symmetry
        MakeNM.NumberOfTests = 4
    End If
End Function

Public Function MakeNB(N0 As Boolean, N1 As Boolean, N2 As Boolean, N3 As Boolean, N4 As Boolean, N5 As Boolean, N6 As Boolean, N7 As Boolean) As NeighbourhoodBoolean
    Static I As Integer
    
    'Insert the values
    MakeNB.N(0) = N0
    MakeNB.N(1) = N1
    MakeNB.N(2) = N2
    MakeNB.N(3) = N3
    MakeNB.N(4) = N4
    MakeNB.N(5) = N5
    MakeNB.N(6) = N6
    MakeNB.N(7) = N7
    
    'Count the number of true values
    MakeNB.TrueCount = 0
    For I = 0 To 7
        If MakeNB.N(I) Then
            MakeNB.TrueCount = MakeNB.TrueCount + 1
        End If
    Next I
End Function

Public Function TileRect_IsEqual(A As TileRect, B As TileRect) As Boolean
    TileRect_IsEqual = (A.MinX = B.MinX And A.MaxX = B.MaxX And A.MinZ = B.MinZ And A.MaxZ = B.MaxZ)
End Function

Public Function GetXAxisFromDirection_TileVector(Direction As Byte) As TileVector
    Select Case Direction
    Case 0
        '0 degrees CW
        GetXAxisFromDirection_TileVector = MakeTileVector(1, 0)
    Case 1
        '90 degrees CW
        GetXAxisFromDirection_TileVector = MakeTileVector(0, -1)
    Case 2
        '180 degrees CW
        GetXAxisFromDirection_TileVector = MakeTileVector(-1, 0)
    Case 3
        '270 degrees CW
        GetXAxisFromDirection_TileVector = MakeTileVector(0, 1)
    Case Else
        MsgBox "Invalid direction detected in GetXAxisFromDirection_TileVector.", vbCritical, "Error!"
    End Select
End Function

Public Function GetZAxisFromDirection_TileVector(Direction As Byte) As TileVector
    Select Case Direction
    Case 0
        '0 degrees CW
        GetZAxisFromDirection_TileVector = MakeTileVector(0, 1)
    Case 1
        '90 degrees CW
        GetZAxisFromDirection_TileVector = MakeTileVector(1, 0)
    Case 2
        '180 degrees CW
        GetZAxisFromDirection_TileVector = MakeTileVector(0, -1)
    Case 3
        '270 degrees CW
        GetZAxisFromDirection_TileVector = MakeTileVector(-1, 0)
    Case Else
        MsgBox "Invalid direction detected in GetXAxisFromDirection_TileVector.", vbCritical, "Error!"
    End Select
End Function

Public Function GetXAxisFromDirection_Vector3(Direction As Byte) As Vector3
    Select Case Direction
    Case 0
        '0 degrees CW
        GetXAxisFromDirection_Vector3 = MakeVector3(1, 0, 0)
    Case 1
        '90 degrees CW
        GetXAxisFromDirection_Vector3 = MakeVector3(0, 0, -1)
    Case 2
        '180 degrees CW
        GetXAxisFromDirection_Vector3 = MakeVector3(-1, 0, 0)
    Case 3
        '270 degrees CW
        GetXAxisFromDirection_Vector3 = MakeVector3(0, 0, 1)
    Case Else
        MsgBox "Invalid direction detected in GetXAxisFromDirection_Vector3.", vbCritical, "Error!"
    End Select
End Function

Public Function GetZAxisFromDirection_Vector3(Direction As Byte) As Vector3
    Select Case Direction
    Case 0
        '0 degrees CW
        GetZAxisFromDirection_Vector3 = MakeVector3(0, 0, 1)
    Case 1
        '90 degrees CW
        GetZAxisFromDirection_Vector3 = MakeVector3(1, 0, 0)
    Case 2
        '180 degrees CW
        GetZAxisFromDirection_Vector3 = MakeVector3(0, 0, -1)
    Case 3
        '270 degrees CW
        GetZAxisFromDirection_Vector3 = MakeVector3(-1, 0, 0)
    Case Else
        MsgBox "Invalid direction detected in GetXAxisFromDirection_Vector3.", vbCritical, "Error!"
    End Select
End Function
