    Option Explicit

Rem =============================================================
Rem     External user defined types for Solver arguments.
Rem =============================================================

Rem Array element structure of Solution() argument (returned from Solver):
    Type typeSolution
        AcrossWord As Integer   ' Index in Word() if across word starts at Col/Row.
        DownWord As Integer     ' Index in Word() if down word starts at Col/Row.
        Col As Integer          ' Starting column for word.
        Row As Integer          ' Starting row for word.
    End Type

Rem Structure of Args argument (some elements passed and some returned):
Rem
Rem FirstWord   (passed to Solver) - The Word array index of the
Rem             first word to be placed into the grid. If FirstWord is
Rem             less than the lower bound of Word, then the first word
Rem             is selected in the same way as the rest of the words.
Rem FirstAcross (passed to Solver) - If set to true, the first word
Rem             placed into the grid will appear as an Across word in
Rem             the solution. Otherwise, the first word will appear as a
Rem             Down word.
Rem MaxSize     (passed to Solver) - The maximum permissible size, in
Rem             letters, of the grid occupied by words in the solution.
Rem             The grid is always square, so MaxSize limits both the
Rem             number of letters across as well as down. If MaxSize is
Rem             zero, no size limit is imposed on the solution.
Rem RandomWord  (passed to Solver) - If set to true, words are selected
Rem             at random from the Word array for testing. Otherwise, words
Rem             are selected sequentially.
Rem RandomMatch (passed to Solver) - If set to true, letter matches
Rem             within a word are tested in a random order. Otherwise,
Rem             letter matches are tested in the order in which they occur
Rem             in each word.
Rem Recalled    (passed to Solver) - If set to true, Solver will use
Rem             the Match array from the last time Solver was run.
Rem             The Match array is always the same for a given list of
Rem             words, so it’s a waste of time to recreate it each
Rem             time Solver is run with the same list of words. After
Rem             running Solver once for a list of words, Recalled should
Rem             be set to true.
Rem PlacedCnt   (returned from Solver) - The number of words included in
Rem             the solution.
Rem Size        (returned from Solver) - The minimum size, in letters,
Rem             of a square grid capable of containing the solution.
Rem ErrorMsg    (returned from Solver) - Contains any error messages.

    Type typeArgs
        FirstWord As Integer
        FirstAcross As Integer
        MaxSize As Integer
        RandomWord As Integer
        RandomMatch As Integer
        Recalled As Integer
        PlacedCnt As Integer
        Size As Integer
        ErrorMsg As String
    End Type

Rem ====================================================================
Rem     Internally used types, module level variables and constants.
Rem ====================================================================

Rem Used to indicate direction in Vector().
    Const ACROSS = 1
    Const DOWN = 2

Rem Intermediate solution vector.
    Type typeWordVector
        Col As Integer      ' Starting column index in Grid() for word.
        Row As Integer      ' Starting row index in Grid() for word.
        Direction As Integer' Direction of word in Grid() - across/down.
        Link As Integer     ' Index to next Vector() element in row/col sort.
    End Type

Rem Array with one vector for each word in Word().
    Dim Vector() As typeWordVector

Rem Range of indices in Match().
    Type typeWordMatch
        FirstMatch As Integer   ' Index of first Match() element for Word1.
        LastMatch As Integer    ' Index of last Match() element for Word1.
    End Type

Rem Array with one range of indices for each word in Word().
    Dim WordMatch() As typeWordMatch

Rem One letter match between a pair of words.
    Type typeMatch
        Ltr1Posn As Integer     ' Position of matching letter in Word1
        Word2Idx As Integer    ' Index of Word2 in Word().
        Ltr2Posn As Integer     ' Position of matching letter in Word2.
    End Type

Rem Array of all letter matches between every pair of words.
    Dim Match() As typeMatch
    Dim UMatch As Integer   ' Upper bound of Match() - lower is 1.
    Dim MatchCnt As Integer ' Highest index used in Match().
    
Rem Array with one tested flag for each element in Match().
    Dim MatchTested() As Integer

Rem Array of letters as they placed in the puzzle.
    Dim Grid() As String * 1
    Dim UGrid As Integer    ' Upper bound of Grid().
    Dim LGrid As Integer    ' Lower bound of Grid().
    
Rem Miscellaneous module level variables.
    Dim LWord As Integer    ' Lower bound of Word().
    Dim UWord As Integer    ' Upper bound of Word().
    Dim MaxSize As Integer  ' Maximum size of solution grid.
    Dim MinRowPzl As Integer    ' Lowest row index used in Grid().
    Dim MaxRowPzl As Integer    ' Highest row index used in Grid().
    Dim MinColPzl As Integer    ' Lowest column index used in Grid().
    Dim MaxColPzl As Integer    ' Highest column index used in Grid().
    Dim MaxWordLength As Integer' Maximum word length found in Word().
    Dim WordCnt As Integer  ' Number of words in Word().
    Dim PlacedCnt As Integer' Number of words successfully placed in Grid().
    Dim LinkEnd As Integer  ' Index of Vector element at end of linked list.

Private Function MakeMatch (Word() As String) As Integer
Rem Put letter matches into Match() and indices into WordMatch().

    Dim Word1 As String
    Dim Word1Idx As Integer
    Dim Ltr1Posn As Integer
    Dim Length1 As Integer
    Dim Ltr1 As String * 1
    Dim Word2 As String
    Dim Word2Idx As Integer
    Dim Ltr2Posn As Integer
    Dim Prev2Posn As Integer
    Dim MatchIdx As Integer

    Rem Clear Match() and dimension to reasonable size.
    ReDim Match(1 To UWord)
    UMatch = UWord
    MaxWordLength = 0
    MatchIdx = 0
    
    Rem Check each word in Word().
    For Word1Idx = LWord To UWord
        Word1 = Word(Word1Idx)
        Length1 = Len(Word1)
        
        Rem Word can't be longer than the maximum grid size.
        If (MaxSize = 0) Or (Length1 <= MaxSize) Then
            If Length1 > MaxWordLength Then
                Rem Keep track of maximum word length for sizing Grid().
                MaxWordLength = Length1
            End If

            GoSub FindMatches
        End If
    Next Word1Idx
    
    MakeMatch = (MatchIdx > 0)
    
    Exit Function

FindMatches:
    Rem Match each letter of each word against each other.
    For Word2Idx = LWord To UWord
        DoEvents
        
        If Word2Idx <> Word1Idx Then
            Word2 = Word(Word2Idx)
            For Ltr1Posn = 1 To Length1
                Ltr1 = Mid$(Word1, Ltr1Posn, 1)
                Prev2Posn = 1
                Do
                    Ltr2Posn = InStr(Prev2Posn, Word2, Ltr1)
                    If Ltr2Posn Then
                        Rem Redim Match() if necessary.
                        If MatchIdx + 1 > UMatch Then
                            UMatch = UMatch + Int(UWord / 2) + 1
                            ReDim Preserve Match(1 To UMatch)
                        End If
                        
                        Rem Define letter match in Match().
                        MatchIdx = MatchIdx + 1
                        Match(MatchIdx).Ltr1Posn = Ltr1Posn
                        Match(MatchIdx).Word2Idx = Word2Idx
                        Match(MatchIdx).Ltr2Posn = Ltr2Posn

                        Rem Put Match() indices into WordMatch().
                        If WordMatch(Word1Idx).FirstMatch = 0 Then
                            WordMatch(Word1Idx).FirstMatch = MatchIdx
                        End If
                        WordMatch(Word1Idx).LastMatch = MatchIdx

                        Prev2Posn = Ltr2Posn + 1
                    Else
                        Exit Do
                    End If
                Loop
            Next Ltr1Posn
        End If
    Next Word2Idx
    Return

End Function

Function MakeSolution (Solution() As typeSolution)
Rem Generate Solution() with column and row adjusted to base of 1.
    
    Dim Col1 As Integer
    Dim Row1 As Integer
    Dim SolutionIdx As Integer
    Dim Link As Integer

    ReDim Solution(1 To PlacedCnt)
    
    Col1 = -1   ' Trigger SolutionIdx increment first time round.
    SolutionIdx = 0
    Link = LinkEnd
    Do Until Link < LWord
        DoEvents

        Rem Only Vectors with same col/row share same Solution element.
        If (Vector(Link).Col - MinColPzl + 1 <> Col1) Or (Vector(Link).Row - MinRowPzl + 1 <> Row1) Then
            Rem Initialize next position in Solution().
            SolutionIdx = SolutionIdx + 1
            Solution(SolutionIdx).AcrossWord = LWord - 1
            Solution(SolutionIdx).DownWord = LWord - 1
        End If

        Col1 = Vector(Link).Col - MinColPzl + 1
        Row1 = Vector(Link).Row - MinRowPzl + 1
        Solution(SolutionIdx).Col = Col1
        Solution(SolutionIdx).Row = Row1
        If Vector(Link).Direction = ACROSS Then
            Solution(SolutionIdx).AcrossWord = Link
        Else
            Solution(SolutionIdx).DownWord = Link
        End If
        Link = Vector(Link).Link
    Loop

    Rem Reduce size of array to number of elements actually used.
    ReDim Preserve Solution(1 To SolutionIdx)

    MakeSolution = True

End Function

Private Function MakeVectors (Word() As String, Args As typeArgs) As Integer
Rem Generate Vector() word vectors.

    Dim Vector1 As typeWordVector
    Dim Vector2 As typeWordVector
    Dim Word1Idx As Integer
    Dim Word2Idx As Integer
    Dim Col1 As Integer
    Dim Row1 As Integer
    Dim Direction1 As Integer
    Dim WordsTried() As Integer
    Dim WordsToTry As Integer
    Dim PlacingWords As Integer
    Dim MatchIdx As Integer
    Dim FirstMatch As Integer
    Dim LastMatch As Integer
    Dim MatchCnt As Integer
    Dim MatchTried() As Integer
    Dim MatchToTry As Integer
    Dim NoValidMatch As Integer
    Dim Rtn As Integer

    Rem Initialize number of words placed in Grid.
    PlacedCnt = 0
    
    Rem Dimension Grid() to a reasonable size.
    If MaxSize > 0 Then
        UGrid = MaxSize * 4
    ElseIf MaxWordLength > WordCnt Then
        UGrid = MaxWordLength * 4
    Else
        UGrid = WordCnt * 4
    End If
    LGrid = 0
    ReDim Grid(LGrid - 1 To UGrid + 1, LGrid - 1 To UGrid + 1)
    
    Rem Initialize markers to test for exceeding maximum size.
    MinRowPzl = UGrid
    MaxRowPzl = LGrid
    MinColPzl = UGrid
    MaxColPzl = LGrid

    Rem Initialize marker to end of linked list.
    LinkEnd = LWord - 1

    GoSub PlaceFirstWord
    GoSub PlaceRestOfWords
    
    Rem Return size of Grid and number of words in it.
    Col1 = MaxColPzl - MinColPzl + 1
    Row1 = MaxRowPzl - MinRowPzl + 1
    If Col1 > Row1 Then
        Args.Size = Col1
    Else
        Args.Size = Row1
    End If
    Args.PlacedCnt = PlacedCnt

    Rem Done with these arrays.
    Erase WordsTried
    Erase MatchTried
    Erase Grid

    MakeVectors = (PlacedCnt > 1)
    Exit Function

PlaceFirstWord:
Rem Place first word with a letter match into the Grid.
    ReDim WordsTried(LWord To UWord)
    WordsToTry = WordCnt
    Word1Idx = Args.FirstWord
    If Args.FirstAcross Then
        Direction1 = ACROSS
    Else
        Direction1 = DOWN
    End If

    PlacingWords = True
    Do  ' While PlacingWords
        DoEvents
        If Word1Idx >= LWord Then
            If WordMatch(Word1Idx).FirstMatch > 0 Then
                Rem Position word near the center of Grid().
                Vector(Word1Idx).Col = Int((UGrid - Len(Word(Word1Idx))) / 2)
                Vector(Word1Idx).Row = Int(UGrid / 2)
                Vector(Word1Idx).Direction = Direction1
                Rtn = PlaceWord(Word(Word1Idx), Word1Idx)
                PlacingWords = False
            End If
        End If

        If PlacingWords Then
            PlacingWords = NextIdx(Word1Idx, WordsTried(), WordsToTry, Args.RandomWord)
        End If
    Loop While PlacingWords
    Return

PlaceRestOfWords:
Rem Test successive words at crosspoints and place into Grid() if valid.
    ReDim WordsTried(LWord To UWord)
    ReDim MatchTried(1 To 1)
    WordsToTry = WordCnt
    Do  ' While PlacingWords
        PlacingWords = False
        Do  ' Until no more words to try.
            If Vector(Word1Idx).Direction = 0 Then
                Rem Word1 hasn't been put into the Grid() yet.
                NoValidMatch = True
                FirstMatch = WordMatch(Word1Idx).FirstMatch
                If FirstMatch > 0 Then
                    LastMatch = WordMatch(Word1Idx).LastMatch
                    MatchCnt = LastMatch - FirstMatch + 1
                    MatchToTry = MatchCnt
                    ReDim MatchTried(FirstMatch To LastMatch)
                    MatchIdx = FirstMatch - 1
                Else
                    MatchToTry = 0
                End If
                MatchIdx = FirstMatch - 1   ' MatchIdx hasn't been tried.
                
                Do While NextIdx(MatchIdx, MatchTried(), MatchToTry, Args.RandomMatch)
                    DoEvents
                    If Not MatchTested(MatchIdx) Then
                        Rem We didn't fail this letter match yet.
                        Word2Idx = Match(MatchIdx).Word2Idx
                        If Vector(Word2Idx).Direction Then
                            Rem Test match on Word2 already in Grid().
                            Vector1 = Vector(Word1Idx)
                            Vector2 = Vector(Word2Idx)
                            If TestMatch(Word(Word1Idx), MatchIdx, Vector1, Vector2) Then
                                Vector(Word1Idx) = Vector1
                                Rtn = PlaceWord(Word(Word1Idx), Word1Idx)

                                NoValidMatch = False

                                Rem Word1 may provide crosses for other words.
                                PlacingWords = True

                                Rem Force end of MatchIdx loop for Word1.
                                MatchToTry = 0
                            Else
                                MatchTested(MatchIdx) = True
                            End If
                        Else
                            Rem Word2 may get put into Grid() before Word1.
                            NoValidMatch = False
                        End If
                    End If
                Loop ' Do while NextIdx

                If NoValidMatch Then
                    Rem Stop testing Word1 since all letter matches failed.
                    Vector(Word1Idx).Direction = -1
                End If
            End If
        Loop While NextIdx(Word1Idx, WordsTried(), WordsToTry, Args.RandomWord)
    Loop While PlacingWords
    Return

End Function

Private Function NextIdx (Idx As Integer, Tried() As Integer, ToTry As Integer, Randum As Integer) As Integer
Rem Handle random selection of the next index in array to try.
    
    Dim LTried As Integer
    Dim UTried As Integer
    Dim CurrIdx As Integer
    Dim Cnt As Integer

    LTried = LBound(Tried)
    UTried = UBound(Tried)
    Cnt = UTried - LTried + 1
    If (Idx >= LTried) And (Idx <= UTried) Then
        Tried(Idx) = True
        ToTry = ToTry - 1
    End If

    If ToTry > 0 Then
        CurrIdx = Idx
        Do
            If Randum Then
                Rem Set random index between LTried and UTried.
                Idx = Int((Cnt * Rnd) + LTried)
            Else
                Rem Increment index.
                Idx = Idx + 1
                If Idx > UTried Then
                    Rem Wrap around to front of array.
                    Idx = LTried
                End If
            End If
        Loop While Tried(Idx) Or (Idx = CurrIdx)
    End If

    NextIdx = (ToTry > 0)

End Function

Private Function PlaceWord (Word1 As String, WordIdx As Integer) As Integer
Rem Place word into Grid().

    Dim Col1 As Integer
    Dim Row1 As Integer
    Dim Direction1 As Integer
    Dim Word1Across As Integer
    Dim Word1Len As Integer
    Dim Col As Integer
    Dim Row As Integer
    Dim LtrPosn As Integer
    Dim Link As Integer
    Dim PrevLink As Integer

    Col1 = Vector(WordIdx).Col
    Row1 = Vector(WordIdx).Row
    Direction1 = Vector(WordIdx).Direction
    Word1Across = (Direction1 = ACROSS)

    Rem Place each character into Grid().
    Col = Col1
    Row = Row1
    Word1Len = Len(Word1)
    For LtrPosn = 1 To Word1Len
        Grid(Col, Row) = Mid$(Word1, LtrPosn, 1)
        
        If Word1Across Then
            Col = Col + 1
        Else
            Row = Row + 1
        End If
    Next LtrPosn
    PlacedCnt = PlacedCnt + 1

    Rem Update outer boundaries of words actually placed in Grid.
    If Word1Across Then
        If Col1 < MinColPzl Then MinColPzl = Col1
        Col = Col1 + Word1Len - 1
        If Col > MaxColPzl Then MaxColPzl = Col
    Else
        If Row1 < MinRowPzl Then MinRowPzl = Row1
        Row = Row1 + Word1Len - 1
        If Row > MaxRowPzl Then MaxRowPzl = Row
    End If

    Rem Maintain list links in Vector().Link sorted by row/col/direction.
    Link = LinkEnd
    Do Until Link < LWord
        Rem Link to vector with next higher row/col/direction.
        Select Case Vector(Link).Row
            Case Is < Row1

            Case Row1
                Select Case Vector(Link).Col
                    Case Is < Col1

                    Case Col1
                        If Vector(Link).Direction > Direction1 Then Exit Do

                    Case Else
                        Exit Do
                End Select
            
            Case Else
                Exit Do
        End Select
        
        PrevLink = Link
        Link = Vector(Link).Link
    Loop
    Vector(WordIdx).Link = Link
    If Link = LinkEnd Then
        LinkEnd = WordIdx
    Else
        Vector(PrevLink).Link = WordIdx
    End If
    
    PlaceWord = True

End Function

Function Solver (Word() As String, Solution() As typeSolution, Args As typeArgs) As Integer
Rem Main routine called to create Solution() from Word().

   Randomize

    Solver = False  ' Default

    On Error GoTo SolverFailure

    Args.ErrorMsg = ""
    LWord = LBound(Word)
    UWord = UBound(Word)
    WordCnt = UWord - LWord + 1
    MaxSize = Args.MaxSize
    If MaxSize < 0 Then MaxSize = 0

    If WordCnt <= 1 Then
        Args.ErrorMsg = "Word array must contain at least 2 words."
    Else
        If Not Args.Recalled Then
            Rem Find letter matches if not being recalled.
            ReDim WordMatch(LWord To UWord)
            MatchCnt = MakeMatch(Word())
        End If
        ReDim MatchTested(1 To UMatch)
        
        If MatchCnt Then
            Rem Dimension Vector() for 1 to 1 relation with Word().
            ReDim Vector(LWord To UWord)

            If MakeVectors(Word(), Args) Then
                Solver = MakeSolution(Solution())
            Else
                Args.ErrorMsg = "Unable to develop a solution."
            End If

            Erase Vector
        Else
            Args.ErrorMsg = "No letter matches between words."
        End If

        Erase MatchTested
    End If

    Exit Function

SolverFailure:
    MsgBox "Unexpected fatal error in Solver: " & Error$
    Exit Function
    
End Function

Private Function TestMatch (Text1 As String, MatchIdx As Integer, Vector1 As typeWordVector, Vector2 As typeWordVector)
Rem Determine if Word1 can be placed in Grid() crossing Word2 already in Grid().
Rem Return Vector1 if valid.

    Dim ValidMatch As Integer
    Dim Text As String
    Dim Ltr As String * 1
    Dim LtrPosn As Integer
    Dim Length As Integer
    Dim ColPzl As Integer
    Dim ColInc As Integer
    Dim ColSide As Integer
    Dim RowPzl As Integer
    Dim RowInc As Integer
    Dim RowSide As Integer
    Dim zero As String * 1
    Dim PosnEnd As Integer
    Dim TmpGrid() As String * 1

    ValidMatch = True
    Length = Len(Text1)

    Rem Set variables so that testing logic is independent of direction.
    If Vector2.Direction = ACROSS Then
        Vector1.Col = Vector2.Col + Match(MatchIdx).Ltr2Posn - 1
        Vector1.Row = Vector2.Row - (Match(MatchIdx).Ltr1Posn - 1)
        ColInc = 0
        RowInc = 1
        ColSide = 1
        RowSide = 0
        Vector1.Direction = DOWN
        PosnEnd = Vector1.Row + Length - 1
        If MaxSize > 0 Then
            Rem Test for word extending beyond maximum grid size.
            If Vector1.Row < MinRowPzl Then
                If (MaxRowPzl - Vector1.Row + 1) > MaxSize Then
                    ValidMatch = False
                End If
            ElseIf PosnEnd > MaxRowPzl Then
                If (PosnEnd - MinRowPzl + 1) > MaxSize Then
                    ValidMatch = False
                End If
            End If
        End If
        If ValidMatch And (Vector1.Row < LGrid) Or (PosnEnd > UGrid) Then
            GoSub GrowGrid
        End If
    Else
        Vector1.Col = Vector2.Col - (Match(MatchIdx).Ltr1Posn - 1)
        Vector1.Row = Vector2.Row + Match(MatchIdx).Ltr2Posn - 1
        ColInc = 1
        RowInc = 0
        ColSide = 0
        RowSide = 1
        Vector1.Direction = ACROSS
        PosnEnd = Vector1.Col + Length - 1
        If MaxSize > 0 Then
            Rem Test for word extending beyond maximum grid size.
            If Vector1.Col < MinColPzl Then
                If (MaxColPzl - Vector1.Col + 1) > MaxSize Then
                    ValidMatch = False
                End If
            ElseIf PosnEnd > MaxColPzl Then
                If (PosnEnd - MinColPzl + 1) > MaxSize Then
                    ValidMatch = False
                End If
            End If
        End If
        If ValidMatch And (Vector1.Col < LGrid) Or (PosnEnd > UGrid) Then
            GoSub GrowGrid
        End If
    End If
    
    If ValidMatch Then
        Rem Prevent butting into other words.
        zero = Chr$(0)
        Text = zero & Text1 & zero
        Length = Length + 2

        ColPzl = Vector1.Col - ColInc
        RowPzl = Vector1.Row - RowInc
        LtrPosn = 1
        Do
            Ltr = Grid(ColPzl, RowPzl)
            Select Case Ltr
                Case Is = Mid$(Text, LtrPosn, 1)
                    Rem Matches letter in Text.
                    If Ltr <> zero Then
                        If Grid(ColPzl + ColInc, RowPzl + RowInc) <> zero Then
                            Rem Prevent running into start of another word.
                            ValidMatch = False
                        End If
                    End If

                Case Is = zero
                    Rem Cell is empty. Check adjoining cells.
                    If Grid(ColPzl + ColSide, RowPzl + RowSide) = zero Then
                        If Grid(ColPzl - ColSide, RowPzl - RowSide) <> zero Then
                            ValidMatch = False
                        End If
                    Else
                        ValidMatch = False
                    End If

                Case Else
                    Rem Cell already has another letter.
                    ValidMatch = False
            End Select
            
            LtrPosn = LtrPosn + 1
            ColPzl = ColPzl + ColInc
            RowPzl = RowPzl + RowInc
        Loop Until Not ValidMatch Or (LtrPosn > Length)
    End If

    TestMatch = ValidMatch
    Exit Function

GrowGrid:
Rem VB can't preserve contents of array when both dimensions are ReDim'd.
    
    Rem Copy Grid() to temporary array.
    ReDim TmpGrid(MinColPzl To MaxColPzl, MinRowPzl To MaxRowPzl)
    For ColPzl = MinColPzl To MaxColPzl
        For RowPzl = MinRowPzl To MaxRowPzl
            TmpGrid(ColPzl, RowPzl) = Grid(ColPzl, RowPzl)
        Next RowPzl
    Next ColPzl

    Rem Redimension Grid() and recopy contents to original positions.
    UGrid = UGrid + Int(UGrid / 4)
    LGrid = LGrid - Int(UGrid / 4)
    ReDim Grid(LGrid - 1 To UGrid + 1, LGrid - 1 To UGrid + 1)
    For ColPzl = MinColPzl To MaxColPzl
        For RowPzl = MinRowPzl To MaxRowPzl
            Grid(ColPzl, RowPzl) = TmpGrid(ColPzl, RowPzl)
        Next RowPzl
    Next ColPzl
    Return

End Function

