namespace rooibos
    ' @ignore
    class MochaTestReporter extends rooibos.BaseTestReporter

        function new(runner as rooibos.TestRunner)
            super(runner)
        end function

        private failureCount = 0

        ' override function onBegin(event as rooibos.TestReporterOnBeginEvent)
        '   'override me
        ' end function

        override function onSuiteBegin(event as rooibos.TestReporterOnSuiteBeginEvent)
            print m.colorLines(rooibos.reporters.mocha.colors.suite, event.suite.name)
        end function

        override function onTestGroupBegin(event as rooibos.TestReporterOnTestGroupBeginEvent)
            print tab(2) m.colorLines(rooibos.reporters.mocha.colors.suite, event.group.name)
        end function

        ' override function onTestBegin(event as rooibos.TestReporterOnTestBeginEvent)
        '   'override me
        ' end function

        override function onTestComplete(event as rooibos.TestReporterOnTestCompleteEvent)
            test = event.test
            status = test.result.getStatusText()

            lineColor = rooibos.reporters.mocha.colors.light
            symbolColor = ""
            symbol = "?"
            if status = "PASS" then
                symbol = "✔"
                symbolColor = rooibos.reporters.mocha.colors.checkMark
            else if status = "FAIL" or status = "CRASH" then
                symbol = "✖"
                symbolColor = rooibos.reporters.mocha.colors.brightFail
            else if status = "SKIP" then
                symbol = "-"
                symbolColor = rooibos.reporters.mocha.colors.pending
                lineColor = rooibos.reporters.mocha.colors.pending
            end if

            params = ""
            if test.isParamTest then
                rawParams = invalid
                if type(test.rawParams) = "roAssociativeArray" then
                    rawParams = {}
                    for each key in test.rawParams
                        if type(test.rawParams[key]) <> "Function" and type(test.rawParams[key]) <> "roFunction" then
                            rawParams[key] = test.rawParams[key]
                        end if
                    end for
                else
                    rawParams = test.rawParams
                end if

                params = " " + formatJson(rawParams)
            end if

            duration = ""
            if test.result.time > test.slow then
                duration = m.colorLines(rooibos.reporters.mocha.colors.slow, ` (${test.result.time}ms)`)
            else if test.result.time > test.slow / 2 then
                duration = m.colorLines(rooibos.reporters.mocha.colors.medium, ` (${test.result.time}ms)`)
                ' else if test.result.time > slow / 4
                '   duration = m.colorLines(rooibos.reporters.mocha.colors.fast, ` (${test.result.time}ms)`)
            end if

            print tab(4) m.colorLines(symbolColor, symbol) + " " + m.colorLines(lineColor, test.name + params) + duration
        end function

        ' override function onTestGroupComplete(event as rooibos.TestReporterOnTestGroupCompleteEvent)
        '   'override me
        ' end function

        ' override function onSuiteComplete(event as rooibos.TestReporterOnSuiteCompleteEvent)
        '   'override me
        ' end function

        override function onEnd(event as rooibos.TestReporterOnEndEvent)
            print m.formatStatsString(event.stats)

            for each testSuite in m.testRunner.testSuites
                for each testGroup in testSuite.groups
                    m.logFailures(testGroup)
                end for
            end for
        end function

        ' Creates a formatted string from the stats object
        ' example:
        '`
        '     327 passed (5113ms)
        '     1 crashed
        '     207 failing
        '     8 skipped
        '`
        function formatStatsString(stats as rooibos.Stats) as string
            statusString = chr(10)

            indent = string(1, chr(9))
            statusString += `${indent}${m.colorLines(rooibos.reporters.mocha.colors.brightPass, `${stats.passedCount} passed`)} ${m.colorLines(rooibos.reporters.mocha.colors.light, ` (${stats.time}ms)`)}`

            if stats.crashedCount > 0 then
                statusString += chr(10) + m.colorLines(rooibos.reporters.mocha.colors.fail, `${indent}${stats.crashedCount} crashed`)
            end if

            if stats.failedCount > 0 then
                statusString += chr(10) + m.colorLines(rooibos.reporters.mocha.colors.fail, `${indent}${stats.failedCount} failing`)
            end if

            if stats.ignoredCount > 0 then
                statusString += chr(10) + m.colorLines(rooibos.reporters.mocha.colors.pending, `${indent}${stats.ignoredCount} skipped`)
            end if

            statusString += chr(10)
            return statusString
        end function


        ' Logs all failures for a given test group
        ' example:

        '    1) Rooibos failed assertion tests
        '      tests fail on crash
        '        reports error:
        '
        '    Error: some error
        '        $anon_6c() As Dynamic (pkg:/source/FailedAssertion.spec.brs:11)
        '        $anon_303() As Dynamic (pkg:/source/rooibos/Test.brs:45)
        '        $anon_1f2(test As Object) As Dynamic (pkg:/source/rooibos/BaseTestSuite.brs:243)
        '        $anon_30a() As Dynamic (pkg:/source/rooibos/TestGroup.brs:88)
        '        $anon_309() As Dynamic (pkg:/source/rooibos/TestGroup.brs:68)
        '        $anon_1ec() As Dynamic (pkg:/source/rooibos/BaseTestSuite.brs:131)
        '        $anon_1eb() As Dynamic (pkg:/source/rooibos/BaseTestSuite.brs:121)
        '        $anon_325(testsuite As Dynamic) As Void (pkg:/source/rooibos/TestRunner.brs:191)
        '        $anon_322() As Dynamic (pkg:/source/rooibos/TestRunner.brs:72)
        '        rooibos_init(testSceneName As Dynamic) As Void (pkg:/source/rooibos/Rooibos.brs:27)
        '        main(args As Dynamic) As Dynamic (pkg:/source/Main.brs:2)
        '
        '    at (file:///Users/chris/roku/rooibos/tests/src/source/FailedAssertion.spec.bs:15)
        '
        '    2) Rooibos failed assertion tests
        '        tests AssertTrue fail
        '            AssertTrue with message 0:
        '
        '    AssertionError: expected "false (Boolean)" to be true (Boolean)
        '      + expected - actual
        '
        '      -false (Boolean)
        '      +true (Boolean)
        '
        '    params at (file:///Users/chris/roku/rooibos/tests/src/source/FailedAssertion.spec.bs:23)
        '    assertion at (file:///Users/chris/roku/rooibos/tests/src/source/FailedAssertion.spec.bs:31)
        '
        function logFailures(testGroup as rooibos.TestGroup)
            for each test in testGroup.tests
                if test.result.isFail then
                    m.failureCount++

                    resultMessage = ""
                    resultMessage += `${string(1, chr(9))}${m.failureCount.toStr()}) ${test.testSuite.name}\n`
                    resultMessage += `${string(2, chr(9))}${testGroup.name}\n`
                    resultMessage += `${string(3, chr(9))}${test.name}:\n\n`
                    if not test.result.isCrash then
                        resultMessage += `${string(1, chr(9))}AssertionError: ${test.result.getMessage()}`

                        if (test.result.actual <> "" or test.result.expected <> "") and (test.result.actual <> test.result.expected) then
                            resultMessage += m.unifiedDiff(test.result.actual, test.result.expected)
                        end if

                        resultMessage += chr(10)
                    else
                        resultMessage += `${string(1, chr(9))}Error: ${m.getStackTrace(test.result.error)}\n`
                    end if

                    if test.result.error <> invalid and not test.result.isCrash then
                        resultMessage += m.getStackTrace(test.result.error, false) + chr(10)
                    end if

                    if test.isParamTest then
                        resultMessage += `${string(6, " ")}${m.colorLines(rooibos.reporters.mocha.colors.errorStack, `params at (file://${test.testSuite.filePath.trim()}:${Rooibos.Common.AsString(test.paramLineNumber)})`)}\n`
                    end if

                    if test.result.lineNumber > -1 then
                        resultMessage += `${string(6, " ")}${m.colorLines(rooibos.reporters.mocha.colors.errorStack, `assertion at (file://${test.testSuite.filePath.trim()}:${Rooibos.Common.AsString(test.result.lineNumber)})`)}\n`
                    else
                        resultMessage += `${string(6, " ")}${m.colorLines(rooibos.reporters.mocha.colors.errorStack, `test at (file://${test.testSuite.filePath.trim()}:${Rooibos.Common.AsString(test.lineNumber)})`)}\n`
                    end if
                    print resultMessage
                end if

            end for
        end function

        ' Returns a string representation of the stack trace
        ' example:
        '    Error: some error
        '        $anon_6c() As Dynamic (pkg:/source/FailedAssertion.spec.brs:11)
        '        $anon_303() As Dynamic (pkg:/source/rooibos/Test.brs:45)
        '        $anon_1f2(test As Object) As Dynamic (pkg:/source/rooibos/BaseTestSuite.brs:243)
        '        $anon_30a() As Dynamic (pkg:/source/rooibos/TestGroup.brs:88)
        '        $anon_309() As Dynamic (pkg:/source/rooibos/TestGroup.brs:68)
        '        $anon_1ec() As Dynamic (pkg:/source/rooibos/BaseTestSuite.brs:131)
        '        $anon_1eb() As Dynamic (pkg:/source/rooibos/BaseTestSuite.brs:121)
        '        $anon_325(testsuite As Dynamic) As Void (pkg:/source/rooibos/TestRunner.brs:191)
        '        $anon_322() As Dynamic (pkg:/source/rooibos/TestRunner.brs:72)
        '        rooibos_init(testSceneName As Dynamic) As Void (pkg:/source/rooibos/Rooibos.brs:27)
        '        main(args As Dynamic) As Dynamic (pkg:/source/Main.brs:2)
        function getStackTrace(error as roAssociativeArray, fullTrace = true as boolean) as string
            if fullTrace then
                output = m.colorLines(rooibos.reporters.mocha.colors.errorTitle, `${error.message}\n`)
                indent = 6
            else
                output = ""
                indent = 6
            end if

            foundNonFrameworkFile = false

            for i = error.backTrace.count() - 1 to 0 step -1
                e = error.backTrace[i]
                isFrameworkFile = e.filename.instr("pkg:/source/rooibos") > -1 or e.filename.instr("pkg:/components/rooibos/generated") > -1

                if fullTrace or not isFrameworkFile then
                    output += m.colorLines(rooibos.reporters.mocha.colors.errorStack, `${string(indent, " ")}${e["function"]} (${e.filename.trim()}:${Rooibos.Common.AsString(e.line_number)})`) + chr(10)
                    foundNonFrameworkFile = true
                end if

                if not fullTrace and (foundNonFrameworkFile and isFrameworkFile) then
                    return output
                end if
            end for
            return output
        end function

        ' Returns a unified diff string based on the actual and expected string values
        ' example:
        '      + expected - actual
        '
        '      -0 (Integer)
        '      +true (Boolean)
        function unifiedDiff(actual as string, expected as string) as string
            cleanUp = function(line as string, m as rooibos.MochaTestReporter) as string or dynamic
                indent = "      "
                if line.left(1) = "+" then
                    return indent + m.colorLines(rooibos.reporters.mocha.colors.diffAdded, line)
                end if
                if line.left(1) = "-" then
                    return indent + m.colorLines(rooibos.reporters.mocha.colors.diffRemoved, line)
                end if
                if CreateObject("roRegex", "@@", "").isMatch(line) then
                    return "--"
                end if
                if CreateObject("roRegex", "\\ No newline", "").isMatch(line) then
                    return invalid
                end if
                return indent + line
            end function

            msg = m.createPatch("string", actual, expected)
            lines = msg.split(chr(10)).slice(5)

            final = chr(10) + "      " + m.colorLines(rooibos.reporters.mocha.colors.diffAdded, "+ expected") + " " + m.colorLines(rooibos.reporters.mocha.colors.diffRemoved, "- actual")
            final += chr(10) + chr(10)

            cleanLines = []
            for i = 0 to lines.count() - 1
                cleaned = cleanUp(lines[i], m)
                if cleaned <> invalid then
                    cleanLines.push(cleaned)
                end if
            end for

            return final + cleanLines.join(chr(10))
        end function

        ' Applies Asci colors to each line of a string based on the supplied color type
        function colorLines(name as dynamic, targetString as string) as string
            lines = targetString.split(chr(10))

            for i = 0 to lines.count() - 1
                lines[i] = m.colors(name, lines[i])
            end for

            return lines.join(chr(10))
        end function

        ' Applies Asci colors the supplied of a string based on the supplied color type
        function colors(colorType as string, targetString as string) as string
            ' colors = {
            '   pass: 90,
            '   fail: 31,
            '   "bright pass": 92,
            '   "bright fail": 91,
            '   "bright yellow": 93,
            '   pending: 36,
            '   suite: 0,
            '   "error title": 0,
            '   "error message": 31,
            '   "error stack": 90,
            '   checkMark: 32,
            '   fast: 90,
            '   medium: 33,
            '   slow: 31,
            '   green: 32,
            '   light: 90,
            '   "diff gutter": 90,
            '   "diff added": 32,
            '   "diff removed": 31,
            '   "diff added inline": "30;42",
            '   "diff removed inline": "30;41"
            ' }

            if m.colorizeOutput then
                return chr(27) + "[" + colorType + "m" + targetString + chr(27) + "[0m"
                ' return chr(27) + "[" + colors[colorType].toStr() + "m" + targetString + chr(27) + "[0m"
                ' return "\u001b[" + colors[colorType].toStr() + "m" + targetString + "\u001b[0m"
            else
                return targetString
            end if
        end function

        ' Creates a patch file string based on the the differences of the two supplied strings
        function createPatch(fileName as string, oldStr as string, newStr as string) as string
            result = m.structuredPatch(fileName, fileName, oldStr, newStr, invalid, invalid, {
                context: 4
                newlineIsToken: false
            })
            if result <> invalid then
                return m.formatPatch(result)
            end if
            return invalid
        end function

        ' Generate a structured patch object from two strings
        function structuredPatch(oldFileName as string, newFileName as string, oldStr as string, newStr as string, oldHeader as dynamic, newHeader as dynamic, options as roAssociativeArray) as roAssociativeArray
            if options = invalid then
                options = {}
            end if
            if options.context = invalid then
                options.context = 4
            end if
            if options.newlineIsToken = true then
                throw "newlineIsToken may not be used with patch-generation functions, only with diffing functions"
            end if

            return m.diffLinesResultToPatch(m.diffLines(oldStr, newStr, options), oldFileName, newFileName, oldHeader, newHeader, options)
        end function

        ' Diff two sets of strings, comparing them line by line
        function diffLines(oldStr as string, newStr as string, callback as dynamic) as roAssociativeArray
            lineDiff = rooibos.reporters.mocha.new_lineDiff()

            return lineDiff.diff(oldStr, newStr, {
                ignoreCase: false
                comparator: invalid
                useLongestToken: false
                oneChangePerToken: false
                maxEditLength: invalid
            })
        end function

        ' Convert a diff result into a patch
        function diffLinesResultToPatch(diff as roArray, oldFileName as string, newFileName as string, oldHeader as dynamic, newHeader as dynamic, options as roAssociativeArray) as roAssociativeArray
            ' STEP 1: Build up the patch with no "\ No newline at end of file" lines and with the arrays
            '         of lines containing trailing newline characters. We'll tidy up later...

            if diff = invalid then
                return invalid
            end if

            diff.push({ value: "", lines: [] }) ' Append an empty value to make cleanup easier

            hunks = []
            oldRangeStart = 0
            newRangeStart = 0
            curRange = []
            oldLine = 1
            newLine = 1
            for i = 0 to diff.count() - 1
                current = diff[i]
                if current.lines <> invalid then
                    lines = current.lines
                else
                    lines = m.splitLines(current.value)
                end if
                current.lines = lines

                if current.added = true or current.removed = true then
                    ' If we have previous context, start with that
                    if not (oldRangeStart) = true then
                        prev = diff[i - 1]
                        oldRangeStart = oldLine
                        newRangeStart = newLine

                        if prev <> invalid then
                            if options.context > 0 then
                                curRange = m.contextLines(prev.lines.slice(-options.context))
                            else
                                curRange = []
                            end if
                            oldRangeStart -= curRange.count()
                            newRangeStart -= curRange.count()
                        end if
                    end if

                    ' Output our changes
                    for each entry in lines
                        if current.added then
                            curRange.push("+" + entry)
                        else
                            curRange.push("-" + entry)
                        end if
                    end for

                    ' Track the updated file position
                    if current.added then
                        newLine += lines.count()
                    else
                        oldLine += lines.count()
                    end if
                else
                    ' Identical context lines. Track line changes
                    if oldRangeStart then
                        ' Close out any changes that have been output (or join overlapping)
                        if lines.count() <= options.context * 2 and i < diff.count() - 2 then
                            ' Overlapping
                            curRange.append(m.contextLines(lines))
                        else
                            ' end the range and output
                            contextSize = rooibos.reporters.mocha.min(lines.count(), options.context)
                            curRange.append(m.contextLines(lines.slice(0, contextSize)))

                            hunk = {
                                oldStart: oldRangeStart
                                oldLines: (oldLine - oldRangeStart + contextSize)
                                newStart: newRangeStart
                                newLines: (newLine - newRangeStart + contextSize)
                                lines: curRange
                            }
                            hunks.push(hunk)

                            oldRangeStart = 0
                            newRangeStart = 0 'bs:disable-line LINT1005
                            curRange = [] 'bs:disable-line LINT1005
                        end if
                    end if
                    oldLine += lines.count()
                    newLine += lines.count()
                end if
            end for

            ' Step 2: eliminate the trailing `\n` from each line of each hunk, and, where needed, add
            '         "\ No newline at end of file".
            for each hunk in hunks
                for i = 0 to hunk.lines.count() - 1
                    if hunk.lines[i].endsWith(chr(10)) then
                        hunk.lines[i] = hunk.lines[i].mid(0, len(hunk.lines[i]) - 1)
                    else
                        hunk.lines = rooibos.reporters.mocha.arraySplice(hunk.lines, i + 1, 0, ["\ No newline at end of file"])
                        i++ ' Skip the line we just added, then continue iterating
                    end if
                end for
            end for

            return {
                oldFileName: oldFileName
                newFileName: newFileName
                oldHeader: oldHeader
                newHeader: newHeader
                hunks: hunks
            }
        end function

        ' Split `text` into an array of lines, including the trailing newline character (where present)
        function splitLines(text as string) as roArray
            hasTrailingNl = text.endsWith(chr(10))
            result = rooibos.reporters.mocha.arrayMap(text.split(chr(10)), function(line as string, _ = invalid as dynamic) as string
                return line + chr(10)
            end function)
            if hasTrailingNl then
                result.pop()
            else
                lastEntry = result.pop()
                result.push(lastEntry.mid(0, len(lastEntry) - 1))
            end if
            return result
        end function

        function contextLines(lines as string[]) as string[]
            return rooibos.reporters.mocha.arrayMap(lines, function(entry as string, _ = invalid as dynamic) as string
                return " " + entry
            end function)
        end function

        ' Return a unified patch file contents from a structured patch
        function formatPatch(diff as dynamic, _ = invalid as dynamic) as string
            if type(diff) = "roArray" then
                return rooibos.reporters.mocha.arrayMap(diff, m.formatPatch).join(chr(10))
            end if

            ret = []
            if diff.oldFileName = diff.newFileName then
                ret.push("Index: " + diff.oldFileName)
            end if

            ret.push("===================================================================")
            if diff.oldHeader <> invalid then
                ret.push("--- " + diff.oldFileName + chr(9) + diff.oldHeader)
            else
                ret.push("--- " + diff.oldFileName)
            end if

            if diff.newHeader <> invalid then
                ret.push("+++ " + diff.newFileName + chr(9) + diff.newHeader)
            else
                ret.push("+++ " + diff.newFileName)
            end if

            for i = 0 to diff.hunks.count() - 1
                hunk = diff.hunks[i]
                ' Unified Diff Format quirk: If the chunk size is 0,
                ' the first number is one lower than one would expect.
                ' https://www.artima.com/weblogs/viewpost.jsp?thread=164293
                if hunk.oldLines = 0 then
                    hunk.oldStart -= 1
                end if
                if hunk.newLines = 0 then
                    hunk.newStart -= 1
                end if

                ret.push("@@ -" + hunk.oldStart.toStr() + "," + hunk.oldLines.toStr() + " +" + hunk.newStart.toStr() + "," + hunk.newLines.toStr() + " @@")
                ret.append(hunk.lines)
            end for

            return ret.join(chr(10)) + chr(10)
        end function

    end class

    ' @ignore
    namespace reporters
        ' @ignore
        namespace mocha
            enum colors
                pass = "90"
                fail = "31"
                brightPass = "92"
                brightFail = "91"
                brightYellow = "93"
                pending = "36"
                suite = "0"
                errorTitle = "0"
                errorMessage = "31"
                errorStack = "90"
                checkMark = "32"
                fast = "90"
                medium = "33"
                slow = "31"
                green = "32"
                light = "90"
                diffGutter = "90"
                diffAdded = "32"
                diffRemoved = "31"
                diffAddedInline = "30;42"
                diffRemovedInline = "30;41"
            end enum

            function buildValues(diff as roAssociativeArray, lastComponent as roAssociativeArray, newString as string[], oldString as string[], useLongestToken as boolean or dynamic) as roArray
                ' First we convert our linked list of components in reverse order to an
                ' array in the right order:
                components = []
                nextComponent = invalid
                while lastComponent <> invalid
                    components.push(lastComponent)
                    nextComponent = lastComponent.previousComponent
                    lastComponent.delete("previousComponent")
                    lastComponent = nextComponent
                end while
                components.reverse()

                componentPos = 0
                componentLen = components.count()
                newPos = 0
                oldPos = 0

                for componentPos = 0 to componentLen - 1
                    component = components[componentPos]
                    if not component.removed then
                        if not component.added and useLongestToken = true then
                            value = newString.slice(newPos, newPos + component.count)

                            newValue = createObject("roArray", component.count(), true)
                            for i = 0 to value.count() - 1
                                currentValue = value[i]
                                oldValue = oldString[oldPos + i]
                                if len(oldValue) > len(currentValue) then
                                    newValue[i] = oldValue
                                else
                                    newValue[i] = currentValue
                                end if
                            end for

                            value = newValue
                            component.value = diff.join(value)
                        else
                            component.value = diff.join(newString.slice(newPos, newPos + component.count))
                        end if
                        newPos += component.count

                        ' Common case
                        if not component.added then
                            oldPos += component.count
                        end if
                    else
                        component.value = diff.join(oldString.slice(oldPos, oldPos + component.count))
                        oldPos += component.count
                    end if
                end for

                return components
            end function

            function new_Diff() as roAssociativeArray
                return {
                    diff: function(oldString as string, newString as string, options = {} as roAssociativeArray) as roAssociativeArray
                        Infinity = 2147483647

                        tokenizedOldString = m.removeEmpty(m.tokenize(oldString, options))
                        tokenizedNewString = m.removeEmpty(m.tokenize(newString, options))
                        newLen = tokenizedNewString.count()
                        oldLen = tokenizedOldString.count()

                        editLength = 1
                        maxEditLength = newLen + oldLen
                        if options.maxEditLength <> invalid then
                            maxEditLength = rooibos.reporters.mocha.min(maxEditLength, options.maxEditLength)
                        end if

                        maxExecutionTime = Infinity
                        abortAfterTimestamp = CreateObject("roDateTime").asSeconds() + maxExecutionTime

                        bestPath = rooibos.reporters.mocha.new_objectArray()

                        ' bestPath = [{ oldPos: -1, lastComponent: invalid }]
                        bestPath.set(0, { oldPos: -1, lastComponent: invalid })

                        ' Seed editLength = 0, i.e. the content starts with the same values
                        ' newPos = m.extractCommon(bestPath[0], tokenizedNewString, tokenizedOldString, 0, options)
                        newPos = m.extractCommon(bestPath.get(0), tokenizedNewString, tokenizedOldString, 0, options)
                        if bestPath.get(0).oldPos + 1 >= oldLen and newPos + 1 >= newLen then
                            ' Identity per the equality and tokenizer
                            ' return m.done(buildValues(m, bestPath[0].lastComponent, tokenizedNewString, tokenizedOldString, m.useLongestToken))
                            return m.done(rooibos.reporters.mocha.buildValues(m, bestPath.get(0).lastComponent, tokenizedNewString, tokenizedOldString, m.useLongestToken), options)
                        end if

                        ' Once we hit the right edge of the edit graph on some diagonal k, we can
                        ' definitely reach the end of the edit graph in no more than k edits, so
                        ' there's no point in considering any moves to diagonal k+1 any more (from
                        ' which we're guaranteed to need at least k+1 more edits).
                        ' Similarly, once we've reached the bottom of the edit graph, there's no
                        ' point considering moves to lower diagonals.
                        ' We record this fact by setting minDiagonalToConsider and
                        ' maxDiagonalToConsider to some finite value once we've hit the edge of
                        ' the edit graph.
                        ' This optimization is not faithful to the original algorithm presented in
                        ' Myers's paper, which instead pointlessly extends D-paths off the end of
                        ' the edit graph - see page 7 of Myers's paper which notes this point
                        ' explicitly and illustrates it with a diagram. This has major performance
                        ' implications for some common scenarios. For instance, to compute a diff
                        ' where the new text simply appends d characters on the end of the
                        ' original text of length n, the true Myers algorithm will take O(n+d^2)
                        ' time while this optimization needs only O(n+d) time.
                        minDiagonalToConsider = -Infinity
                        maxDiagonalToConsider = Infinity

                        ' Performs the length of edit iteration. Is a bit fugly as this has to support the
                        ' sync and async mode which is never fun. Loops over execEditLength until a value
                        ' is produced, or until the edit length exceeds options.maxEditLength (if given),
                        ' in which case it will return undefined.
                        execEditParams = {
                            bestPath: bestPath
                            editLength: editLength
                            newString: tokenizedNewString
                            oldString: tokenizedOldString
                            minDiagonalToConsider: minDiagonalToConsider
                            maxDiagonalToConsider: maxDiagonalToConsider
                            options: options
                            newLen: newLen
                            oldLen: oldLen
                        }
                        while execEditParams.editLength <= maxEditLength and CreateObject("roDateTime").asSeconds() <= abortAfterTimestamp
                            execEdit = m.execEditLength(execEditParams)
                            execEditParams = execEdit
                            if execEdit.ret <> invalid then
                                return execEdit.ret
                            end if
                        end while

                        return invalid
                    end function

                    ' Main worker method. checks all permutations of a given edit length for acceptance.
                    execEditLength: function(execEditParams as roAssociativeArray) as roAssociativeArray
                        startingDiagonalPath = rooibos.reporters.mocha.max(execEditParams.minDiagonalToConsider, -execEditParams.editLength) 'bs:disable-line LINT1005
                        diagonalPath = rooibos.reporters.mocha.max(execEditParams.minDiagonalToConsider, -execEditParams.editLength)
                        ' while diagonalPath <= min(execEditParams.maxDiagonalToConsider, execEditParams.editLength)
                        for diagonalPath = rooibos.reporters.mocha.max(execEditParams.minDiagonalToConsider, -execEditParams.editLength) to rooibos.reporters.mocha.min(execEditParams.maxDiagonalToConsider, execEditParams.editLength) step 2
                            removePath = execEditParams.bestPath.get(diagonalPath - 1)
                            addPath = execEditParams.bestPath.get(diagonalPath + 1)
                            if removePath <> invalid then
                                ' No one else is going to attempt to use this value, clear it
                                execEditParams.bestPath.set(diagonalPath - 1, invalid)
                            end if

                            canAdd = false
                            if addPath <> invalid then
                                ' what newPos will be after we do an insertion:
                                addPathNewPos = addPath.oldPos - diagonalPath
                                canAdd = addPath <> invalid and 0 <= addPathNewPos and addPathNewPos < execEditParams.newLen
                            end if

                            canRemove = removePath <> invalid and removePath.oldPos + 1 < execEditParams.oldLen
                            if not canAdd and not canRemove then
                                ' If this path is a terminal then prune
                                execEditParams.bestPath.set(diagonalPath, invalid)
                                continue for
                            end if

                            ' Select the diagonal that we want to branch from. We select the prior
                            ' path whose position in the old string is the farthest from the origin
                            ' and does not pass the bounds of the diff graph
                            if not canRemove or (canAdd and removePath.oldPos < addPath.oldPos) then
                                basePath = m.addToPath(addPath, true, false, 0, execEditParams.options)
                            else
                                basePath = m.addToPath(removePath, false, true, 1, execEditParams.options)
                            end if

                            newPos = m.extractCommon(basePath, execEditParams.newString, execEditParams.oldString, diagonalPath, execEditParams.options)

                            if basePath.oldPos + 1 >= execEditParams.oldLen and newPos + 1 >= execEditParams.newLen then
                                ' If we have hit the end of both strings, then we are done
                                execEditParams.ret = m.done(rooibos.reporters.mocha.buildValues(m, basePath.lastComponent, execEditParams.newString, execEditParams.oldString, m.useLongestToken), execEditParams.options)
                                return execEditParams
                            else
                                execEditParams.bestPath.set(diagonalPath, basePath)
                                if basePath.oldPos + 1 >= execEditParams.oldLen then
                                    execEditParams.maxDiagonalToConsider = rooibos.reporters.mocha.min(execEditParams.maxDiagonalToConsider, diagonalPath - 1)
                                end if
                                if newPos + 1 >= execEditParams.newLen then
                                    execEditParams.minDiagonalToConsider = rooibos.reporters.mocha.max(execEditParams.minDiagonalToConsider, diagonalPath + 1)
                                end if
                            end if
                        end for

                        execEditParams.editLength++
                        return execEditParams
                    end function

                    addToPath: function(path as roAssociativeArray, added as boolean, removed as boolean, oldPosInc as integer, options as roAssociativeArray) as roAssociativeArray
                        last = path.lastComponent
                        if last <> invalid and not options.oneChangePerToken and last.added = added and last.removed = removed then
                            return {
                                oldPos: path.oldPos + oldPosInc
                                lastComponent: { count: last.count + 1, added: added, removed: removed, previousComponent: last.previousComponent }
                            }
                        else
                            return {
                                oldPos: path.oldPos + oldPosInc
                                lastComponent: { count: 1, added: added, removed: removed, previousComponent: last }
                            }
                        end if
                    end function

                    extractCommon: function(basePath as roAssociativeArray, newString as string[], oldString as string[], diagonalPath as integer, options as roAssociativeArray) as integer
                        newLen = newString.count()
                        oldLen = oldString.count()
                        oldPos = basePath.oldPos
                        newPos = oldPos - diagonalPath

                        commonCount = 0
                        while newPos + 1 < newLen and oldPos + 1 < oldLen and m.equals(oldString[oldPos + 1], newString[newPos + 1], options)
                            newPos++
                            oldPos++
                            commonCount++
                            if options.oneChangePerToken then
                                basePath.lastComponent = { count: 1, previousComponent: basePath.lastComponent, added: false, removed: false }
                            end if
                        end while

                        if commonCount and not options.oneChangePerToken then
                            basePath.lastComponent = { count: commonCount, previousComponent: basePath.lastComponent, added: false, removed: false }
                        end if

                        basePath.oldPos = oldPos
                        return newPos
                    end function

                    equals: function(left as string, right as string, options as roAssociativeArray) as boolean
                        if options.comparator <> invalid then
                            return options.comparator(left, right)
                        else
                            return left = right or (options.ignoreCase = true and lCase(left) = lCase(right))
                        end if
                    end function

                    removeEmpty: function(array as string[]) as roArray
                        ret = []
                        for i = 0 to array.count() - 1
                            if array[i] <> "" then
                                ret.push(array[i])
                            end if
                        end for
                        return ret
                    end function

                    tokenize: function(value as string, options as roAssociativeArray) as string[]
                        return value.split("")
                    end function

                    join: function(chars as string[]) as string
                        return chars.join("")
                    end function

                    postProcess: function(changeObjects as dynamic, options as roAssociativeArray) as dynamic
                        return changeObjects
                    end function

                    done: function(value as dynamic, options as roAssociativeArray) as dynamic
                        value = m.postProcess(value, options)
                        return value
                    end function
                }
            end function

            function arrayMap(arr as object, callback as function) as roArray
                if type(arr) <> "roArray" then
                    print "Error: First argument must be an array."
                    return invalid
                end if

                if type(callback) <> "Function" then
                    print "Error: Second argument must be a function."
                    return invalid
                end if

                ' Create a new array to store the results
                result = []
                for each item in arr
                    ' Apply the callback function to the item
                    transformedItem = callback(item)
                    result.Push(transformedItem)
                end for

                return result
            end function

            function min(a as dynamic, b as dynamic) as dynamic
                if a < b then
                    return a
                else
                    return b
                end if
            end function

            function max(a as dynamic, b as dynamic) as dynamic
                if a > b then
                    return a
                else
                    return b
                end if
            end function

            function arraySplice(array as dynamic[], start as integer, deleteCount as integer, items = [] as dynamic) as dynamic[]
                partOne = array.slice(0, start)
                partTwo = array.slice(start + deleteCount)

                if items <> invalid then
                    partOne.append(items)
                end if

                partOne.append(partTwo)
                return partOne
            end function

            function new_lineDiff() as roAssociativeArray
                lineDiff = rooibos.reporters.mocha.new_Diff()
                lineDiff.tokenize = function(value as string, options as roAssociativeArray) as string[]
                    if options.stripTrailingCr = true then
                        ' remove one \r before \n to match GNU diff's --strip-trailing-cr behavior
                        value = CreateObject("roRegex", "\r\n", "g").ReplaceAll(value, chr(10))
                    end if

                    retLines = [] 'bs:disable-line LINT1005
                    linesAndNewlines = CreateObject("roRegex", "(\n|\r\n)", "g").split(value).toArray()

                    ' Ignore the final empty token that occurs if the string ends with a new line
                    if linesAndNewlines[linesAndNewlines.count() - 1] = "" then
                        linesAndNewlines.pop()
                    end if

                    ' Add the newlines back that where stripped out by the split
                    for i = 0 to linesAndNewlines.count() - 2
                        linesAndNewlines[i] = linesAndNewlines[i] + chr(10)
                    end for

                    ' Merge the content and line separators into single tokens
                    ' for  i = 0 to linesAndNewlines.count() - 1
                    '   line = linesAndNewlines[i]

                    '   if i mod 2 and not options.newlineIsToken = true
                    '     retLines[retLines.count() - 1] = retLines[retLines.count() - 1] + line
                    '   else
                    '     retLines.push(line)
                    '   end if
                    ' end for

                    return linesAndNewlines
                end function

                lineDiff.equals = function(leftPart as string, rightPart as string, options as roAssociativeArray) as boolean
                    ' If we're ignoring whitespace, we need to normalize lines by stripping
                    ' whitespace before checking equality. (This has an annoying interaction
                    ' with newlineIsToken that requires special handling: if newlines get their
                    ' own token, then we DON'T want to trim the *newline* tokens down to empty
                    ' strings, since this would cause us to treat whitespace-only line content
                    ' as equal to a separator between lines, which would be weird and
                    ' inconsistent with the documented behavior of the options.)
                    if options.ignoreWhitespace = true then
                        if not options.newlineIsToken = true or leftPart.inStr(0, chr(10)) > -1 then
                            leftPart = leftPart.trim()
                        end if
                        if not options.newlineIsToken = true or rightPart.inStr(0, chr(10)) > -1 then
                            rightPart = rightPart.trim()
                        end if
                    else if options.ignoreNewlineAtEof = true and not options.newlineIsToken = true then
                        if leftPart.endsWith(chr(10)) then
                            leftPart = leftPart.mid(0, len(leftPart) - 1)
                        end if
                        if rightPart.endsWith(chr(10)) then
                            rightPart = rightPart.mid(0, len(rightPart) - 1)
                        end if
                    end if
                    return rooibos.reporters.mocha.new_Diff().equals(leftPart, rightPart, options)
                end function

                return lineDiff
            end function

            function new_objectArray() as roAssociativeArray
                return {
                    array: []
                    aa: {}

                    count: function() as integer
                        return m.array.count()
                    end function

                    set: function(index as integer, value as dynamic)
                        if index >= 0 then
                            m.array[index] = value
                        else
                            m.aa[index.toStr()] = value
                        end if
                    end function

                    get: function(index as integer) as dynamic
                        if index >= 0 then
                            return m.array[index]
                        else
                            return m.aa[index.toStr()]
                        end if
                    end function
                }
            end function

        end namespace
    end namespace
end namespace
