/*
 * This Source Code is subject to the terms of the Mozilla Public License
 * version 2.0 (the "License"). You can obtain a copy of the License at
 * http://mozilla.org/MPL/2.0/.
 */

/**
 * Implementation of the various actions performed on the rules.
 * @class
 */
var RuleActions =
{
  /**
   * Initializes rule actions.
   */
  init: function()
  {
    let me = this;
    this.treeElement.parentNode.addEventListener("keypress", function(event)
    {
      me.keyPress(event);
    }, true);
    this.treeElement.view = RuleView;

    this.treeElement.inputField.addEventListener("keypress", function(event)
    {
      // Prevent the tree from capturing cursor keys pressed in the input field
      if (event.keyCode >= event.DOM_VK_PAGE_UP && event.keyCode <= event.DOM_VK_DOWN)
        event.stopPropagation();
    }, false);

    // Create a copy of the view menu
    function fixId(node, newId)
    {
      if (node.nodeType == node.ELEMENT_NODE)
      {
        if (node.hasAttribute("id"))
          node.setAttribute("id", node.getAttribute("id").replace(/\d+$/, newId));

        for (let i = 0, len = node.childNodes.length; i < len; i++)
          fixId(node.childNodes[i], newId);
      }
      return node;
    }
    E("viewMenu").appendChild(fixId(E("rules-view-menu1").cloneNode(true), "2"));
  },

  /**
   * <tree> element containing the rules.
   * @type XULElement
   */
  get treeElement() E("rulesTree"),

  /**
   * Tests whether the tree is currently visible.
   */
  get visible()
  {
    return !this.treeElement.parentNode.collapsed;
  },

  /**
   * Tests whether the tree is currently focused.
   * @type Boolean
   */
  get focused()
  {
    let focused = document.commandDispatcher.focusedElement;
    while (focused)
    {
      if ("treeBoxObject" in focused && focused.treeBoxObject == RuleView.boxObject)
        return true;
      focused = focused.parentNode;
    }
    return false;
  },

  /**
   * Updates visible rule commands whenever the selected subscription changes.
   */
  updateCommands: function()
  {
    E("rules-add-command").setAttribute("disabled", !RuleView.editable);
  },

  /**
   * Called whenever rule actions menu is opened, initializes menu items.
   */
  fillActionsPopup: function()
  {
    let editable = RuleView.editable;
    let items = RuleView.selectedItems.filter(function(i) !i.rule.dummy);
    items.sort(function(entry1, entry2) entry1.index - entry2.index);
    let activeItems = items.filter(function(i) i.rule instanceof ActiveRule);

    E("rules-edit-command").setAttribute("disabled", !editable || !items.length);
    E("rules-delete-command").setAttribute("disabled", !editable || !items.length);
    E("rules-resetHitCounts-command").setAttribute("disabled", !activeItems.length);
    E("rules-moveUp-command").setAttribute("disabled", !editable || RuleView.isSorted() || !items.length || items[0].index == 0);
    E("rules-moveDown-command").setAttribute("disabled", !editable || RuleView.isSorted() || !items.length || items[items.length - 1].index == RuleView.rowCount - 1);
    E("rules-copy-command").setAttribute("disabled", !items.length);
    E("rules-cut-command").setAttribute("disabled", !editable || !items.length);
    E("rules-paste-command").setAttribute("disabled", !editable || !Utils.clipboard.hasDataMatchingFlavors(["text/unicode"], 1, Utils.clipboard.kGlobalClipboard));
  },

  /**
   * Changes sort current order for the tree. Sorts by rule column if the list is unsorted.
   * @param {String} order  either "ascending" or "descending"
   */
  setSortOrder: function(sortOrder)
  {
    let col = (RuleView.sortColumn ? RuleView.sortColumn.id : "col-rule");
    RuleView.sortBy(col, sortOrder);
  },

  /**
   * Toggles the visibility of a tree column.
   */
  toggleColumn: function(/**String*/ id)
  {
    let col = E(id);
    col.setAttribute("hidden", col.hidden ? "false" : "true");
  },

  /**
   * Enables or disables all rules in the current selection.
   */
  selectionToggleDisabled: function()
  {
    if (this.treeElement.editingColumn)
      return;

    let items = RuleView.selectedItems.filter(function(i) i.rule instanceof ActiveRule);
    if (items.length)
    {
      RuleView.boxObject.beginUpdateBatch();
      let newValue = !items[0].rule.disabled;
      for (let i = 0; i < items.length; i++)
        items[i].rule.disabled = newValue;
      RuleView.boxObject.endUpdateBatch();
    }
  },

  /**
   * Selects all entries in the list.
   */
  selectAll: function()
  {
    if (this.treeElement.editingColumn)
      return;

    RuleView.selection.selectAll();
    this.treeElement.focus();
  },

  /**
   * Starts editing the current rule.
   */
  startEditing: function()
  {
    if (this.treeElement.editingColumn)
      return;

    this.treeElement.startEditing(RuleView.selection.currentIndex, RuleView.boxObject.columns.getNamedColumn("col-rule"));
  },

  /**
   * Starts editing a new rule at the current position.
   */
  insertRule: function()
  {
    if (!RuleView.editable || this.treeElement.editingColumn)
      return;

    RuleView.insertEditDummy();
    this.startEditing();

    let tree = this.treeElement;
    let listener = function(event)
    {
      if (event.attrName == "editing" && tree.editingRow < 0)
      {
        tree.removeEventListener("DOMAttrModified", listener, false);
        RuleView.removeEditDummy();
      }
    }
    tree.addEventListener("DOMAttrModified", listener, false);
  },

  /**
   * Deletes items from the list.
   */
  deleteItems: function(/**Array*/ items)
  {
    let oldIndex = RuleView.selection.currentIndex;
    items.sort(function(entry1, entry2) entry2.index - entry1.index);

    for (let i = 0; i < items.length; i++)
      RuleStorage.removeRule(items[i].rule, RuleView._subscription, items[i].index);

    RuleView.selectRow(oldIndex);
  },

  /**
   * Deletes selected rules.
   */
  deleteSelected: function()
  {
    if (!RuleView.editable || this.treeElement.editingColumn)
      return;

    let items = RuleView.selectedItems;
    if (items.length == 0 || (items.length >= 2 && !Utils.confirm(window, this.treeElement.getAttribute("_removewarning"))))
      return;

    this.deleteItems(items)
  },

  /**
   * Resets hit counts of the selected rules.
   */
  resetHitCounts: function()
  {
    if (this.treeElement.editingColumn)
      return;

    let items = RuleView.selectedItems.filter(function(i) i.rule instanceof ActiveRule);
    if (items.length)
      RuleStorage.resetHitCounts(items.map(function(i) i.rule));
  },

  /**
   * Moves items to a different position in the list.
   * @param {Array} items
   * @param {Integer} offset  negative offsets move the items up, positive down
   */
  _moveItems: function(/**Array*/ items, /**Integer*/ offset)
  {
    if (!items.length)
      return;

    if (offset < 0)
    {
      items.sort(function(entry1, entry2) entry1.index - entry2.index);
      let position = items[0].index + offset;
      if (position < 0)
        return;

      for (let i = 0; i < items.length; i++)
        RuleStorage.moveRule(items[i].rule, RuleView._subscription, items[i].index, position++);
      RuleView.selection.rangedSelect(position - items.length, position - 1, false);
    }
    else if (offset > 0)
    {
      items.sort(function(entry1, entry2) entry2.index - entry1.index);
      let position = items[0].index + offset;
      if (position >= RuleView.rowCount)
        return;

      for (let i = 0; i < items.length; i++)
        RuleStorage.moveRule(items[i].rule, RuleView._subscription, items[i].index, position--);
      RuleView.selection.rangedSelect(position + 1, position + items.length, false);
    }
  },

  /**
   * Moves selected rules one line up.
   */
  moveUp: function()
  {
    if (!RuleView.editable || RuleView.isEmpty || RuleView.isSorted() || this.treeElement.editingColumn)
      return;

    this._moveItems(RuleView.selectedItems, -1);
  },

  /**
   * Moves selected rules one line down.
   */
  moveDown: function()
  {
    if (!RuleView.editable || RuleView.isEmpty || RuleView.isSorted() || this.treeElement.editingColumn)
      return;

    this._moveItems(RuleView.selectedItems, 1);
  },

  /**
   * Fills the context menu of the rules columns.
   */
  fillColumnPopup: function(/**Element*/ element)
  {
    let suffix = element.id.match(/\d+$/)[0] || "1";

    E("rules-view-rule" + suffix).setAttribute("checked", !E("col-rule").hidden);
    E("rules-view-slow" + suffix).setAttribute("checked", !E("col-slow").hidden);
    E("rules-view-enabled" + suffix).setAttribute("checked", !E("col-enabled").hidden);
    E("rules-view-hitcount" + suffix).setAttribute("checked", !E("col-hitcount").hidden);
    E("rules-view-lasthit" + suffix).setAttribute("checked", !E("col-lasthit").hidden);

    let sortColumn = RuleView.sortColumn;
    let sortColumnID = (sortColumn ? sortColumn.id : null);
    let sortDir = (sortColumn ? sortColumn.getAttribute("sortDirection") : "natural");
    E("rules-sort-none" + suffix).setAttribute("checked", sortColumn == null);
    E("rules-sort-rule" + suffix).setAttribute("checked", sortColumnID == "col-rule");
    E("rules-sort-enabled" + suffix).setAttribute("checked", sortColumnID == "col-enabled");
    E("rules-sort-hitcount" + suffix).setAttribute("checked", sortColumnID == "col-hitcount");
    E("rules-sort-lasthit" + suffix).setAttribute("checked", sortColumnID == "col-lasthit");
    E("rules-sort-asc" + suffix).setAttribute("checked", sortDir == "ascending");
    E("rules-sort-desc" + suffix).setAttribute("checked", sortDir == "descending");
  },

  /**
   * Fills tooltip with the item data.
   */
  fillTooltip: function(event)
  {
    let item = RuleView.getItemAt(event.clientX, event.clientY);
    if (!item || item.rule.dummy)
    {
      event.preventDefault();
      return;
    }

    function setMultilineContent(box, text)
    {
      while (box.firstChild)
        box.removeChild(box.firstChild);

      for (var i = 0; i < text.length; i += 80)
      {
        var description = document.createElement("description");
        description.setAttribute("value", text.substr(i, 80));
        box.appendChild(description);
      }
    }

    setMultilineContent(E("tooltip-rule"), item.rule.text);

    E("tooltip-hitcount-row").hidden = !(item.rule instanceof ActiveRule);
    E("tooltip-lasthit-row").hidden = !(item.rule instanceof ActiveRule) || !item.rule.lastHit;
    if (item.rule instanceof ActiveRule)
    {
      E("tooltip-hitcount").setAttribute("value", item.rule.hitCount)
      E("tooltip-lasthit").setAttribute("value", Utils.formatTime(item.rule.lastHit))
    }

    E("tooltip-additional").hidden = false;
    if (item.rule instanceof InvalidRule && item.rule.reason)
      E("tooltip-additional").textContent = item.rule.reason;
    else if (item.rule instanceof RegExpRule && AllMatchers.isSlowRule(item.rule))
      E("tooltip-additional").textContent = Utils.getString("rule_regexp_tooltip");
    else
      E("tooltip-additional").hidden = true;
  },

  /**
   * Called whenever a key is pressed on the list.
   */
  keyPress: function(/**Event*/ event)
  {
    if (event.target != E("rulesTree"))
      return;

    let modifiers = 0;
    if (event.altKey)
      modifiers |= SubscriptionActions._altMask;
    if (event.ctrlKey)
      modifiers |= SubscriptionActions._ctrlMask;
    if (event.metaKey)
      modifiers |= SubscriptionActions._metaMask;

    if (event.charCode == " ".charCodeAt(0) && modifiers == 0 && !E("col-enabled").hidden)
      this.selectionToggleDisabled();
    else if (event.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_UP && modifiers == SubscriptionActions._accelMask)
    {
      E("rules-moveUp-command").doCommand();
      event.preventDefault();
      event.stopPropagation();
    }
    else if (event.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_DOWN && modifiers == SubscriptionActions._accelMask)
    {
      E("rules-moveDown-command").doCommand();
      event.preventDefault();
      event.stopPropagation();
    }
  },

  /**
   * Copies selected items to clipboard and optionally removes them from the
   * list after that.
   */
  copySelected: function(/**Boolean*/ keep)
  {
    let items = RuleView.selectedItems;
    if (!items.length)
      return;

    items.sort(function(entry1, entry2) entry1.index - entry2.index);
    let text = items.map(function(i) i.rule.text).join(Utils.getLineBreak());
    Utils.clipboardHelper.copyString(text);

    if (!keep && RuleView.editable && !this.treeElement.editingColumn)
      this.deleteItems(items);
  },

  /**
   * Pastes text from clipboard as rules at the current position.
   */
  paste: function()
  {
    if (!RuleView.editable || this.treeElement.editingColumn)
      return;

    let transferable = Cc["@mozilla.org/widget/transferable;1"].createInstance(Ci.nsITransferable);
    transferable.addDataFlavor("text/unicode");

    let data;
    try
    {
      data = {};
      Utils.clipboard.getData(transferable, Utils.clipboard.kGlobalClipboard);
      transferable.getTransferData("text/unicode", data, {});
      data = data.value.QueryInterface(Ci.nsISupportsString).data;
    }
    catch (e) {
      return;
    }

    let item = RuleView.currentItem;
    let position = (item ? item.index : RuleView.data.length);

    let lines = data.replace(/\r/g, "").split("\n");
    for (let i = 0; i < lines.length; i++)
    {
      let rule = Rule.fromText(lines[i]);
      if (rule)
        RuleStorage.addRule(rule, RuleView._subscription, position++);
    }
  },

  dragItems: null,

  /**
   * Called whenever the user starts a drag operation.
   */
  startDrag: function(/**Event*/ event)
  {
    let items = RuleView.selectedItems;
    if (!items.length)
      return;

    items.sort(function(entry1, entry2) entry1.index - entry2.index);
    event.dataTransfer.setData("text/plain", items.map(function(i) i.rule.text).join(Utils.getLineBreak()));
    this.dragItems = items;
    event.stopPropagation();
  },

  /**
   * Called to check whether moving the items to the given position is possible.
   */
  canDrop: function(/**Integer*/ newPosition, /**nsIDOMDataTransfer*/ dataTransfer)
  {
    if (!RuleView.editable || this.treeElement.editingColumn)
      return false;

    // If we aren't dragging items then maybe we got rules as plain text
    if (!this.dragItems)
      return dataTransfer && dataTransfer.getData("text/plain");

    if (RuleView.isEmpty || RuleView.isSorted())
      return false;

    if (newPosition < this.dragItems[0].index)
      return true;
    else if (newPosition > this.dragItems[this.dragItems.length - 1].index + 1)
      return true;
    else
      return false;
  },

  /**
   * Called when the user decides to drop the items.
   */
  drop: function(/**Integer*/ newPosition, /**nsIDOMDataTransfer*/ dataTransfer)
  {
    if (!RuleView.editable || this.treeElement.editingColumn)
      return;

    if (!this.dragItems)
    {
      // We got rules as plain text, insert them into the list
      let data = (dataTransfer ? dataTransfer.getData("text/plain") : null);
      if (data)
      {
        let lines = data.replace(/\r/g, "").split("\n");
        for (let i = 0; i < lines.length; i++)
        {
          let rule = Rule.fromText(lines[i]);
          if (rule)
            RuleStorage.addRule(rule, RuleView._subscription, newPosition++);
        }
      }
      return;
    }

    if (RuleView.isEmpty || RuleView.isSorted())
      return;

    if (newPosition < this.dragItems[0].index)
      this._moveItems(this.dragItems, newPosition - this.dragItems[0].index);
    else if (newPosition > this.dragItems[this.dragItems.length - 1].index + 1)
      this._moveItems(this.dragItems, newPosition - this.dragItems[this.dragItems.length - 1].index - 1);
  },

  /**
   * Called whenever the a drag operation finishes.
   */
  endDrag: function(/**Event*/ event)
  {
    this.dragItems = null;
  },

  /**
   * Called if rules have been dragged into a subscription and need to be removed.
   */
  removeDraggedRules: function()
  {
    if (!this.dragItems)
      return;

    this.deleteItems(this.dragItems);
  }
};

window.addEventListener("load", function()
{
  RuleActions.init();
}, false);
