/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* 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/. */
/* import-globals-from ../debugger-controller.js */
/* import-globals-from ../debugger-view.js */
/* import-globals-from ../utils.js */
/* globals document, window */
"use strict";

const {setTooltipVariableContent} = require("devtools/client/shared/widgets/tooltip/VariableContentHelper");

/**
 * Functions handling the variables bubble UI.
 */
function VariableBubbleView(DebuggerController, DebuggerView) {
  dumpn("VariableBubbleView was instantiated");

  this.StackFrames = DebuggerController.StackFrames;
  this.Parser = DebuggerController.Parser;
  this.DebuggerView = DebuggerView;

  this._onMouseMove = this._onMouseMove.bind(this);
  this._onMouseOut = this._onMouseOut.bind(this);
  this._onPopupHiding = this._onPopupHiding.bind(this);
}

VariableBubbleView.prototype = {
  /**
   * Delay before showing the variables bubble tooltip when hovering a valid
   * target.
   */
  TOOLTIP_SHOW_DELAY: 750,

  /**
   * Tooltip position for the variables bubble tooltip.
   */
  TOOLTIP_POSITION: "topcenter bottomleft",

  /**
   * Initialization function, called when the debugger is started.
   */
  initialize: function () {
    dumpn("Initializing the VariableBubbleView");

    this._toolbox = DebuggerController._toolbox;
    this._editorContainer = document.getElementById("editor");
    this._editorContainer.addEventListener("mousemove", this._onMouseMove, false);
    this._editorContainer.addEventListener("mouseout", this._onMouseOut, false);

    this._tooltip = new Tooltip(document, {
      closeOnEvents: [{
        emitter: this._toolbox,
        event: "select"
      }, {
        emitter: this._editorContainer,
        event: "scroll",
        useCapture: true
      }, {
        emitter: document,
        event: "keydown"
      }]
    });
    this._tooltip.defaultPosition = this.TOOLTIP_POSITION;
    this._tooltip.panel.addEventListener("popuphiding", this._onPopupHiding);
  },

  /**
   * Destruction function, called when the debugger is closed.
   */
  destroy: function () {
    dumpn("Destroying the VariableBubbleView");

    this._tooltip.panel.removeEventListener("popuphiding", this._onPopupHiding);
    this._editorContainer.removeEventListener("mousemove", this._onMouseMove, false);
    this._editorContainer.removeEventListener("mouseout", this._onMouseOut, false);
  },

  /**
   * Specifies whether literals can be (redundantly) inspected in a popup.
   * This behavior is deprecated, but still tested in a few places.
   */
  _ignoreLiterals: true,

  /**
   * Searches for an identifier underneath the specified position in the
   * source editor, and if found, opens a VariablesView inspection popup.
   *
   * @param number x, y
   *        The left/top coordinates where to look for an identifier.
   */
  _findIdentifier: function (x, y) {
    let editor = this.DebuggerView.editor;

    // Calculate the editor's line and column at the current x and y coords.
    let hoveredPos = editor.getPositionFromCoords({ left: x, top: y });
    let hoveredOffset = editor.getOffset(hoveredPos);
    let hoveredLine = hoveredPos.line;
    let hoveredColumn = hoveredPos.ch;

    // A source contains multiple scripts. Find the start index of the script
    // containing the specified offset relative to its parent source.
    let contents = editor.getText();
    let location = this.DebuggerView.Sources.selectedValue;
    let parsedSource = this.Parser.get(contents, location);
    let scriptInfo = parsedSource.getScriptInfo(hoveredOffset);

    // If the script length is negative, we're not hovering JS source code.
    if (scriptInfo.length == -1) {
      return;
    }

    // Using the script offset, determine the actual line and column inside the
    // script, to use when finding identifiers.
    let scriptStart = editor.getPosition(scriptInfo.start);
    let scriptLineOffset = scriptStart.line;
    let scriptColumnOffset = (hoveredLine == scriptStart.line ? scriptStart.ch : 0);

    let scriptLine = hoveredLine - scriptLineOffset;
    let scriptColumn = hoveredColumn - scriptColumnOffset;
    let identifierInfo = parsedSource.getIdentifierAt({
      line: scriptLine + 1,
      column: scriptColumn,
      scriptIndex: scriptInfo.index,
      ignoreLiterals: this._ignoreLiterals
    });

    // If the info is null, we're not hovering any identifier.
    if (!identifierInfo) {
      return;
    }

    // Transform the line and column relative to the parsed script back
    // to the context of the parent source.
    let { start: identifierStart, end: identifierEnd } = identifierInfo.location;
    let identifierCoords = {
      line: identifierStart.line + scriptLineOffset,
      column: identifierStart.column + scriptColumnOffset,
      length: identifierEnd.column - identifierStart.column
    };

    // Evaluate the identifier in the current stack frame and show the
    // results in a VariablesView inspection popup.
    this.StackFrames.evaluate(identifierInfo.evalString)
      .then(frameFinished => {
        if ("return" in frameFinished) {
          this.showContents({
            coords: identifierCoords,
            evalPrefix: identifierInfo.evalString,
            objectActor: frameFinished.return
          });
        } else {
          let msg = "Evaluation has thrown for: " + identifierInfo.evalString;
          console.warn(msg);
          dumpn(msg);
        }
      })
      .then(null, err => {
        let msg = "Couldn't evaluate: " + err.message;
        console.error(msg);
        dumpn(msg);
      });
  },

  /**
   * Shows an inspection popup for a specified object actor grip.
   *
   * @param string object
   *        An object containing the following properties:
   *          - coords: the inspected identifier coordinates in the editor,
   *                    containing the { line, column, length } properties.
   *          - evalPrefix: a prefix for the variables view evaluation macros.
   *          - objectActor: the value grip for the object actor.
   */
  showContents: function ({ coords, evalPrefix, objectActor }) {
    let editor = this.DebuggerView.editor;
    let { line, column, length } = coords;

    // Highlight the function found at the mouse position.
    this._markedText = editor.markText(
      { line: line - 1, ch: column },
      { line: line - 1, ch: column + length });

    // If the grip represents a primitive value, use a more lightweight
    // machinery to display it.
    if (VariablesView.isPrimitive({ value: objectActor })) {
      let className = VariablesView.getClass(objectActor);
      let textContent = VariablesView.getString(objectActor);
      this._tooltip.setTextContent({
        messages: [textContent],
        messagesClass: className,
        containerClass: "plain"
      }, [{
        label: L10N.getStr("addWatchExpressionButton"),
        className: "dbg-expression-button",
        command: () => {
          this.DebuggerView.VariableBubble.hideContents();
          this.DebuggerView.WatchExpressions.addExpression(evalPrefix, true);
        }
      }]);
    } else {
      setTooltipVariableContent(this._tooltip, objectActor, {
        searchPlaceholder: L10N.getStr("emptyPropertiesFilterText"),
        searchEnabled: Prefs.variablesSearchboxVisible,
        eval: (variable, value) => {
          let string = variable.evaluationMacro(variable, value);
          this.StackFrames.evaluate(string);
          this.DebuggerView.VariableBubble.hideContents();
        }
      }, {
        getEnvironmentClient: aObject => gThreadClient.environment(aObject),
        getObjectClient: aObject => gThreadClient.pauseGrip(aObject),
        simpleValueEvalMacro: this._getSimpleValueEvalMacro(evalPrefix),
        getterOrSetterEvalMacro: this._getGetterOrSetterEvalMacro(evalPrefix),
        overrideValueEvalMacro: this._getOverrideValueEvalMacro(evalPrefix)
      }, {
        fetched: (aEvent, aType) => {
          if (aType == "properties") {
            window.emit(EVENTS.FETCHED_BUBBLE_PROPERTIES);
          }
        }
      }, [{
        label: L10N.getStr("addWatchExpressionButton"),
        className: "dbg-expression-button",
        command: () => {
          this.DebuggerView.VariableBubble.hideContents();
          this.DebuggerView.WatchExpressions.addExpression(evalPrefix, true);
        }
      }], this._toolbox);
    }

    this._tooltip.show(this._markedText.anchor);
  },

  /**
   * Hides the inspection popup.
   */
  hideContents: function () {
    clearNamedTimeout("editor-mouse-move");
    this._tooltip.hide();
  },

  /**
   * Checks whether the inspection popup is shown.
   *
   * @return boolean
   *         True if the panel is shown or showing, false otherwise.
   */
  contentsShown: function () {
    return this._tooltip.isShown();
  },

  /**
   * Functions for getting customized variables view evaluation macros.
   *
   * @param string aPrefix
   *        See the corresponding VariablesView.* functions.
   */
  _getSimpleValueEvalMacro: function (aPrefix) {
    return (item, string) =>
      VariablesView.simpleValueEvalMacro(item, string, aPrefix);
  },
  _getGetterOrSetterEvalMacro: function (aPrefix) {
    return (item, string) =>
      VariablesView.getterOrSetterEvalMacro(item, string, aPrefix);
  },
  _getOverrideValueEvalMacro: function (aPrefix) {
    return (item, string) =>
      VariablesView.overrideValueEvalMacro(item, string, aPrefix);
  },

  /**
   * The mousemove listener for the source editor.
   */
  _onMouseMove: function (e) {
    // Prevent the variable inspection popup from showing when the thread client
    // is not paused, or while a popup is already visible, or when the user tries
    // to select text in the editor.
    let isResumed = gThreadClient && gThreadClient.state != "paused";
    let isSelecting = this.DebuggerView.editor.somethingSelected() && e.buttons > 0;
    let isPopupVisible = !this._tooltip.isHidden();
    if (isResumed || isSelecting || isPopupVisible) {
      clearNamedTimeout("editor-mouse-move");
      return;
    }
    // Allow events to settle down first. If the mouse hovers over
    // a certain point in the editor long enough, try showing a variable bubble.
    setNamedTimeout("editor-mouse-move",
      this.TOOLTIP_SHOW_DELAY, () => this._findIdentifier(e.clientX, e.clientY));
  },

  /**
   * The mouseout listener for the source editor container node.
   */
  _onMouseOut: function () {
    clearNamedTimeout("editor-mouse-move");
  },

  /**
   * Listener handling the popup hiding event.
   */
  _onPopupHiding: function ({ target }) {
    if (this._tooltip.panel != target) {
      return;
    }
    if (this._markedText) {
      this._markedText.clear();
      this._markedText = null;
    }
    if (!this._tooltip.isEmpty()) {
      this._tooltip.empty();
    }
  },

  _editorContainer: null,
  _markedText: null,
  _tooltip: null
};

DebuggerView.VariableBubble = new VariableBubbleView(DebuggerController, DebuggerView);