/* -*- 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";

/**
 * Functions handling the stackframes UI.
 */
function StackFramesView(DebuggerController, DebuggerView) {
  dumpn("StackFramesView was instantiated");

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

  this._onStackframeRemoved = this._onStackframeRemoved.bind(this);
  this._onSelect = this._onSelect.bind(this);
  this._onScroll = this._onScroll.bind(this);
  this._afterScroll = this._afterScroll.bind(this);
  this._getStackAsString = this._getStackAsString.bind(this);
}

StackFramesView.prototype = Heritage.extend(WidgetMethods, {
  /**
   * Initialization function, called when the debugger is started.
   */
  initialize: function () {
    dumpn("Initializing the StackFramesView");

    this._popupset = document.getElementById("debuggerPopupset");

    this.widget = new BreadcrumbsWidget(document.getElementById("stackframes"));
    this.widget.addEventListener("select", this._onSelect, false);
    this.widget.addEventListener("scroll", this._onScroll, true);
    this.widget.setAttribute("context", "stackFramesContextMenu");
    window.addEventListener("resize", this._onScroll, true);

    this.autoFocusOnFirstItem = false;
    this.autoFocusOnSelection = false;

    // This view's contents are also mirrored in a different container.
    this._mirror = this.DebuggerView.StackFramesClassicList;
  },

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

    this.widget.removeEventListener("select", this._onSelect, false);
    this.widget.removeEventListener("scroll", this._onScroll, true);
    window.removeEventListener("resize", this._onScroll, true);
  },

  /**
   * Adds a frame in this stackframes container.
   *
   * @param string aTitle
   *        The frame title (function name).
   * @param string aUrl
   *        The frame source url.
   * @param string aLine
   *        The frame line number.
   * @param number aDepth
   *        The frame depth in the stack.
   * @param boolean aIsBlackBoxed
   *        Whether or not the frame is black boxed.
   */
  addFrame: function (aFrame, aLine, aColumn, aDepth, aIsBlackBoxed) {
    let { source } = aFrame;

    // The source may not exist in the source listing yet because it's
    // an unnamed eval source, which we hide, so we need to add it
    if (!DebuggerView.Sources.getItemByValue(source.actor)) {
      DebuggerView.Sources.addSource(source, { force: true });
    }

    let location = DebuggerView.Sources.getDisplayURL(source);
    let title = StackFrameUtils.getFrameTitle(aFrame);

    // Blackboxed stack frames are collapsed into a single entry in
    // the view. By convention, only the first frame is displayed.
    if (aIsBlackBoxed) {
      if (this._prevBlackBoxedUrl == location) {
        return;
      }
      this._prevBlackBoxedUrl = location;
    } else {
      this._prevBlackBoxedUrl = null;
    }

    // Create the element node for the stack frame item.
    let frameView = this._createFrameView(
      title, location, aLine, aDepth, aIsBlackBoxed
    );

    // Append a stack frame item to this container.
    this.push([frameView], {
      index: 0, /* specifies on which position should the item be appended */
      attachment: {
        title: title,
        url: location,
        line: aLine,
        depth: aDepth,
        column: aColumn
      },
      // Make sure that when the stack frame item is removed, the corresponding
      // mirrored item in the classic list is also removed.
      finalize: this._onStackframeRemoved
    });

    // Mirror this newly inserted item inside the "Call Stack" tab.
    this._mirror.addFrame(title, location, aLine, aDepth);
  },

  _getStackAsString: function () {
    return [...this].map(frameItem => {
      const { attachment: { title, url, line, column }} = frameItem;
      return title + "@" + url + ":" + line + ":" + column;
    }).join("\n");
  },

  addCopyContextMenu: function () {
    let menupopup = document.createElement("menupopup");
    let menuitem = document.createElement("menuitem");

    menupopup.id = "stackFramesContextMenu";
    menuitem.id = "copyStackMenuItem";

    menuitem.setAttribute("label", "Copy");
    menuitem.addEventListener("command", () => {
      let stack = this._getStackAsString();
      clipboardHelper.copyString(stack);
    }, false);
    menupopup.appendChild(menuitem);
    this._popupset.appendChild(menupopup);
  },

  /**
   * Selects the frame at the specified depth in this container.
   * @param number aDepth
   */
  set selectedDepth(aDepth) {
    this.selectedItem = aItem => aItem.attachment.depth == aDepth;
  },

  /**
   * Gets the currently selected stack frame's depth in this container.
   * This will essentially be the opposite of |selectedIndex|, which deals
   * with the position in the view, where the last item added is actually
   * the bottommost, not topmost.
   * @return number
   */
  get selectedDepth() {
    return this.selectedItem.attachment.depth;
  },

  /**
   * Specifies if the active thread has more frames that need to be loaded.
   */
  dirty: false,

  /**
   * Customization function for creating an item's UI.
   *
   * @param string aTitle
   *        The frame title to be displayed in the list.
   * @param string aUrl
   *        The frame source url.
   * @param string aLine
   *        The frame line number.
   * @param number aDepth
   *        The frame depth in the stack.
   * @param boolean aIsBlackBoxed
   *        Whether or not the frame is black boxed.
   * @return nsIDOMNode
   *         The stack frame view.
   */
  _createFrameView: function (aTitle, aUrl, aLine, aDepth, aIsBlackBoxed) {
    let container = document.createElement("hbox");
    container.id = "stackframe-" + aDepth;
    container.className = "dbg-stackframe";

    let frameDetails = SourceUtils.trimUrlLength(
      SourceUtils.getSourceLabel(aUrl),
      STACK_FRAMES_SOURCE_URL_MAX_LENGTH,
      STACK_FRAMES_SOURCE_URL_TRIM_SECTION);

    if (aIsBlackBoxed) {
      container.classList.add("dbg-stackframe-black-boxed");
    } else {
      let frameTitleNode = document.createElement("label");
      frameTitleNode.className = "plain dbg-stackframe-title breadcrumbs-widget-item-tag";
      frameTitleNode.setAttribute("value", aTitle);
      container.appendChild(frameTitleNode);

      frameDetails += SEARCH_LINE_FLAG + aLine;
    }

    let frameDetailsNode = document.createElement("label");
    frameDetailsNode.className = "plain dbg-stackframe-details breadcrumbs-widget-item-id";
    frameDetailsNode.setAttribute("value", frameDetails);
    container.appendChild(frameDetailsNode);

    return container;
  },

  /**
   * Function called each time a stack frame item is removed.
   *
   * @param object aItem
   *        The corresponding item.
   */
  _onStackframeRemoved: function (aItem) {
    dumpn("Finalizing stackframe item: " + aItem.stringify());

    // Remove the mirrored item in the classic list.
    let depth = aItem.attachment.depth;
    this._mirror.remove(this._mirror.getItemForAttachment(e => e.depth == depth));

    // Forget the previously blackboxed stack frame url.
    this._prevBlackBoxedUrl = null;
  },

  /**
   * The select listener for the stackframes container.
   */
  _onSelect: function (e) {
    let stackframeItem = this.selectedItem;
    if (stackframeItem) {
      // The container is not empty and an actual item was selected.
      let depth = stackframeItem.attachment.depth;

      // Mirror the selected item in the classic list.
      this.suppressSelectionEvents = true;
      this._mirror.selectedItem = e => e.attachment.depth == depth;
      this.suppressSelectionEvents = false;

      DebuggerController.StackFrames.selectFrame(depth);
    }
  },

  /**
   * The scroll listener for the stackframes container.
   */
  _onScroll: function () {
    // Update the stackframes container only if we have to.
    if (!this.dirty) {
      return;
    }
    // Allow requests to settle down first.
    setNamedTimeout("stack-scroll", STACK_FRAMES_SCROLL_DELAY, this._afterScroll);
  },

  /**
   * Requests the addition of more frames from the controller.
   */
  _afterScroll: function () {
    let scrollPosition = this.widget.getAttribute("scrollPosition");
    let scrollWidth = this.widget.getAttribute("scrollWidth");

    // If the stackframes container scrolled almost to the end, with only
    // 1/10 of a breadcrumb remaining, load more content.
    if (scrollPosition - scrollWidth / 10 < 1) {
      this.ensureIndexIsVisible(CALL_STACK_PAGE_SIZE - 1);
      this.dirty = false;

      // Loads more stack frames from the debugger server cache.
      DebuggerController.StackFrames.addMoreFrames();
    }
  },

  _mirror: null,
  _prevBlackBoxedUrl: null
});

DebuggerView.StackFrames = new StackFramesView(DebuggerController, DebuggerView);
