text: /**
   * @provides javelin-behavior-differential-edit-inline-comments
   * @requires javelin-behavior
   *           javelin-stratcom
   *           javelin-dom
   *           javelin-util
   *           javelin-vector
   *           differential-inline-comment-editor
   */
  
  JX.behavior('differential-edit-inline-comments', function(config) {
  
    var selecting = false;
    var reticle = JX.$N('div', {className: 'differential-reticle'});
    var old_cells = [];
    JX.DOM.hide(reticle);
  
    var origin = null;
    var target = null;
    var root   = null;
    var changeset = null;
  
    var editor = null;
  
    function updateReticleForComment(e) {
      root = e.getNode('differential-changeset');
      if (!root) {
        return;
      }
  
      var data = e.getNodeData('differential-inline-comment');
      var change = e.getNodeData('differential-changeset');
  
      var id_part = data.on_right ? change.right : change.left;
      var new_part = data.isNewFile ? 'N' : 'O';
      var prefix = 'C' + id_part + new_part + 'L';
  
      origin = JX.$(prefix + data.number);
      target = JX.$(prefix + (parseInt(data.number, 10) +
                              parseInt(data.length, 10)));
  
      updateReticle();
    }
  
    function updateReticle() {
      JX.DOM.getContentFrame().appendChild(reticle);
  
      var top = origin;
      var bot = target;
      if (JX.$V(top).y > JX.$V(bot).y) {
        var tmp = top;
        top = bot;
        bot = tmp;
      }
  
      // Find the leftmost cell that we're going to highlight: this is the next
      // <td /> in the row. In 2up views, it should be directly adjacent. In
      // 1up views, we may have to skip over the other line number column.
      var l = top;
      while (JX.DOM.isType(l, 'th')) {
        l = l.nextSibling;
      }
  
      // Find the rightmost cell that we're going to highlight: this is the
      // farthest consecutive, adjacent <td /> in the row. Sometimes the left
      // and right nodes are the same (left side of 2up view); sometimes we're
      // going to highlight several nodes (copy + code + coverage).
      var r = l;
      while (r.nextSibling && JX.DOM.isType(r.nextSibling, 'td')) {
        r = r.nextSibling;
      }
  
      var pos = JX.$V(l)
        .add(JX.Vector.getAggregateScrollForNode(l));
  
      var dim = JX.$V(r)
        .add(JX.Vector.getAggregateScrollForNode(r))
        .add(-pos.x, -pos.y)
        .add(JX.Vector.getDim(r));
  
      var bpos = JX.$V(bot)
        .add(JX.Vector.getAggregateScrollForNode(bot));
      dim.y = (bpos.y - pos.y) + JX.Vector.getDim(bot).y;
  
      pos.setPos(reticle);
      dim.setDim(reticle);
  
      JX.DOM.show(reticle);
  
      // Find all the cells in the same row position between the top and bottom
      // cell, so we can highlight them.
      var seq = 0;
      var row = top.parentNode;
      for (seq = 0; seq < row.childNodes.length; seq++) {
        if (row.childNodes[seq] == top) {
          break;
        }
      }
  
      var cells = [];
      while (true) {
        cells.push(row.childNodes[seq]);
        if (row.childNodes[seq] == bot) {
          break;
        }
        row = row.nextSibling;
      }
  
      setSelectedCells(cells);
    }
  
    function setSelectedCells(new_cells) {
      updateSelectedCellsClass(old_cells, false);
      updateSelectedCellsClass(new_cells, true);
      old_cells = new_cells;
    }
  
    function updateSelectedCellsClass(cells, selected) {
      for (var ii = 0; ii < cells.length; ii++) {
        JX.DOM.alterClass(cells[ii], 'selected', selected);
      }
    }
  
    function hideReticle() {
      JX.DOM.hide(reticle);
      setSelectedCells([]);
    }
  
    JX.DifferentialInlineCommentEditor.listen('done', function() {
      selecting = false;
      editor = false;
      hideReticle();
      set_link_state(false);
    });
  
    function isOnRight(node) {
      return node.parentNode.firstChild != node;
    }
  
    function isNewFile(node) {
      var data = JX.Stratcom.getData(root);
      return isOnRight(node) || (data.left != data.right);
    }
  
    function getRowNumber(th_node) {
      try {
        return parseInt(th_node.id.match(/^C\d+[ON]L(\d+)$/)[1], 10);
      } catch (x) {
        return undefined;
      }
    }
  
    var set_link_state = function(active) {
      JX.DOM.alterClass(JX.$(config.stage), 'inline-editor-active', active);
    };
  
    JX.Stratcom.listen(
      'mousedown',
      ['differential-changeset', 'tag:th'],
      function(e) {
        if (e.isRightButton() ||
            getRowNumber(e.getTarget()) === undefined) {
          return;
        }
  
        if (editor) {
          new JX.DifferentialInlineCommentEditor(config.uri)
            .setOperation('busy')
            .setRow(editor.getRow().previousSibling)
            .start();
          return;
        }
  
        if (selecting) {
          return;
        }
  
        selecting = true;
        root = e.getNode('differential-changeset');
  
        origin = target = e.getTarget();
  
        var data = e.getNodeData('differential-changeset');
        if (isOnRight(target)) {
          changeset = data.right;
        } else {
          changeset = data.left;
        }
  
        updateReticle();
  
        e.kill();
      });
  
    JX.Stratcom.listen(
      ['mouseover', 'mouseout'],
      ['differential-changeset', 'tag:th'],
      function(e) {
        if (e.getIsTouchEvent()) {
          return;
        }
  
        if (editor) {
          // Don't update the reticle if we're editing a comment, since this
          // would be distracting and we want to keep the lines corresponding
          // to the comment highlighted during the edit.
          return;
        }
  
        if (getRowNumber(e.getTarget()) === undefined) {
          // Don't update the reticle if this "<th />" doesn't correspond to a
          // line number. For instance, this may be a dead line number, like the
          // empty line numbers on the left hand side of a newly added file.
          return;
        }
  
        if (selecting) {
          if (isOnRight(e.getTarget()) != isOnRight(origin)) {
            // Don't update the reticle if we're selecting a line range and the
            // "<th />" under the cursor is on the wrong side of the file. You
            // can only leave inline comments on the left or right side of a
            // file, not across lines on both sides.
            return;
          }
  
          if (e.getNode('differential-changeset') !== root) {
            // Don't update the reticle if we're selecting a line range and
            // the "<th />" under the cursor corresponds to a different file.
            // You can only leave inline comments on lines in a single file,
            // not across multiple files.
            return;
          }
        }
  
        if (e.getType() == 'mouseout') {
          if (selecting) {
            // Don't hide the reticle if we're selecting, since we want to
            // keep showing the line range that will be used if the mouse is
            // released.
            return;
          }
          hideReticle();
        } else {
          target = e.getTarget();
          if (!selecting) {
            // If we're just hovering the mouse and not selecting a line range,
            // set the origin to the current row so we highlight it.
            origin = target;
          }
  
          updateReticle();
        }
      });
  
    JX.Stratcom.listen(
      'mouseup',
      null,
      function(e) {
        if (editor || !selecting) {
          return;
        }
  
        var o = getRowNumber(origin);
        var t = getRowNumber(target);
  
        var insert;
        var len;
        if (t < o) {
          len = (o - t);
          o = t;
          insert = origin.parentNode;
        } else {
          len = (t - o);
          insert = target.parentNode;
        }
  
        var view = JX.ChangesetViewManager.getForNode(root);
  
        editor = new JX.DifferentialInlineCommentEditor(config.uri)
          .setTemplates(view.getUndoTemplates())
          .setOperation('new')
          .setChangesetID(changeset)
          .setLineNumber(o)
          .setLength(len)
          .setIsNew(isNewFile(target) ? 1 : 0)
          .setOnRight(isOnRight(target) ? 1 : 0)
          .setRow(insert.nextSibling)
          .setTable(insert.parentNode)
          .setRenderer(view.getRenderer())
          .start();
  
        set_link_state(true);
  
        e.kill();
      });
  
    JX.Stratcom.listen(
      ['mouseover', 'mouseout'],
      'differential-inline-comment',
      function(e) {
        if (e.getIsTouchEvent()) {
          return;
        }
  
        if (e.getType() == 'mouseout') {
          hideReticle();
        } else {
          updateReticleForComment(e);
        }
      });
  
    var action_handler = function(op, e) {
      e.kill();
  
      if (editor) {
        return;
      }
  
      var node = e.getNode('differential-inline-comment');
  
      // If we're on a touch device, we didn't highlight the affected lines
      // earlier because we can't use hover events to mutate the document.
      // Highlight them now.
      updateReticleForComment(e);
  
      handle_inline_action(node, op);
    };
  
    var handle_inline_action = function(node, op) {
      var data = JX.Stratcom.getData(node);
  
      // If you click an action in the preview at the bottom of the page, we
      // find the corresponding node and simulate clicking that, if it's
      // present on the page. This gives the editor a more consistent view
      // of the document.
      if (JX.Stratcom.hasSigil(node, 'differential-inline-comment-preview')) {
        var nodes = JX.DOM.scry(
          JX.DOM.getContentFrame(),
          'div',
          'differential-inline-comment');
  
        var found = false;
        var node_data;
        for (var ii = 0; ii < nodes.length; ++ii) {
          if (nodes[ii] == node) {
            // Don't match the preview itself.
            continue;
          }
          node_data = JX.Stratcom.getData(nodes[ii]);
          if (node_data.id == data.id) {
            node = nodes[ii];
            data = node_data;
            found = true;
            break;
          }
        }
  
        if (!found) {
          switch (op) {
            case 'delete':
              new JX.DifferentialInlineCommentEditor(config.uri)
                .deleteByID(data.id);
              return;
          }
        }
  
        if (op == 'delete') {
          op = 'refdelete';
        }
      }
  
      if (op == 'done') {
        var checkbox = JX.DOM.find(node, 'input', 'differential-inline-done');
        new JX.DifferentialInlineCommentEditor(config.uri)
          .toggleCheckbox(data.id, checkbox);
        return;
      }
  
      var original = data.original;
      var reply_phid = null;
      if (op == 'reply') {
        // If the user hit "reply", the original text is empty (a new reply), not
        // the text of the comment they're replying to.
        original = '';
        reply_phid = data.phid;
      }
  
      var row = JX.DOM.findAbove(node, 'tr');
      var changeset_root = JX.DOM.findAbove(
        node,
        'div',
        'differential-changeset');
      var view = JX.ChangesetViewManager.getForNode(changeset_root);
  
      editor = new JX.DifferentialInlineCommentEditor(config.uri)
        .setTemplates(view.getUndoTemplates())
        .setOperation(op)
        .setID(data.id)
        .setChangesetID(data.changesetID)
        .setLineNumber(data.number)
        .setLength(data.length)
        .setOnRight(data.on_right)
        .setOriginalText(original)
        .setRow(row)
        .setTable(row.parentNode)
        .setReplyToCommentPHID(reply_phid)
        .setRenderer(view.getRenderer())
        .start();
  
      set_link_state(true);
    };
  
    for (var op in {'edit': 1, 'delete': 1, 'reply': 1, 'done': 1}) {
      JX.Stratcom.listen(
        'click',
        ['differential-inline-comment', 'differential-inline-' + op],
        JX.bind(null, action_handler, op));
    }
  
    JX.Stratcom.listen(
      'differential-inline-action',
      null,
      function(e) {
        var data = e.getData();
        handle_inline_action(data.node, data.op);
      });
  
    // Respond to the user clicking the "Hide Inline" button on an inline
    // comment.
    JX.Stratcom.listen('click', 'hide-inline', function(e) {
      e.kill();
  
      var row = e.getNode('inline-row');
      JX.DOM.hide(row);
  
      var prev = row.previousSibling;
      while (prev && JX.Stratcom.hasSigil(prev, 'inline-row')) {
        prev = prev.previousSibling;
      }
  
      if (!prev) {
        return;
      }
  
      var comment = e.getNodeData('differential-inline-comment');
  
      var slots = [];
      for (var ii = 0; ii < prev.childNodes.length; ii++) {
        if (JX.DOM.isType(prev.childNodes[ii], 'th')) {
          slots.push(prev.childNodes[ii]);
        }
      }
  
      // Select the right-hand side if the comment is on the right.
      var slot = (comment.on_right && slots[1]) || slots[0];
  
      var reveal = JX.DOM.scry(slot, 'a', 'reveal-inlines')[0];
      if (!reveal) {
        reveal = JX.$N(
          'a',
          {
            className: 'reveal-inlines',
            sigil: 'reveal-inlines'
          },
          JX.$H(config.revealIcon));
  
        JX.DOM.prependContent(slot, reveal);
      }
  
      new JX.Workflow(config.uri, {op: 'hide', ids: comment.id})
        .setHandler(JX.bag)
        .start();
    });
  
    JX.Stratcom.listen('click', 'reveal-inlines', function(e) {
      e.kill();
  
      var row = e.getNode('tag:tr');
      var next = row.nextSibling;
  
      var ids = [];
      var ii;
  
      // Show any hidden inline comment rows directly below this one.
      while (next && JX.Stratcom.hasSigil(next, 'inline-row')) {
        JX.DOM.show(next);
  
        var comments = JX.DOM.scry(next, 'div', 'differential-inline-comment');
        for (ii = 0; ii < comments.length; ii++) {
          var id = JX.Stratcom.getData(comments[ii]).id;
          if (id) {
            ids.push(id);
          }
        }
  
        next = next.nextSibling;
      }
  
      // Remove any "reveal" icons on the row.
      var reveals = JX.DOM.scry(row, 'a', 'reveal-inlines');
      for (ii = 0; ii < reveals.length; ii++) {
        JX.DOM.remove(reveals[ii]);
      }
  
      new JX.Workflow(config.uri, {op: 'show', ids: ids.join(',')})
        .setHandler(JX.bag)
        .start();
    });
  
  
  });
  