/*
 * json-diff.c
 *
 * Copyright (C) 2014 Andreas Gruenbacher <andreas.gruenbacher@gmail.com>
 *
 * Licensed under the GNU AFFERO GENERAL PUBLIC LICENSE Version 3,
 * http://www.fsf.org/licensing/licenses/agpl-3.0.html, or any later version.
 */

/*
 * Function json_diff(a, b)
 *
 * Create an RFC 6902 style patch between two JSON objects.
 * For example, json_diff({a: [9, 7]}, {a: [9, 8, 7]}) results in:
 * [{op: 'add', path: '/a/1', value: 8}].
 *
 * No cyclic references are allowed in a or b. Because the algorithm computes
 * differences between arrays in a way similar to the diff utility, it will
 * become slow for large arrays of complex objects.
 */
var json_diff = (function() {
  function equal(a, b) {
    if (a === b)
      return true;
    if (typeof a !== 'object' || typeof b !== 'object')
      return false;
    if (a === null || b === null)
      return false;
    if (a instanceof Array !== b instanceof Array)
      return false;
    if (a instanceof Array) {
      if (a.length != b.length)
	return false;
      for (var n = 0; n < a.length; n++)
	if (!equal(a[n], b[n]))
	  return false;
    } else {
      for (var key in a) {
	if (!(key in b) || !equal(a[key], b[key]))
	  return false;
      }
      for (var key in b) {
	if (!(key in a))
	  return false;
      }
    }
    return true;
  }

  function type(v) {
    if (typeof v === 'object') {
      if (v instanceof Array)
	return 'array';
      if (v === null)
	return 'null';
    }
    return typeof v  // 'undefined', 'string', 'number', 'boolean'
  }

  // The actual json_diff function
  return function(a, b) {
    var path = [], operations = [];

    function json_pointer(tail) {
      if (tail === undefined)
	return '/';
      else {
	path.push(tail);
	var pointer = '';
	for (var n = 0; n < path.length; n++)
	  pointer = pointer + '/' + path[n].toString().replace(/~/g, '~0').replace(/\//g, '~1');
	path.pop();
	return pointer;
      }
    }

    function result(op, tail, value) {
      var pointer = json_pointer(tail);
      if (op === 'remove')
	operations.push({op: op, path: pointer});
      else
	operations.push({op: op, path: pointer,
			 value: value === undefined ? null : value});
    }

    // Based on the Dynamic Programming Longest Common Subsequence algorithm at
    // http://rosettacode.org/wiki/Longest_common_subsequence#JavaScript
    function diffseq(a, b) {
      var i, j, m, n,
	  row = [], c = [],
	  left, diag, latch;
      var skip, edit = [];

      m = a.length;
      n = b.length;

      // ignore equal elements at both ends
      for (; m && n; m--, n--)
	if (!equal(a[m - 1], b[n - 1]))
	  break;
      for (skip = 0; m && n; skip++, m--, n--)
	if (!equal(a[skip], b[skip]))
	  break;

      if (m && n) {
	//build the c-table
	for (j = 0; j < n; row[j++] = 0);
	for (i = 0 ; i < m; i++) {
	  c[i] = row = row.slice();
	  for (diag = 0, j = 0; j < n; j++, diag = latch) {
	    latch = row[j];
	    if (equal(a[i + skip], b[j + skip]))
	      row[j] = diag + 1;
	    else {
	      left = row[j - 1] || 0;
	      if (left > row[j])
		row[j] = left;
	    }
	  }
	}
	i--, j--;

	//row[j] now contains the length of the lcs
	//compute the edit sequence from the table
	while (i > -1 && j > -1) {
	  switch (c[i][j]) {
	    default:
	      edit.unshift('=');
	      i--; j--;
	      break;
	    case j && c[i][j - 1]:
	      edit.unshift('+');
	      j--;
	      break;
	    case i && c[i - 1][j]:
	      edit.unshift('-');
	      i--;
	      break;
	  }
	}
      } else {
	i = m - 1;
	j = n - 1;
      }
      for (; j > -1; j--)
	edit.unshift('+');
      for (; i > -1; i--)
	edit.unshift('-');

      var edit_replace = [];
      for (n = 0; n < edit.length; n++) {
	if (edit[n] === '-') {
	  for (i = n + 1; i < edit.length && edit[i] === '-'; i++);
	  for (j = i; j < edit.length && edit[j] === '+'; j++);
	  if (i - n == j - i) {
	    while (i++ < j)
	      edit_replace.push('*');
	    n = j - 1;
	  } else {
	    for (; n < j; n++)
	      edit_replace.push(edit[n]);
	    n--;
	  }
	} else
	  edit_replace.push(edit[n]);
      }
      // console.log('>>> ' + skip + ' ' + edit.join('') + ' ' + edit_replace.join(''));
      edit = edit_replace;

      for (i = 0, j = 0, n = 0; n < edit.length; n++) {
	switch(edit[n]) {
	  case '*':
	    diff_recursive(a[i + skip], b[j + skip], j + skip);
	  case '=':
	    i++;
	    j++;
	    break;
	  case '-':
	    result('remove', j + skip);
	    i++;
	    break;
	  case '+':
	    result('add', j + skip, b[j + skip]);
	    j++;
	    break;
	}
      }
    }

    function diff_arrays(a, b, tail) {
      if (tail !== undefined)
	path.push(tail);
      diffseq(a, b);
      if (tail !== undefined)
	path.pop();
    }

    function diff_objects(a, b, tail) {
      if (tail !== undefined)
	path.push(tail);
      for (var key in a) {
	if (key in b)
	  diff_recursive(a[key], b[key], key);
	else
	  result('remove', key);
      }
      for (var key in b) {
	if (!(key in a))
	  result('add', key, b[key]);
      }
      if (tail !== undefined)
	path.pop();
    }

    function diff_recursive(a, b, tail) {
      if (a === b)
	return;
      var ta = type(a), tb = type(b);
      if (ta !== tb)
	result('replace', tail, b);
      else {
	switch(ta) {
	  case 'object':
	    diff_objects(a, b, tail);
	    break;
	  case 'array':
	    diff_arrays(a, b, tail);
	    break;
	  default:
	    result('replace', tail, b);
	}
      }
    }

    diff_recursive(a, b, undefined);
    return operations;
  };
})();

if (typeof exports !== 'undefined')
  exports.diff = json_diff;