/// <summary>
        /// Diff two JSON objects.
        ///
        /// The output is a JObject that contains enough information to represent the
        /// delta between the two objects and to be able perform patch and reverse operations.
        /// </summary>
        /// <param name="left">The base JSON object</param>
        /// <param name="right">The JSON object to compare against the base</param>
        /// <returns>JSON Patch Document</returns>
        public JToken Diff(JToken left, JToken right)
        {
            var objectHash = this._options.ObjectHash;
            var itemMatch  = new DefaultItemMatch(objectHash);

            if (left == null)
            {
                left = new JValue("");
            }
            if (right == null)
            {
                right = new JValue("");
            }

            if (left.Type == JTokenType.Object && right.Type == JTokenType.Object)
            {
                return(ObjectDiff((JObject)left, (JObject)right));
            }

            if (_options.ArrayDiff == ArrayDiffMode.Efficient &&
                left.Type == JTokenType.Array &&
                right.Type == JTokenType.Array)
            {
                return(ArrayDiff((JArray)left, (JArray)right));
            }

            if (_options.TextDiff == TextDiffMode.Efficient &&
                left.Type == JTokenType.String &&
                right.Type == JTokenType.String &&
                (left.ToString().Length > _options.MinEfficientTextDiffLength || right.ToString().Length > _options.MinEfficientTextDiffLength))
            {
                var          dmp     = new diff_match_patch();
                List <Patch> patches = dmp.patch_make(left.ToObject <string>(), right.ToObject <string>());
                return(patches.Any()
                                        ? new JArray(dmp.patch_toText(patches), 0, (int)DiffOperation.TextDiff)
                                        : null);
            }

            if (!itemMatch.Match(left, right))
            {
                return(new JArray(left, right));
            }

            return(null);
        }
        private JObject ArrayDiff(JArray left, JArray right)
        {
            var objectHash = this._options.ObjectHash;
            var itemMatch  = new DefaultItemMatch(objectHash);
            var result     = JObject.Parse(@"{ ""_t"": ""a"" }");

            int commonHead = 0;
            int commonTail = 0;

            if (itemMatch.Match(left, right))
            {
                return(null);
            }

            var childContext = new List <JToken>();

            // Find common head
            while (commonHead < left.Count &&
                   commonHead < right.Count &&
                   itemMatch.Match(left[commonHead], right[commonHead]))
            {
                var index = commonHead;
                var child = Diff(left[index], right[index]);
                if (child != null)
                {
                    result[$"{index}"] = child;
                }
                commonHead++;
            }

            // Find common tail
            while (commonTail + commonHead < left.Count &&
                   commonTail + commonHead < right.Count &&
                   itemMatch.Match(left[left.Count - 1 - commonTail], right[right.Count - 1 - commonTail]))
            {
                var index1 = left.Count - 1 - commonTail;
                var index2 = right.Count - 1 - commonTail;
                var child  = Diff(left[index1], right[index2]);
                if (child != null)
                {
                    result[$"{index2}"] = child;
                }
                commonTail++;
            }

            if (commonHead + commonTail == left.Count)
            {
                // Trivial case, a block (1 or more consecutive items) was added
                for (int index = commonHead; index < right.Count - commonTail; ++index)
                {
                    result[$"{index}"] = new JArray(right[index]);
                }

                return(result);
            }
            if (commonHead + commonTail == right.Count)
            {
                // Trivial case, a block (1 or more consecutive items) was removed
                for (int index = commonHead; index < left.Count - commonTail; ++index)
                {
                    if (result.ContainsKey(index.ToString()))
                    {
                        result.Remove(index.ToString());
                    }
                    result[$"_{index}"] = new JArray(left[index], 0, (int)DiffOperation.Deleted);
                }

                return(result);
            }

            // Complex Diff, find the LCS (Longest Common Subsequence)
            List <JToken> trimmedLeft  = left.ToList().GetRange(commonHead, left.Count - commonTail - commonHead);
            List <JToken> trimmedRight = right.ToList().GetRange(commonHead, right.Count - commonTail - commonHead);
            Lcs           lcs          = Lcs.Get(trimmedLeft, trimmedRight, itemMatch);

            for (int index = commonHead; index < left.Count - commonTail; ++index)
            {
                if (lcs.Indices1.IndexOf(index - commonHead) < 0)
                {
                    // Removed
                    if (result.ContainsKey(index.ToString()))
                    {
                        result.Remove(index.ToString());
                    }
                    result[$"_{index}"] = new JArray(left[index], 0, (int)DiffOperation.Deleted);
                }
            }

            for (int index = commonHead; index < right.Count - commonTail; index++)
            {
                int indexRight = lcs.Indices2.IndexOf(index - commonHead);

                if (indexRight < 0)
                {
                    // Added
                    result[$"{index}"] = new JArray(right[index]);
                }
                else
                {
                    int li = lcs.Indices1[indexRight] + commonHead;
                    int ri = lcs.Indices2[indexRight] + commonHead;

                    JToken diff = Diff(left[li], right[ri]);

                    if (diff != null)
                    {
                        result[$"{index}"] = diff;
                    }
                }
            }

            return(result);
        }