/* 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/. */

"use strict";

const Editor = require("resource://devtools/client/shared/sourceeditor/editor.js");
const EventEmitter = require("resource://devtools/shared/event-emitter.js");

/**
 * A wrapper around the Editor component, that allows editing of HTML.
 *
 * The main functionality this provides around the Editor is the ability
 * to show/hide/position an editor inplace. It only appends once to the
 * body, and uses CSS to position the editor.  The reason it is done this
 * way is that the editor is loaded in an iframe, and calling appendChild
 * causes it to reload.
 *
 * Meant to be embedded inside of an HTML page, as in markup.html.
 */
class HTMLEditor extends EventEmitter {
  /**
   * @param  {HTMLDocument} htmlDocument
   *         The document to attach the editor to.  Will also use this
   *         document as a basis for listening resize events.
   */
  constructor(htmlDocument) {
    super();

    this.doc = htmlDocument;
    this.#container = this.doc.createElement("div");
    this.#container.className = "html-editor theme-body";
    this.#container.style.display = "none";
    this.#editorInner = this.doc.createElement("div");
    this.#editorInner.className = "html-editor-inner devtools-input";
    this.#container.appendChild(this.#editorInner);
    this.doc.body.appendChild(this.#container);
    this.doc.defaultView.addEventListener("resize", this.refresh, true);

    const config = {
      mode: Editor.modes.html,
      lineWrapping: true,
      styleActiveLine: false,
      keyMap: [
        { key: HTMLEditor.#ctrl("Enter"), run: this.hide },
        { key: "F2", run: this.hide },
        {
          key: "Escape",
          run: this.hide.bind(this, false),
          preventDefault: true,
        },
      ],
      theme: "mozilla markup-view",
      cm6: true,
    };

    this.#container.addEventListener("click", this.hide);
    this.#editorInner.addEventListener("click", HTMLEditor.#stopPropagation);
    // Avoid the hijack of the backspace key by the markup when the
    // html editor is open.
    this.#editorInner.addEventListener("keydown", HTMLEditor.#stopPropagation);

    this.editor = new Editor(config);
    this.editor.appendToLocalElement(this.#editorInner);

    this.hide(false);
  }

  editor = null;
  doc = null;

  #container = null;
  #editorInner = null;
  #attachedElement = null;
  #originalValue;

  static #ctrl(k) {
    return (Services.appinfo.OS == "Darwin" ? "Cmd-" : "Ctrl-") + k;
  }

  static #stopPropagation(e) {
    e.stopPropagation();
  }

  /**
   * Need to refresh position by manually setting CSS values, so this will
   * need to be called on resizes and other sizing changes.
   */
  refresh = () => {
    const element = this.#attachedElement;

    if (element) {
      this.#container.style.top = element.offsetTop + "px";
      this.#container.style.left = element.offsetLeft + "px";
      this.#container.style.width = element.offsetWidth + "px";
      this.#container.style.height = element.parentNode.offsetHeight + "px";
    }
  };

  /**
   * Anchor the editor to a particular element.
   *
   * @param  {DOMNode} element
   *         The element that the editor will be anchored to.
   *         Should belong to the HTMLDocument passed into the constructor.
   */
  #attach(element) {
    this.#detach();
    this.#attachedElement = element;
    element.classList.add("html-editor-container");
    this.refresh();
  }

  /**
   * Unanchor the editor from an element.
   */
  #detach() {
    if (this.#attachedElement) {
      this.#attachedElement.classList.remove("html-editor-container");
      this.#attachedElement = undefined;
    }
  }

  /**
   * Anchor the editor to a particular element, and show the editor.
   *
   * @param  {DOMNode} element
   *         The element that the editor will be anchored to.
   *         Should belong to the HTMLDocument passed into the constructor.
   * @param  {string} text
   *         Value to set the contents of the editor to
   * @param  {Function} cb
   *         The function to call when hiding
   */
  show = (element, text) => {
    if (this.isVisible) {
      return;
    }

    this.#originalValue = text;
    this.editor.setText(text, { saveTransactionToHistory: false });
    this.#attach(element);
    this.#container.style.display = "flex";
    this.isVisible = true;

    this.editor.focus();

    this.emit("popupshown");
  };

  /**
   * Hide the editor, optionally committing the changes
   *
   * @param  {boolean} shouldCommit
   *         A change will be committed by default.  If this param
   *         strictly equals false, no change will occur.
   */
  hide = shouldCommit => {
    if (!this.isVisible) {
      return;
    }

    this.#container.style.display = "none";
    this.#detach();

    const newValue = this.editor.getText();
    const valueHasChanged = this.#originalValue !== newValue;
    const preventCommit = shouldCommit === false || !valueHasChanged;
    this.#originalValue = undefined;
    this.isVisible = undefined;
    this.emit("popuphidden", !preventCommit, newValue);
  };

  /**
   * Destroy this object and unbind all event handlers
   */
  destroy() {
    this.doc.defaultView.removeEventListener("resize", this.refresh, true);
    this.#container.removeEventListener("click", this.hide);
    this.#editorInner.removeEventListener("click", HTMLEditor.#stopPropagation);
    this.#editorInner.removeEventListener(
      "keydown",
      HTMLEditor.#stopPropagation
    );

    this.hide(false);
    this.#container.remove();
    this.editor.destroy();
  }
}

module.exports = HTMLEditor;
