// mrdiff.js

/*

Copyright (c) 2005-2009 by Matthias Hertel, http://www.mathertel.de/
Copyright (c) 2011 by Mason Giles

All rights reserved.

Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:

1. Redistributions of source code must retain the above copyright notice, this
   list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
   this list of conditions and the following disclaimer in the documentation
   and/or other materials provided with the distribution.
3. Neither the name of the copyright owners nor the names of its contributors
   may be used to endorse or promote products derived from this software without
   specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

*/

"use strict";

var mrdiff = {

	///////////////////////////////////////////////////////////////////////////

	'MRDIFF_VERSION':   "1.2",
	
	'MRDIFF_INVALID':   0,
	'MRDIFF_REFERENCE': 1,
	'MRDIFF_LITERAL':   2,

	///////////////////////////////////////////////////////////////////////////

	'warn': function(message) {
		// This function, and all calls to it, should be removed from any final
		//   minified builds.
		console.log(message);
	},

	///////////////////////////////////////////////////////////////////////////
	
	'dup': function(d) {
		//return d.slice(0);
		var ret = [];
		for (var i = 0, e = d.length; i != e; i++) {
			ret.push(d[i].slice(0));
		}
		return ret;
	},
	
	///////////////////////////////////////////////////////////////////////////

	'diff': function(sl, sr) {
		var ret = [];
		var common_suffix = [this.MRDIFF_REFERENCE, "", 0];
		
		// Trivial case
		if (sl.length == sr.length && sl == sr) {
			var trivial = [this.MRDIFF_REFERENCE, "", sl.length];
			ret.push( trivial );
			return ret;
		}

		// Extract common prefix
		var p = 0, m = (sl.length < sr.length) ? sl.length : sr.length;
		while(p < m && sl[p] == sr[p]) p++;

		if (p) { // Found a common prefix
			var common_prefix = [this.MRDIFF_REFERENCE, "", p];
			ret.push(common_prefix);
			sr = sr.substr(p); sl = sl.substr(p);
		}

		// Extract common suffix
		var sl_m = sl.length, sr_m = sr.length;
		p = 0; m = (sl_m < sr_m) ? sl_m : sr_m;
		while (p < m && sl[sl_m - p - 1] == sr[sr_m - p - 1]) p++;

		if (p) { // Found a common suffix
			common_suffix[2] = p; // = [this.MRDIFF_REFERENCE, "", p];
			sr = sr.substr(0, sr_m-p); sl = sl.substr(0, sl_m-p);
		}

		// Apply real diff algorithm to interior, and optimise away shared types
		var real_diff = this._optimise(this._diff_alg(sl, sr));

		// Tidy up
		ret.push.apply(ret, real_diff);
		if (common_suffix[2]) ret.push(common_suffix);

		return ret;
	},
	
	///////////////////////////////////////////////////////////////////////////
	
	'patch': function(orig, d) {
		var p = 0, orig_len = orig.length
		var ret = "";

		for (var i = 0, e = d.length; i < e; i++) {
			// Error checking:
			if (p > orig_len) return ret;

			switch (d[i][0]) {

				case this.MRDIFF_LITERAL: {
					ret += d[i][1];
					p += d[i][2];
				} break;

				case this.MRDIFF_REFERENCE: {
					ret += orig.substr(p, d[i][2]);
					p += d[i][2];
				} break;

				default: { // Invalid... ignore it
					this.warn("WARNING: Unknown object type");
				} break;
			}
		}

		// Tidy up

		return ret;
	},
	
	///////////////////////////////////////////////////////////////////////////
	
	'merge': function(sl, sr) {
		var ret = [];
				
		var ll = this.dup(sl), lr = this.dup(sr);
		// We're always working with the first element of ll/lr.

		for(;;) {

			// Find longest common reference area from current offsets
			// If this length is greater than zero:
			//		Copy shortest common reference area	
			// Otherwise, if the length is zero:
			//		Copy literal, and advance src_len places in the other string


			// Find longest common reference area from current offsets
			if (ll.length && lr.length) {
				if (
					ll[0][0] == this.MRDIFF_REFERENCE &&
					lr[0][0] == this.MRDIFF_REFERENCE
				) {
					
					// Copy the shortest common reference area.
					var l_len = ll[0][2], r_len = lr[0][2];
					var common_len = (l_len > r_len) ? r_len : l_len;

					var common_reference_area = [this.MRDIFF_REFERENCE, "", common_len];
					ret.push( common_reference_area );

					// Apply offsets
					ll[0][2] -= common_len;
					lr[0][2] -= common_len;

				} else { // At least one of them is a literal.
					
					var collide = function(llit, lref) {
						// Copy either literal, and advance the other string, swapping
						//  to the next diff_t if necessary

						ret.push( llit[0] );
					
						// We need to clear llit:src_len characters from lref, regardless of 
						// whatever type they are.
						// Iterate, and just ignore any literals by converting them to references.

						var clear_len = llit[0][2];
						llit.shift();

						while (clear_len) {
							var obj_len = lref[0][2];

							if (clear_len >= obj_len) {
								// >: Can't clear enough characters from this object, need more
								// =: Easy case, this is just what we need
								clear_len -= obj_len;
								lref.shift();
								// =: clear_len is now 0 and we're done

							} else { // (clear_len < obj_len)
								// We... only need a part of this object
								// Truncate it to a reference and shrink it by clear_len-obj_len
								lref[0][0]  = this.MRDIFF_REFERENCE; // ignore l/l collisions
								lref[0][2] -= clear_len;
								clear_len = 0;
							}

						};
					};
					
					// Determine which literal to keep.
					//list<diff_t> *llit = &lr, *lref = &ll;
					// Javascript: Arrays are objects, so this all happens by reference
					if (ll[0][0] == this.MRDIFF_LITERAL) {
						collide.call(this, ll, lr);
					} else {
						collide.call(this, lr, ll);
					}
					
					
				}

				// Remove the first entry if they've been nulled

				if (ll.length && ll[0][0] == this.MRDIFF_REFERENCE && ll[0][2] == 0) {
					ll.shift();
				}
				if (lr.length && lr[0][0] == this.MRDIFF_REFERENCE && lr[0][2] == 0) {
					lr.shift();
				}
			
			}

			// Check if either list has been cleared
			// If so, keep everything remaining in the other list if there are any insertions.
			// If there were only references, then one of the diffs specifies deleting this area.
			
			// FIXME: deduplicate this somehow
			// (easy to deduplicate in JS, but it's also important to keep the
			//   C++ and JS in line as much as possible. + it doesn't really hurt)
			
			if (! ll.length) {
				if (lr.length) {
					// Check lr for insertions
					var found_insertion = false;
					for (var i = 0, e = lr.length; i != e; i++) {
						if (lr[i][0] == this.MRDIFF_LITERAL) {
							found_insertion = true;
							break;
						}
					}
					if (found_insertion) {
						//ret.insert(ret.end(), lr.begin(), lr.end());
						ret.push.apply(ret, lr);
					}
				}
				break; // Finished
			}

			if (! lr.length) {
				if (ll.length) {
					// Check lr for insertions
					var found_insertion = false;
					for (var i = 0, e = ll.length; i != e; i++) {
						if (ll[i][0] == this.MRDIFF_LITERAL) {
							found_insertion = true;
							break;
						}
					}
					if (found_insertion) {
						ret.push.apply(ret, ll);
						//ret.insert(ret.end(), ll.begin(), ll.end());
					}
				}
				break; // Finished
			}

		}	
			
		return ret;
	},
	
	///////////////////////////////////////////////////////////////////////////
	
	'serialise': function(diff) {
		var ret = "";
		for (var i = 0, e = diff.length; i != e; i++) {
			if (diff[i][0] == this.MRDIFF_LITERAL) {

				ret += "a:" + diff[i][2] + ":" + diff[i][1].length + ":";
				ret += diff[i][1];
				ret += ":";
				
			} else if (diff[i][0] == this.MRDIFF_REFERENCE) {

				ret += "b:" + diff[i][2] + ":";
				
			} else {
				this.warn("WARNING: Unknown type " + diff[i][0]);
			}
		}
		return ret;
	},
	
	///////////////////////////////////////////////////////////////////////////
	
	'pos': function(p) {
		return 0;
	},
	
	///////////////////////////////////////////////////////////////////////////
	
	'unserialise': function(d) {
		//return [];
		
		var getSerialisedToken = function(str) {
			var pos = str.indexOf(':', p);
			if (pos != -1) {
				var token = str.substr(p, pos - p);
				p = pos;
				return parseInt(token);
			} else {
				// Invalid stream!
				return -1;
			}
		}
				
		var ret = [];//vector<diff_t> ret;
		var p = 0; var e = d.length;

		while (p < e) {
			if (d[p] == 'a' && d[p+1] == ':') {
				p += 2;
				if (p>=e) { this.warn('mrunserialise'); return [] };

				// Extract length tokens
				var src_len = getSerialisedToken(d, p); p++;
				if (p>=e) { this.warn('mrunserialise'); return [] };
				var lit_len = getSerialisedToken(d, p); p++;
				if (p>=e) { this.warn('mrunserialise'); return [] };

				// Extract string literal
				var literal = d.substr(p, lit_len);
				p += lit_len;
				if (p>=e) { this.warn('mrunserialise'); return [] };

				if (d[p] == ':') {
					p++;
					var lit_entry = [this.MRDIFF_LITERAL, literal, src_len];
					ret.push(lit_entry);
				} else {
					// Something's gone wrong: Invalid ending token
					this.warn("mrunserialise: Invalid ending token");
					//ret.clear();
					return [];
				}

			} else if (d[p] == 'b' && d[p+1] == ':') {
				p += 2;
				if (p>=e) { this.warn('mrunserialise'); return [] };

				// Extract length token
				var src_len = getSerialisedToken(d, p);
				if (p>=e) { this.warn('mrunserialise'); return [] };

				if (d[p] == ':') {
					p++;
					var ref_entry = [this.MRDIFF_REFERENCE, "", src_len];
					ret.push(ref_entry);
				} else {
					// Something's gone wrong: Invalid ending token
					this.warn("mrunserialise: Invalid ending token");
					//ret.clear();
					return [];
				}

			} else {
				// Something's gone wrong: Invalid diff format
				this.warn("mrunserialise: Invalid diff format");
				//ret.clear();
				return [];
			}		
		}

		return ret;
		

	},
	
	///////////////////////////////////////////////////////////////////////////
	
	'_optimise': function(d) {
		var ret = [];
		var last_type = this.MRDIFF_INVALID;
		
		for (var i = 0, e = d.length; i < e; i++) {
			if (d[i][0] == this.MRDIFF_LITERAL && ! d[i][2] && ! d[i][1].length) {
				continue;
			}

			if (d[i].type == last_type) { // Coalesce
				switch( last_type ) {
					case this.MRDIFF_LITERAL: {
						ret[ret.length-1][1] += d[i][1]; // literal
						ret[ret.length-1][2] += d[i][2]; // src_len
					} break;
					case this.MRDIFF_REFERENCE: {
						ret[ret.length-1][2] += d[i][2];
					} break;
					default: { // Unknown or invalid type... push it through as-is
						// Always happens once, as long as there are at least
						//  two elements, for the first comparison
						//this.warn("Unknown or invalid type to optimise");
						ret.push(d[i]);
					} break;
				}
			} else { // Add 
				ret.push(d[i]);
			}
			last_type = d[i].type;
		}
		return ret;
	},
	
	///////////////////////////////////////////////////////////////////////////
		
	'_sms': function(DataA, LowerA, UpperA, DataB, LowerB, UpperB, DownVector, UpVector) {
		var ret = {'x': 0, 'y': 0};
		
		var MAX = DataA.length() + DataB.length() + 1;

		var DownK = LowerA - LowerB; // the k-line to start the forward search
		var UpK   = UpperA - UpperB; // the k-line to start the reverse search

		var Delta = (UpperA - LowerA) - (UpperB - LowerB);
		var oddDelta = (Delta % 2) == 1;//(Delta & 1) != 0;

		// The vectors in the publication accepts negative indexes. the vectors implemented
		//  here are 0-based and are access using a specific offset: UpOffset UpVector
		//  and DownOffset for DownVektor
		var DownOffset = MAX - DownK, UpOffset = MAX - UpK;

		var MaxD = ((UpperA - LowerA + UpperB - LowerB) / 2) + 1;

		// init vectors
		DownVector[DownOffset + DownK + 1] = LowerA;
		UpVector[UpOffset + UpK - 1] = UpperA;

		for (var D = 0; D <= MaxD; D++) {

			// Extend the forward path.
			for (var k = DownK - D; k <= DownK + D; k += 2) {

				// find the only or better starting point
				var x, y;
				if (k == DownK - D) {
					x = DownVector[DownOffset + k + 1]; // down
				} else {
					x = DownVector[DownOffset + k - 1] + 1; // a step to the right
					if ((k < DownK + D) && (DownVector[DownOffset + k + 1] >= x))
						x = DownVector[DownOffset + k + 1]; // down
				}
				y = x - k;

				// find the end of the furthest reaching forward D-path in diagonal k.
				while ((x < UpperA) && (y < UpperB) && (DataA.data[x] == DataB.data[y])) {
					x++; y++;
				}
				DownVector[DownOffset + k] = x;

				// overlap ?
				if (oddDelta && (UpK - D < k) && (k < UpK + D)) {
					if (UpVector[UpOffset + k] <= DownVector[DownOffset + k]) {
						ret.x = DownVector[DownOffset + k];
						ret.y = DownVector[DownOffset + k] - k;
						return (ret);
					}
				}

			}

			// Extend the reverse path.
			for (var k = UpK - D; k <= UpK + D; k += 2) {
				
				// find the only or better starting point
				var x, y;
				if (k == UpK + D) {
					x = UpVector[UpOffset + k - 1]; // up
				} else {
					x = UpVector[UpOffset + k + 1] - 1; // left
					if ((k > UpK - D) && (UpVector[UpOffset + k - 1] < x))
					x = UpVector[UpOffset + k - 1]; // up
				} // if
				y = x - k;

				while (
					(x > LowerA) &&
					(y > LowerB) &&
					(DataA.data[x - 1] == DataB.data[y - 1])
				) {
					x--; y--; // diagonal
				}
				UpVector[UpOffset + k] = x;

				// overlap ?
				if (!oddDelta && (DownK - D <= k) && (k <= DownK + D)) {
					if (UpVector[UpOffset + k] <= DownVector[DownOffset + k]) {
						ret.x = DownVector[DownOffset + k];
						ret.y = DownVector[DownOffset + k] - k;
						return (ret);
					}
				}

			}
		}

		// The algorithm should never come here.
		this.warn("WARNING: SMS Algorithm in invalid state");
		return ret;
	},
	
	///////////////////////////////////////////////////////////////////////////
	
	'_lcs': function(DataA, LowerA, UpperA, DataB, LowerB, UpperB, DownVector, UpVector) {
	
		// Fast walkthrough equal lines at the start
		while (
			LowerA < UpperA &&
			LowerB < UpperB && DataA.data[LowerA] == DataB.data[LowerB]
		) {
			LowerA++; LowerB++;
		}

		// Fast walkthrough equal lines at the end
		while (
			LowerA < UpperA &&
			LowerB < UpperB && DataA.data[UpperA - 1] == DataB.data[UpperB - 1]
		) {
			--UpperA; --UpperB;
		}

		if (LowerA == UpperA) {
			
			// mark as inserted lines.
			while (LowerB < UpperB) DataB.modified[LowerB++] = true;

		} else if (LowerB == UpperB) {

			// mark as deleted lines.
			while (LowerA < UpperA) DataA.modified[LowerA++] = true;

		} else {

			// Find the middle snake and length of an optimal path for A and B
			var smsrd = this._sms(
				DataA, LowerA, UpperA, DataB, LowerB, UpperB, DownVector, UpVector
			);
			
			// The path is from LowerX to (x,y) and (x,y) to UpperX
			this._lcs(DataA, LowerA, smsrd.x, DataB, LowerB, smsrd.y, DownVector, UpVector);
			this._lcs(DataA, smsrd.x, UpperA, DataB, smsrd.y, UpperB, DownVector, UpVector);
		}
	},
	
	///////////////////////////////////////////////////////////////////////////
	
	'_creatediffs': function(DataA, DataB) {

		var a = [];
		var aItem;

		var StartA = 0, StartB = 0, LineA = 0, LineB = 0, pos = 0;

		while (LineA < DataA.length() || LineB < DataB.length()) {
			if (
				(LineA < DataA.length()) && (!DataA.modified[LineA])
				&& (LineB < DataB.length()) && (!DataB.modified[LineB])
			) {
				// equal lines
				LineA++;
				LineB++;

			} else {
				// maybe deleted and/or inserted lines
				StartA = LineA;
				StartB = LineB;

				while (
					LineA < DataA.length() &&
					(LineB >= DataB.length() || DataA.modified[LineA])
				) LineA++;

				while (
					LineB < DataB.length() &&
					(LineA >= DataA.length() || DataB.modified[LineB])
				) LineB++;

				if ((StartA < LineA) || (StartB < LineB)) {
					// store a new difference-item
									
					if (pos < StartA) {
						// Insert a reference to get us up to date.
						var rItem = [this.MRDIFF_REFERENCE, "", StartA - pos];
						a.push(rItem);

						pos += rItem[2]; // = StartA
					}

					var aItem = [
						this.MRDIFF_LITERAL,
						DataB.data.substr(StartB, LineB-StartB),
						LineA-StartA
					];
					pos += aItem[2];

					a.push(aItem);
				}
			}
		}

		if (pos < DataA.length()) { // Insert remaining data
			var rItem = [this.MRDIFF_REFERENCE, "", DataA.length() - pos];
			a.push(rItem);
		}

		return a;
	},
	
	///////////////////////////////////////////////////////////////////////////
	
	'_diff_alg': function(sl, sr) {
		
		var DataA = {
			'data': sl,
			'modified': [],
			'length': function() { return this.data.length; }
		};
		var DataB = {
			'data': sr,
			'modified': [],
			'length': function() { return this.data.length; }
		};
		//diffable DataA(sl), DataB(sr);

		var MAX = DataA.length() + DataB.length() + 1;
		
		// vector for the (0,0) to (x,y) search
		var DownVector = [];// DownVector.resize(2 * MAX + 2);
		// vector for the (u,v) to (N,M) search
		var UpVector = []; //vector<int> UpVector; UpVector.resize(2 * MAX + 2);

		this._lcs(DataA, 0, DataA.length(), DataB, 0, DataB.length(), DownVector, UpVector);
		return this._creatediffs(DataA, DataB);
	}
};
