angular.module('batarang.json-tree', []).
  directive('batJsonTree', [batJsonTreeDirective]);

var BAT_JSON_TREE_TEMPLATE = '<div class="properties-tree"></div>';

var ENTER_KEY = 13,
    EXIT_KEY = 27;

var BAT_JSON_TREE_UNEDITABLE = [
  '$id',

  // managed by ngRepeat
  '$first',
  '$last',
  '$index',
  '$even',
  '$odd'
];

// http://stackoverflow.com/a/6234804/1216976
function escapeHtml(unsafeStr) {
  if (typeof unsafeStr !== 'string') {
    unsafeStr += '';
  }
  return unsafeStr
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#039;');
}
/*
 * TODO: remove dependency on inspectedApp service
 */
function batJsonTreeDirective() {
  return {
    restrict: 'E',
    terminal: true,
    scope: {
      batInspect: '&',
      batAssign: '&',
      batModel: '='
    },
    link: jsonTreeLinkFn
  };

  function jsonTreeLinkFn(scope, element, attrs) {
    var root = angular.element(BAT_JSON_TREE_TEMPLATE);
    element.append(root);

    var branches = {
      '': root
    };

    var expandedPaths = {};

    scope.$watch('batModel', function (val) {
      expandedPaths = {};
      if (!val) {
        root = angular.element(BAT_JSON_TREE_TEMPLATE);
        element.html('');
        element.append(root);
        branches = {
          '': root
        };

        return;
      }
      renderModel(val)
    });

    scope.$on('model:change', function(ev, data){
      if(data.id && scope.batModel && scope.batModel[''] && (data.id === scope.batModel[''].$id)){
        renderModel(scope.batModel);
      }
    });

    function renderModel(val){
      Object.
          keys(val).
          filter(function (key) {
            return key.substr(0, 2) !== '$$';
          }).
          sort(byPathDepth).
          forEach(function (key) {
            buildDom(val[key], key);
          });
    }

    function buildDom(object, depth) {
      branches[depth].html('');

      if (!typeof object === 'undefined') {
        return;
      }

      var buildBranch = function (key) {
        var val = object[key];
        var fullPath = depth;
        if (depth) {
          if (Number.isNaN(parseInt(key, 10))) {
            fullPath += '.' + key;
          } else {
            fullPath += '[' + key + ']';
          }
        } else {
          fullPath += key;
        }

        var parentElt = angular.element('<li title>' +
            '<span class="name">' + escapeHtml(key) + '</span>' +
            '<span class="separator">: </span>' +
          '</li>'),
          childElt;

        if (expandedPaths.hasOwnProperty(fullPath)) {
          parentElt.addClass('expanded');
        }

        if (val === null) {
          childElt = angular.element('<span class="value console-formatted-null">null</span>');
        } else if (val['~object'] || val['~array-length'] !== undefined) {
          parentElt.addClass('parent');

          // you can't expand an empty array
          if (val['~array-length'] !== 0) {
            parentElt.on('click', function () {
              expandedPaths[fullPath] = true;
              scope.batInspect({ path: fullPath });
            });
          }

          if (val['~object']) {
            childElt = angular.element('<span class="console-formatted-object">Object</span>');
          } else {
            childElt = angular.element(
              '<span class="console-formatted-object">Array[' +
                val['~array-length'] +
              ']</span>');
          }
        } else {
          // TODO: what doe sregex look like?
          if (typeof val === 'string') {
            val = '"' + val + '"';
          }

          // TODO: test this
          // some properties (like $id) shouldn't be edited
          if (BAT_JSON_TREE_UNEDITABLE.indexOf(fullPath) > -1) {
            childElt = angular.element(
              '<span class="console-formatted-' + (typeof val) + '">' +
                escapeHtml(val) +
              '</span>');
          } else {
            childElt = angular.element(
              '<span contentEditable="true" class="console-formatted-' + (typeof val) + '">' +
                escapeHtml(val) +
              '</span>');

            // TODO: test this
            childElt.on('keydown', function (ev) {
              if (ev.keyCode === ENTER_KEY || ev.keyCode === EXIT_KEY) {
                ev.preventDefault();
                childElt[0].blur();
                doAssign();
              }
            });

            // TODO: test this
            childElt.on('blur', doAssign);

            function doAssign() {
              scope.batAssign({
                path: fullPath,
                value: childElt.text()
              });
            }
          }
        }

        parentElt.append(childElt);
        branches[fullPath] = childElt;

        return parentElt;
      };

      var properties;
      if (object instanceof Array) {
        properties = object.map(function (item, i) {
          return i;
        });
      } else if (object != null) {
        properties = Object.keys(object);
      } else {
        properties = [];
      }

      properties.
        map(buildBranch).
        forEach(function (elt) {
          branches[depth].append(elt);
        });

    };
  }
}

function byPathDepth(a, b) { // sort '' first
  if (a === '') {
    return -1;
  } else if (b === '') {
    return 1;
  } else { // sort by tree depth, and ensure array objects come before their members
    return a.split(/[\.,\[]/).length - b.split(/[\.,\[]/).length;
  }
}
