/// <summary>
        /// Tests the string mapping.
        /// </summary>
        /// <param name="failures">The number of failures.</param>
        /// <param name="successes">The number of successes.</param>
        /// <param name="expected">The expected output mapped string.</param>
        /// <param name="input">The input string.</param>
        /// <param name="options">The options.</param>
        private static void TestMap(
            ref int failures,
            ref int successes,
            string expected,
            string input,
            TextOptions options)
        {
            Trace.WriteLine(new string('=', 80));

            Assert.IsNotNull(input);
            Assert.IsNotNull(expected);
            // Generate map
            StringMap map    = input.ToMapped(options);
            string    output = map.Mapped;

            Trace.WriteLine($"Options:'{options}' String:'{input.Escape()}' Map: '{output.Escape()}'");

            bool failed = false;

            if (!string.Equals(expected, output, StringComparison.Ordinal))
            {
                Trace.WriteLine($"FAILED - Expected a mapped string of '{expected.Escape()}'");
                failed = true;
            }
            else
            {
                for (int i = 0; i < map.Count; i++)
                {
                    char o = output[i];
                    int  j = map.GetOriginalIndex(i);
                    if (j < 0 || j >= input.Length)
                    {
                        Trace.WriteLine(
                            $"FAILED - Mapped character '{o.ToString().Escape()}' at index '{i}' has invalid mapping to index '{j}'.");
                        failed = true;
                        continue;
                    }
                    char m = input[j];
                    if (o != m)
                    {
                        Trace.WriteLine(
                            $"FAILED - Expected mapped character '{o.ToString().Escape()}' at index '{i}' to equal '{m.ToString().Escape()}' at index '{j}'.");
                        failed = true;
                    }
                }
            }

            Trace.WriteLine(string.Empty);

            if (failed)
            {
                failures++;
            }
            else
            {
                successes++;
            }
        }
        /// <summary>
        /// Initializes a new instance of the <see cref="StringDifferences" /> class.
        /// </summary>
        /// <param name="a">The 'A' string.</param>
        /// <param name="offsetA">The offset to the start of a window in the first string.</param>
        /// <param name="lengthA">The length of the window in the first string.</param>
        /// <param name="b">The 'B' string.</param>
        /// <param name="offsetB">The offset to the start of a window in the second string.</param>
        /// <param name="lengthB">The length of the window in the second string.</param>
        /// <param name="textOptions">The text options.</param>
        /// <param name="comparer">The character comparer.</param>
        /// <exception cref="ArgumentNullException"><paramref name="a" /> is <see langword="null" />.</exception>
        /// <exception cref="ArgumentNullException"><paramref name="b" /> is <see langword="null" />.</exception>
        /// <exception cref="ArgumentNullException"><paramref name="comparer" /> is <see langword="null" />.</exception>
        /// <exception cref="ArgumentOutOfRangeException">The <paramref name="offsetA" /> is out of range.</exception>
        /// <exception cref="ArgumentOutOfRangeException">The <paramref name="lengthA" /> is out of range.</exception>
        /// <exception cref="ArgumentOutOfRangeException">The <paramref name="offsetB" /> is out of range.</exception>
        /// <exception cref="ArgumentOutOfRangeException">The <paramref name="lengthB" /> is out of range.</exception>
        /// <exception cref="Exception">The <paramref name="comparer" /> throws an exception.</exception>
        internal StringDifferences(
            [NotNull] string a,
            int offsetA,
            int lengthA,
            [NotNull] string b,
            int offsetB,
            int lengthB,
            TextOptions textOptions,
            [NotNull] Func <char, char, bool> comparer)
        {
            if (a == null)
            {
                throw new ArgumentNullException(nameof(a));
            }
            if (b == null)
            {
                throw new ArgumentNullException(nameof(b));
            }
            if (comparer == null)
            {
                throw new ArgumentNullException(nameof(comparer));
            }
            A = a;
            B = b;

            if (textOptions != TextOptions.None)
            {
                // Wrap the comparer with an additional check to handle special characters.
                Func <char, char, bool> oc = comparer;
                if (textOptions.HasFlag(TextOptions.IgnoreWhiteSpace))
                {
                    // Ignore white space - treat all whitespace as the same (note this will handle line endings too).
                    comparer = (x, y) => char.IsWhiteSpace(x) ? char.IsWhiteSpace(y) : oc(x, y);
                }
                else if (textOptions.HasFlag(TextOptions.NormalizeLineEndings))
                {
                    // Just normalize line endings - treat '\r' and '\n\ as the same
                    comparer = (x, y) => x == '\r' || x == '\n' ? y == '\r' || y == '\n' : oc(x, y);
                }
            }

            // Map strings based on text options
            StringMap aMap = a.ToMapped(textOptions);
            StringMap bMap = b.ToMapped(textOptions);

            // Perform diff on mapped string
            Differences <char> chunks = aMap.Diff(bMap, comparer);

            // Special case simple equality
            if (chunks.Count < 2)
            {
                Chunk <char> chunk = chunks.Single();
                // ReSharper disable once PossibleNullReferenceException
                _chunks = new[] { new StringChunk(chunk.AreEqual, a, 0, b, 0) };
                return;
            }

            // To reverse the mapping we first calculate the split points in the original strings, and find
            // the last reference to the original strings in each chunk.
            int[] aEnds = new int[chunks.Count];
            int[] bEnds = new int[chunks.Count];
            int   lastA = 0;
            int   lastB = 0;

            for (int i = 0; i < chunks.Count; i++)
            {
                Chunk <char>          chunk  = chunks[i];
                ReadOnlyWindow <char> chunkA = chunk.A;
                ReadOnlyWindow <char> chunkB = chunk.B;
                if (chunk.A != null)
                {
                    aEnds[i] = aMap.GetOriginalIndex(chunkA.Offset + chunkA.Count - 1) + 1;
                    lastA    = i;
                }
                else
                {
                    aEnds[i] = -1;
                }

                if (chunk.B != null)
                {
                    bEnds[i] = bMap.GetOriginalIndex(chunkB.Offset + chunkB.Count - 1) + 1;
                    lastB    = i;
                }
                else
                {
                    bEnds[i] = -1;
                }
            }

            // Now we're ready to build up a new chunk array based on the original strings
            StringChunk[] stringChunks = new StringChunk[chunks.Count];
            int           aStart       = 0;
            int           bStart       = 0;

            for (int i = 0; i < chunks.Count; i++)
            {
                int aEnd = i == lastA ? aMap.OriginalCount : aEnds[i];
                int bEnd = i == lastB ? bMap.OriginalCount : bEnds[i];

                string ac = aEnd > -1 ? a.Substring(aStart, aEnd - aStart) : null;
                string bc = bEnd > -1 ? b.Substring(bStart, bEnd - bStart) : null;

                stringChunks[i] = new StringChunk(chunks[i].AreEqual, ac, aEnd > -1 ? aStart : -1, bc, bEnd > -1 ? bStart : -1);

                if (aEnd > -1)
                {
                    aStart = aEnd;
                }
                if (bEnd > -1)
                {
                    bStart = bEnd;
                }
            }

            _chunks = stringChunks;
        }