/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

/*
 * Selection handler for chrome text inputs
 */

let Ci = Components.interfaces;

var ChromeSelectionHandler = {
  _mode: this._SELECTION_MODE,

  /*************************************************
   * Messaging wrapper
   */

  sendAsync: function sendAsync(aMsg, aJson) {
    SelectionHelperUI.receiveMessage({ 
      name: aMsg,
      json: aJson
    });
  },

  /*************************************************
   * Browser event handlers
   */

  /*
   * General selection start method for both caret and selection mode.
   */
  _onSelectionAttach: function _onSelectionAttach(aJson) {
    // Clear previous ChromeSelectionHandler state.
    this._deactivate();

    // Initialize ChromeSelectionHandler state.
    this._domWinUtils = Util.getWindowUtils(window);
    this._contentWindow = window;
    this._targetElement = aJson.target;
    this._targetIsEditable = Util.isTextInput(this._targetElement) ||
        this._targetElement instanceof Ci.nsIDOMXULTextBoxElement;
    if (!this._targetIsEditable) {
      this._onFail("not an editable?", this._targetElement);
      return;
    }

    let selection = this._getSelection();
    if (!selection) {
      this._onFail("no selection.");
      return;
    }

    if (!this._getTargetElementValue()) {
      this._onFail("Target element does not contain any content to select.");
      return;
    }

    // Add a listener to respond to programmatic selection changes.
    selection.QueryInterface(Ci.nsISelectionPrivate).addSelectionListener(this);

    if (!selection.isCollapsed) {
      this._mode = this._SELECTION_MODE;
      this._updateSelectionUI("start", true, true);
    } else {
      this._mode = this._CARET_MODE;
      this._updateSelectionUI("caret", false, false, true);
    }

    this._targetElement.addEventListener("blur", this, false);
  },

  /*
   * Selection monocle start move event handler
   */
  _onSelectionMoveStart: function _onSelectionMoveStart(aMsg) {
    if (!this.targetIsEditable) {
      this._onFail("_onSelectionMoveStart with bad targetElement.");
      return;
    }

    if (this._selectionMoveActive) {
      this._onFail("mouse is already down on drag start?");
      return;
    }

    // We bail if things get out of sync here implying we missed a message.
    this._selectionMoveActive = true;

    if (this._targetIsEditable) {
      // If we're coming out of an out-of-bounds scroll, the node the user is
      // trying to drag may be hidden (the monocle will be pegged to the edge
      // of the edit). Make sure the node the user wants to move is visible
      // and has focus.
      this._updateInputFocus(aMsg.change);
    }

    // Update the position of our selection monocles
    this._updateSelectionUI("update", true, true);
  },
  
  /*
   * Selection monocle move event handler
   */
  _onSelectionMove: function _onSelectionMove(aMsg) {
    if (!this.targetIsEditable) {
      this._onFail("_onSelectionMove with bad targetElement.");
      return;
    }

    if (!this._selectionMoveActive) {
      this._onFail("mouse isn't down for drag move?");
      return;
    }

    this._handleSelectionPoint(aMsg, false);
  },

  /*
   * Selection monocle move finished event handler
   */
  _onSelectionMoveEnd: function _onSelectionMoveComplete(aMsg) {
    if (!this.targetIsEditable) {
      this._onFail("_onSelectionMoveEnd with bad targetElement.");
      return;
    }

    if (!this._selectionMoveActive) {
      this._onFail("mouse isn't down for drag move?");
      return;
    }

    this._handleSelectionPoint(aMsg, true);
    this._selectionMoveActive = false;
    
    // Clear any existing scroll timers
    this._clearTimers();

    // Update the position of our selection monocles
    this._updateSelectionUI("end", true, true);
  },

  _onSelectionUpdate: function _onSelectionUpdate() {
    if (!this._targetHasFocus()) {
      this._closeSelection();
      return;
    }
    this._updateSelectionUI("update",
                            this._mode == this._SELECTION_MODE,
                            this._mode == this._SELECTION_MODE,
                            this._mode == this._CARET_MODE);
  },

  /*
   * Switch selection modes. Currently we only support switching
   * from "caret" to "selection".
   */
  _onSwitchMode: function _onSwitchMode(aMode, aMarker, aX, aY) {
    if (aMode != "selection") {
      this._onFail("unsupported mode switch");
      return;
    }
    
    // Sanity check to be sure we are initialized
    if (!this._targetElement) {
      this._onFail("not initialized");
      return;
    }

    // Similar to _onSelectionStart - we need to create initial selection
    // but without the initialization bits.
    let framePoint = this._clientPointToFramePoint({ xPos: aX, yPos: aY });
    if (!this._domWinUtils.selectAtPoint(framePoint.xPos, framePoint.yPos,
                                         Ci.nsIDOMWindowUtils.SELECT_CHARACTER)) {
      this._onFail("failed to set selection at point");
      return;
    }

    // We bail if things get out of sync here implying we missed a message.
    this._selectionMoveActive = true;
    this._mode = this._SELECTION_MODE;

    // Update the position of the selection marker that is *not*
    // being dragged.
    this._updateSelectionUI("update", aMarker == "end", aMarker == "start");
  },

  /*
   * Selection close event handler
   *
   * @param aClearSelection requests that selection be cleared.
   */
  _onSelectionClose: function _onSelectionClose(aClearSelection) {
    if (aClearSelection) {
      this._clearSelection();
    }
    this._closeSelection();
  },

  /*
   * Called if for any reason we fail during the selection
   * process. Cancels the selection.
   */
  _onFail: function _onFail(aDbgMessage) {
    if (aDbgMessage && aDbgMessage.length > 0)
      Util.dumpLn(aDbgMessage);
    this.sendAsync("Content:SelectionFail");
    this._clearSelection();
    this._closeSelection();
  },

  /*
   * Empty conversion routines to match those in
   * browser. Called by SelectionHelperUI when
   * sending and receiving coordinates in messages.
   */

  ptClientToBrowser: function ptClientToBrowser(aX, aY, aIgnoreScroll, aIgnoreScale) {
    return { x: aX, y: aY }
  },

  rectBrowserToClient: function rectBrowserToClient(aRect, aIgnoreScroll, aIgnoreScale) {
    return {
      left: aRect.left,
      right: aRect.right,
      top: aRect.top,
      bottom: aRect.bottom
    }
  },

  ptBrowserToClient: function ptBrowserToClient(aX, aY, aIgnoreScroll, aIgnoreScale) {
    return { x: aX, y: aY }
  },

  ctobx: function ctobx(aCoord) {
    return aCoord;
  },

  ctoby: function ctoby(aCoord) {
    return aCoord;
  },

  btocx: function btocx(aCoord) {
    return aCoord;
  },

  btocy: function btocy(aCoord) {
    return aCoord;
  },


  /*************************************************
   * Selection helpers
   */

  /*
   * _clearSelection
   *
   * Clear existing selection if it exists and reset our internal state.
   */
  _clearSelection: function _clearSelection() {
    let selection = this._getSelection();
    if (selection) {
      selection.removeAllRanges();
    }
  },

  /*
   * _closeSelection
   *
   * Shuts ChromeSelectionHandler and SelectionHelperUI down.
   */
  _closeSelection: function _closeSelection() {
    this._deactivate();
    this.sendAsync("Content:HandlerShutdown", {});
  },

  /*
   * _deactivate
   *
   * Resets ChromeSelectionHandler state, previously initialized in
   * general selection start-method |_onSelectionAttach()|.
   */
  _deactivate: function _deactivate() {
    // Remove our selection notification listener.
    let selection = this._getSelection();
    if (selection) {
      try {
        selection.QueryInterface(Ci.nsISelectionPrivate).removeSelectionListener(this);
      } catch(e) {
        // Fail safe during multiple _deactivate() calls.
      }
    }

    this._clearTimers();
    this._cache = null;
    this._contentWindow = null;
    if (this._targetElement) {
      this._targetElement.removeEventListener("blur", this, true);
      this._targetElement = null;
    }
    this._selectionMoveActive = false;
    this._domWinUtils = null;
    this._targetIsEditable = false;
    this._mode = null;
  },

  get hasSelection() {
    if (!this._targetElement) {
      return false;
    }
    let selection = this._getSelection();
    return (selection && !selection.isCollapsed);
  },

  _targetHasFocus: function() {
    if (!this._targetElement || !document.commandDispatcher.focusedElement) {
      return false;
    }
    let bindingParent = this._contentWindow.document.getBindingParent(document.commandDispatcher.focusedElement);
    return (bindingParent && this._targetElement == bindingParent);
  },

  /*************************************************
   * Events
   */

  /*
   * Scroll + selection advancement timer when the monocle is
   * outside the bounds of an input control.
   */
  scrollTimerCallback: function scrollTimerCallback() {
    let result = ChromeSelectionHandler.updateTextEditSelection();
    // Update monocle position and speed if we've dragged off to one side
    if (result.trigger) {
      ChromeSelectionHandler._updateSelectionUI("update", result.start, result.end);
    }
  },

  handleEvent: function handleEvent(aEvent) {
    if (aEvent.type == "blur" && !this._targetHasFocus()) {
      this._closeSelection();
    }
  },

  msgHandler: function msgHandler(aMsg, aJson) {
    if (this._debugEvents && "Browser:SelectionMove" != aMsg) {
      Util.dumpLn("ChromeSelectionHandler:", aMsg);
    }
    switch(aMsg) {
      case "Browser:SelectionDebug":
        this._onSelectionDebug(aJson);
        break;

      case "Browser:SelectionAttach":
        this._onSelectionAttach(aJson);
      break;

      case "Browser:CaretAttach":
        this._onSelectionAttach(aJson);
        break;

      case "Browser:SelectionClose":
        this._onSelectionClose(aJson.clearSelection);
      break;

      case "Browser:SelectionUpdate":
        this._onSelectionUpdate();
      break;

      case "Browser:SelectionMoveStart":
        this._onSelectionMoveStart(aJson);
        break;

      case "Browser:SelectionMove":
        this._onSelectionMove(aJson);
        break;

      case "Browser:SelectionMoveEnd":
        this._onSelectionMoveEnd(aJson);
        break;

      case "Browser:CaretUpdate":
        this._onCaretPositionUpdate(aJson.caret.xPos, aJson.caret.yPos);
        break;

      case "Browser:CaretMove":
        this._onCaretMove(aJson.caret.xPos, aJson.caret.yPos);
        break;

      case "Browser:SelectionSwitchMode":
        this._onSwitchMode(aJson.newMode, aJson.change, aJson.xPos, aJson.yPos);
        break;
    }
  },

  /*************************************************
   * Utilities
   */

  _getSelection: function _getSelection() {
    let targetElementEditor = this._getTargetElementEditor();

    return targetElementEditor ? targetElementEditor.selection : null;
  },

  _getTargetElementValue: function _getTargetElementValue() {
    if (this._targetElement instanceof Ci.nsIDOMXULTextBoxElement) {
      return this._targetElement.inputField.value;
    } else if (Util.isTextInput(this._targetElement)) {
      return this._targetElement.value;
    }
    return null;
  },

  _getSelectController: function _getSelectController() {
    let targetElementEditor = this._getTargetElementEditor();

    return targetElementEditor ? targetElementEditor.selectionController : null;
  },

  _getTargetElementEditor: function() {
    if (this._targetElement instanceof Ci.nsIDOMXULTextBoxElement) {
      return this._targetElement.QueryInterface(Ci.nsIDOMXULTextBoxElement)
          .editor;
    } else if (Util.isTextInput(this._targetElement)) {
      return this._targetElement.QueryInterface(Ci.nsIDOMNSEditableElement)
          .editor;
    }
    return null;
  }
};

ChromeSelectionHandler.__proto__ = new SelectionPrototype();
ChromeSelectionHandler.type = 1; // kChromeSelector

