/**
 * Diff tools.
 * @module
 */

'use strict';

var simpleDiff = require('simplediff');

var Util = require('./Util.js').Util;

/** @namespace */
var Diff = {};

/** @func */
Diff.convertDiffToOffsetPairs = function(diff, srcLengths, outLengths) {
	var currentPair;
	var pairs = [];
	var srcOff = 0;
	var outOff = 0;
	var srcIndex = 0;
	var outIndex = 0;
	diff.forEach(function(change) {
		var pushPair = function(pair, start) {
			if (!pair.added) {
				pair.added = { start: start, end: start };
			} else if (!pair.removed) {
				pair.removed = { start: start, end: start };
			}
			pairs.push([ pair.removed, pair.added ]);
			currentPair = {};
		};

		// Use original line lengths;
		var srcLen = 0;
		var outLen = 0;
		change[1].forEach(function() {
			if (change[0] === '+') {
				outLen += outLengths[outIndex];
				outIndex++;
			} else if (change[0] === '-') {
				srcLen += srcLengths[srcIndex];
				srcIndex++;
			} else {
				srcLen += srcLengths[srcIndex];
				outLen += outLengths[outIndex];
				srcIndex++;
				outIndex++;
			}
		});

		if (!currentPair) {
			currentPair = {};
		}

		if (change[0] === '+') {
			if (currentPair.added) {
				pushPair(currentPair, srcOff); // srcOff used for adding pair.removed
			}

			currentPair.added = { start: outOff };
			outOff += outLen;
			currentPair.added.end = outOff;

			if (currentPair.removed) {
				pushPair(currentPair);
			}
		} else if (change[0] === '-') {
			if (currentPair.removed) {
				pushPair(currentPair, outOff); // outOff used for adding pair.added
			}

			currentPair.removed = { start: srcOff };
			srcOff += srcLen;
			currentPair.removed.end = srcOff;

			if (currentPair.added) {
				pushPair(currentPair);
			}
		} else {
			if (currentPair.added || currentPair.removed) {
				pushPair(currentPair, currentPair.added ? srcOff : outOff);
			}

			srcOff += srcLen;
			outOff += outLen;
		}
	});
	return pairs;
};

/** @func */
Diff.convertChangesToXML = function(changes) {
	var result = [];
	for (var i = 0; i < changes.length; i++) {
		var change = changes[i];
		if (change[0] === '+') {
			result.push('<ins>');
		} else if (change[0] === '-') {
			result.push('<del>');
		}

		result.push(Util.escapeHtml(change[1].join('')));

		if (change[0] === '+') {
			result.push('</ins>');
		} else if (change[0] === '-') {
			result.push('</del>');
		}
	}
	return result.join('');
};

var diffTokens = function(oldString, newString, tokenize) {
	if (oldString === newString) {
		return [['=', [newString]]];
	} else {
		return simpleDiff.diff(tokenize(oldString), tokenize(newString));
	}
};

/** @func */
Diff.diffWords = function(oldString, newString) {
	// This is a complicated regexp, but it improves on the naive \b by:
	// * keeping tag-like things (<pre>, <a, </a>, etc) together
	// * keeping possessives and contractions (don't, etc) together
	// * ensuring that newlines always stay separate, so we don't
	//   have diff chunks that contain multiple newlines
	//   (ie, "remove \n\n" followed by "add \n", instead of
	//   "keep \n", "remove \n")
	var wordTokenize =
		value => value.split(/((?:<\/?)?\w+(?:'\w+|>)?|\s(?:(?!\n)\s)*)/g).filter(
			// For efficiency, filter out zero-length strings from token list
			// UGLY HACK: simplediff trips if one of tokenized words is
			// 'constructor'. Since this failure breaks parserTests.js runs,
			// work around that by hiding that diff for now.
			s => s !== '' && s !== 'constructor'
		);
	return diffTokens(oldString, newString, wordTokenize);
};

/** @func */
Diff.diffLines = function(oldString, newString) {
	var lineTokenize = function(value) {
		return value.split(/^/m).map(function(line) {
			return line.replace(/\r$/g, '\n');
		});
	};
	return diffTokens(oldString, newString, lineTokenize);
};

/** @func */
Diff.colorDiff = function(a, b, options) {
	const context = options && options.context;
	let diffs = 0;
	let buf = '';
	let before = '';
	const visibleWs = s => s.replace(/[ \xA0]/g,'\u2423');
	const funcs = (options && options.html) ? {
		'+': s => '<font color="green">' + Util.escapeHtml(visibleWs(s)) + '</font>',
		'-': s => '<font color="red">' + Util.escapeHtml(visibleWs(s)) + '</font>',
		'=': s => Util.escapeHtml(s),
	} : (options && options.noColor) ? {
		'+': s => '{+' + s + '+}',
		'-': s => '{-' + s + '-}',
		'=': s => s,
	} : {
		// add '' to workaround color bug; make spaces visible
		'+': s => visibleWs(s).green + '',
		'-': s => visibleWs(s).red + '',
		'=': s => s,
	};
	const NL = (options && options.html) ? '<br/>\n' : '\n';
	const DIFFSEP = (options && options.separator) || NL;
	const visibleNL = '\u21b5';
	for (const change of Diff.diffWords(a, b)) {
		const op = change[0];
		const value = change[1].join('');
		if (op !== '=') {
			diffs++;
			buf += before;
			before = '';
			buf += value.split('\n').map((s,i,arr) => {
				if (i !== (arr.length - 1)) { s += visibleNL; }
				return s ? funcs[op](s) : s;
			}).join(NL);
		} else {
			if (context) {
				const lines = value.split('\n');
				if (lines.length > 2 * (context + 1)) {
					const first = lines.slice(0, context + 1).join(NL);
					const last = lines.slice(lines.length - context - 1).join(NL);
					if (diffs > 0) {
						buf += first + NL;
					}
					before = (diffs > 0 ? DIFFSEP : '') + last;
					continue;
				}
			}
			buf += value;
		}
	}
	if (options && options.diffCount) {
		return { count: diffs, output: buf };
	}
	return (diffs > 0) ? buf : '';
};

/**
 * This is essentially lifted from jsDiff@1.4.0, but using our diff and
 * without the header and no newline warning.
 * @private
 */
var createPatch = function(diff) {
	var ret = [];

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

	// Formats a given set of lines for printing as context lines in a patch
	function contextLines(lines) {
		return lines.map(function(entry) { return ' ' + entry; });
	}

	var oldRangeStart = 0;
	var newRangeStart = 0;
	var curRange = [];
	var oldLine = 1;
	var newLine = 1;

	for (var i = 0; i < diff.length; i++) {
		var current = diff[i];
		var lines = current.lines || current.value.replace(/\n$/, '').split('\n');
		current.lines = lines;

		if (current.added || current.removed) {
			// If we have previous context, start with that
			if (!oldRangeStart) {
				var prev = diff[i - 1];
				oldRangeStart = oldLine;
				newRangeStart = newLine;

				if (prev) {
					curRange = contextLines(prev.lines.slice(-4));
					oldRangeStart -= curRange.length;
					newRangeStart -= curRange.length;
				}
			}

			// Output our changes
			curRange.push.apply(curRange, lines.map(function(entry) {
				return (current.added ? '+' : '-') + entry;
			}));

			// Track the updated file position
			if (current.added) {
				newLine += lines.length;
			} else {
				oldLine += lines.length;
			}
		} else {
			// Identical context lines. Track line changes
			if (oldRangeStart) {
				// Close out any changes that have been output (or join overlapping)
				if (lines.length <= 8 && i < diff.length - 2) {
					// Overlapping
					curRange.push.apply(curRange, contextLines(lines));
				} else {
					// end the range and output
					var contextSize = Math.min(lines.length, 4);
					ret.push(
						'@@ -' + oldRangeStart + ',' + (oldLine - oldRangeStart + contextSize)
						+ ' +' + newRangeStart + ',' + (newLine - newRangeStart + contextSize)
						+ ' @@');
					ret.push.apply(ret, curRange);
					ret.push.apply(ret, contextLines(lines.slice(0, contextSize)));

					oldRangeStart = 0;
					newRangeStart = 0;
					curRange = [];
				}
			}
			oldLine += lines.length;
			newLine += lines.length;
		}
	}

	return ret.join('\n') + '\n';
};

/** @func */
Diff.patchDiff = function(a, b) {
	// Essentially lifted from jsDiff@1.4.0's PatchDiff.tokenize
	var patchTokenize = function(value) {
		var ret = [];
		var linesAndNewlines = value.split(/(\n|\r\n)/);
		// Ignore the final empty token that occurs if the string ends with a new line
		if (!linesAndNewlines[linesAndNewlines.length - 1]) {
			linesAndNewlines.pop();
		}
		// Merge the content and line separators into single tokens
		for (var i = 0; i < linesAndNewlines.length; i++) {
			var line = linesAndNewlines[i];
			if (i % 2) {
				ret[ret.length - 1] += line;
			} else {
				ret.push(line);
			}
		}
		return ret;
	};
	var diffs = 0;
	var diff = diffTokens(a, b, patchTokenize)
	.map(function(change) {
		var value = change[1].join('');
		switch (change[0]) {
			case '+':
				diffs++;
				return { value: value, added: true };
			case '-':
				diffs++;
				return { value: value, removed: true };
			default:
				return { value: value };
		}
	});
	if (!diffs) { return null; }
	return createPatch(diff);
};

if (typeof module === "object") {
	module.exports.Diff = Diff;
}