/**
 * Fragment used to represent a string fragment in the diff.
 */
var Fragment = function Fragment(string) {
    this.content = string;
    this.equiv = false;
};

/**
 * Wrap in given tag or return the clean value.
 */
Fragment.prototype.toString = function (tag) {
    if (this.equiv || !tag) {
        return this.content;
    }
    else {
        return '<' + tag + '>' + this.content + '</' + tag + '>';
    }
};

var moveToEnd = function (a, i, k) {
    if (!a.equiv && (!k[i-1] || k[i-1].equiv)) {
        // Find next item equiv item.
        for (var j = i+1; k[j] && !k[j].equiv; j++);
        if (k[j] && k[j].content === a.content) {
            k[i] = k[j];
            k[j] = a;
        }
    }
};

var aggregate = function (a, i, k) {
    if (!a.equiv && k[i+1] && !k[i+1].equiv) {
        k[i+1].content = a.content + k[i+1].content;
        delete k[i];
    }
};

var join = function (what, t) {
    return what.map(function (a) {
        if (a) {
            return a.toString(t);
        }
        return null;
    }).join('');
};

var clone = function(source) {
    if (typeof source === 'object' && source !== null) {
        var target = Array.isArray(source) ? [] : {};
        for (var key in source) target[key] = clone(source[key]);
        return target;
    }
    return source;
};

var WordDiff = {
    nonWord: /(&.+?;|[\u0000-\u0040\u005B-\u0060\u007B-\u00A9\u00AB-\u00B4\u00B6-\u00B9\u00BB-\u00BF\u00D7\u00F7\u02C2-\u02C5\u02D2-\u02DF\u02E5-\u02EB\u02ED\u02EF-\u036F\u0375\u037E\u0384\u0385\u0387\u03F6\u0482-\u0489\u055A-\u055F\u0589\u058A\u0591-\u05C7\u05F3\u05F4\u0600-\u0603\u0606-\u061B\u061E\u061F\u064B-\u065E\u0660-\u066D\u0670\u06D4\u06D6-\u06E4\u06EA-\u06ED\u06F0-\u06F9\u06FD\u06FE\u0700-\u070D\u070F\u0711\u0730-\u074A\u07A6-\u07B0\u07C0-\u07C9\u07EB-\u07F3\u07F6-\u07F9\u0901-\u0903\u093C\u093E-\u094D\u0951-\u0954\u09E2\u0962-\u0970\u06E7-\u06E9\u0981-\u0983\u09BC\u09BE-\u09C4\u09C7\u09C8\u09CB-\u09CD\u09D7\u09E3\u09E6-\u09EF\u09F2-\u09FA\u0A01-\u0A03\u0A3C\u0A3E-\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A66-\u0A71\u0A75\u0A81-\u0A83\u0ABC\u0ABE-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD\u0AE2\u0AE3\u0AE6-\u0AEF\u0AF1\u0B01-\u0B03\u0B3C\u0B3E-\u0B44\u0B47\u0B48\u0B4B-\u0B4D\u0B56\u0B57\u0B62\u0B63\u0B66-\u0B70\u0B82\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD7\u0BE6-\u0BFA\u0C01-\u0C03\u0C3E-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C62\u0C63\u0C66-\u0C6F\u0C78-\u0C7F\u0C82\u0C83\u0CBC\u0CBE-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5\u0CD6\u0CE2\u0CE3\u0CE6-\u0CEF\u0CF1\u0CF2\u0D02\u0D03\u0D3E-\u0D44\u0D46-\u0D48\u0D4A-\u0D4D\u0D57\u0D62\u0D63\u0D66-\u0D75\u0D79\u0D82\u0D83\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DF2-\u0DF4\u0E31\u0E34-\u0E3A\u0E3F\u0E47-\u0E5B\u0EB1\u0EB4-\u0EB9\u0EBB\u0EBC\u0EC8-\u0ECD\u0ED0-\u0ED9\u0F01-\u0F3F\u0F71-\u0F87\u0F90-\u0F97\u0F99-\u0FBC\u0FBE-\u0FCC\u0FCE-\u0FD4\u102B-\u103E\u1040-\u104F\u1056-\u1059\u105E-\u1060\u1062-\u1064\u1067-\u106D\u1071-\u1074\u1082-\u108D\u108F-\u1099\u109E\u109F\u10FB\u135F-\u137C\u1390-\u1399\u166D\u166E\u1680\u169B\u169C\u16EB-\u16F0\u1712-\u1714\u1732-\u1736\u1752\u1753\u1772\u1773\u17B4-\u17D6\u17D8-\u17DB\u17DD\u17E0-\u17E9\u17F0-\u17F9\u1800-\u180E\u1810-\u1819\u18A9\u1920-\u192B\u1930-\u193B\u1940\u1944-\u194F\u19B0-\u19C0\u19C8\u19C9\u19D0-\u19D9\u19DE-\u19FF\u1A17-\u1A1B\u1A1E\u1A1F\u1B00-\u1B04\u1B34-\u1B44\u1B50-\u1B7C\u1B80-\u1B82\u1BA1-\u1BAA\u1BB0-\u1BB9\u1C24-\u1C37\u1C3B-\u1C49\u1C50-\u1C59\u1C7E\u1C7F\u1DC0-\u1DE6\u1DFE\u1DFF\u1FBD\u1FBF-\u1FC1\u1FCD-\u1FCF\u1FDD-\u1FDF\u1FED-\u1FEF\u1FFD\u1FFE\u2000-\u2064\u206A-\u2070\u2074-\u207E\u2080-\u208E\u20A0-\u20B5\u20D0-\u20F0\u2100\u2101\u2103-\u2106\u2108\u2109\u2114\u2116-\u2118\u211E-\u2123\u2125\u2127\u2129\u212E\u213A\u213B\u2140-\u2144\u214A-\u214D\u214F\u2153-\u2182\u2185-\u2188\u2190-\u23E7\u2400-\u2426\u2440-\u244A\u2460-\u269D\u26A0-\u26BC\u26C0-\u26C3\u2701-\u2704\u2706-\u2709\u270C-\u2727\u2729-\u274B\u274D\u274F-\u2752\u2756\u2758-\u275E\u2761-\u2794\u2798-\u27AF\u27B1-\u27BE\u27C0-\u27CA\u27CC\u27D0-\u2B4C\u2B50-\u2B54\u2CE5-\u2CEA\u2CF9-\u2CFF\u2DE0-\u2E2E\u2E30\u2E80-\u2E99\u2E9B-\u2EF3\u2F00-\u2FD5\u2FF0-\u2FFB\u3000-\u3004\u3007-\u3030\u3036-\u303A\u303D-\u303F\u3099-\u309C\u30A0\u30FB\u3190-\u319F\u31C0-\u31E3\u3200-\u321E\u3220-\u3243\u3250-\u32FE\u3300-\u33FF\u4DC0-\u4DFF\uA490-\uA4C6\uA60D-\uA60F\uA620-\uA629\uA66F-\uA673\uA67C-\uA67E\uA700-\uA716\uA720\uA721\uA789\uA78A\uA802\uA806\uA80B\uA823-\uA82B\uA874-\uA877\uA880\uA881\uA8B4-\uA8C4\uA8CE-\uA8D9\uA900-\uA909\uA926-\uA92F\uA947-\uA953\uA95F\uAA29-\uAA36\uAA43\uAA4C\uAA4D\uAA50-\uAA59\uAA5C-\uAA5F\uD800\uDB7F\uDB80\uDBFF\uDC00\uDFFF\uE000\uF8FF\uFB1E\uFB29\uFD3E\uFD3F\uFDFC\uFDFD\uFE00-\uFE19\uFE20-\uFE26\uFE30-\uFE52\uFE54-\uFE66\uFE68-\uFE6B\uFEFF\uFF01-\uFF20\uFF3B-\uFF40\uFF5B-\uFF65\uFFE0-\uFFE6\uFFE8-\uFFEE\uFFF9-\uFFFD])/,

    tokenize: function (args) {
        // Split on non-word characters.
        for (var type in args) {
            args[type] = args[type].split(WordDiff.nonWord).filter(function (s) {
                return s.length;
            });
        }

        // Calculate the indexes and offsets for common suffixes and prefixes.
        var i = -1, j = args.del.length, k = args.ins.length;
        while (args.del[++i] === args.ins[i] && i <= j);
        while (j >= i && k >= i && args.del[--j] === args.ins[--k]);

        args.prefix = args.del.slice(0, i).join('');
        args.suffix = args.del.slice(j + 1).join('');
        args.del = args.del.slice(i, ++j);
        args.ins = args.ins.slice(i, ++k);
    },

    lcs: function (args) {
        var matrix = [];

        for (var i = 0; i < args.del.length; i++) {
            matrix[i] = [];
            for (var j = 0; j < args.ins.length; j++) {
                if (args.del[i] === args.ins[j]) {
                    matrix[i][j] = (matrix[i - 1] && matrix[i - 1][j - 1] || 0) + args.del[i].length;
                }
                else {
                    matrix[i][j] = Math.max(matrix[i][j - 1] || 0, matrix[i - 1] && matrix[i - 1][j] || 0);
                }
            }
        }

        return matrix;
    },

    changeset: function (args, matrix) {
        var result = {};

        ['del', 'ins'].forEach(function (type) {
            result[type] = args[type].map(function (a) { return new Fragment(a); });
        });

        // Backtrack through the matrix.
        for (var i = result.del.length - 1, j = result.ins.length - 1; i >= 0; i--, j--) {
            if (j < 0 || result.del[i].content !== result.ins[j].content) {
                if (j < 0 || (j > 0 && matrix[i - 1] && (matrix[i][j - 1] < matrix[i - 1][j]))) {
                    j++;
                }
                else {
                    i++;
                }
            }
            else {
                result.del[i] = result.ins[j];
                result.del[i].equiv = true;
            }
        }

        // Fill up gaps.
        for (var i2 = 0; i2 < result.del.length; i2++) {
            if (result.del[i2].equiv && result.del[i2].content.length < 3) {
                var j2 = result.ins.indexOf(result.del[i2]);
                if (result.del[i2-1] && result.del[i2+1] && result.ins[j2-1] && result.ins[j2+1] && !result.del[i2-1].equiv && !result.del[i2+1].equiv && !result.ins[j2-1].equiv && !result.ins[j2+1].equiv){
                    result.del[i2].equiv = false;
                    result.ins[j2] = clone(result.del[i2]);
                }
            }
        }

        ['del', 'ins'].forEach(function (type) {
            // Try to move changes to the end.
            for (var i = 0; i < result[type].length; i++)
                moveToEnd(result[type][i], i, result[type]);

            // Aggregate subsequent changes to minimize ins/del tags.
            for (var i2 = 0; i2 < result[type].length; i2++)
                aggregate(result[type][i2], i2, result[type]);
        });

        return result;
    },

    htmlRender: function (args, result) {
        var diff = {
            del: args.prefix + join(result.del, 'del') + args.suffix,
            ins: args.prefix + join(result.ins, 'ins') + args.suffix
        };

        return diff;
    },

    htmlDiff: function (del, ins) {
        var args = { 'del': del, 'ins': ins };

        WordDiff.tokenize(args);
        var matrix = WordDiff.lcs(args);
        var result = WordDiff.changeset(args, matrix);
        return WordDiff.htmlRender(args, result);
    },

    render: function (args, result) {
        var join = function (what, type) {
            return what.map(function (a) {
                if (!a) return null;
                if (a.equiv) return a.content;
                if (type == 'del') return '\x1B[31;4m' + a.content + '\x1B[0m';
                if (type == 'ins') return '\x1B[32;4m' + a.content + '\x1B[0m';
                return null;
            }).join('');
        };

        return {
            del: args.prefix + join(result.del, 'del') + args.suffix,
            ins: args.prefix + join(result.ins, 'ins') + args.suffix
        };
    },

    diff: function(del, ins) {
        var args = { 'del': del, 'ins': ins };

        WordDiff.tokenize(args);
        var matrix = WordDiff.lcs(args);
        var result = WordDiff.changeset(args, matrix);
        return WordDiff.render(args, result);
    }
};

module.exports = WordDiff;
