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

const { Cu } = require("chrome");
const { Class } = require("sdk/core/heritage");
const { EventTarget } = require("sdk/event/target");
const { emit } = require("sdk/event/core");
const { EditorTypeForResource } = require("devtools/client/projecteditor/lib/editors");
const NetworkHelper = require("devtools/shared/webconsole/network-helper");
const promise = require("promise");

/**
 * The Shell is the object that manages the editor for a single resource.
 * It is in charge of selecting the proper Editor (text/image/plugin-defined)
 * and instantiating / appending the editor.
 * This object is not exported, it is just used internally by the ShellDeck.
 *
 * This object has a promise `editorAppended`, that will resolve once the editor
 * is ready to be used.
 */
var Shell = Class({
  extends: EventTarget,

  /**
   * @param ProjectEditor host
   * @param Resource resource
   */
  initialize: function(host, resource) {
    this.host = host;
    this.doc = host.document;
    this.resource = resource;
    this.elt = this.doc.createElement("vbox");
    this.elt.classList.add("view-project-detail");
    this.elt.shell = this;

    let constructor = this._editorTypeForResource();

    this.editor = constructor(this.host);
    this.editor.shell = this;
    this.editorAppended = this.editor.appended;

    this.editor.on("load", () => {
      this.editorDeferred.resolve();
    });
    this.elt.appendChild(this.editor.elt);
  },

  /**
   * Start loading the resource.  The 'load' event happens as
   * a result of this function, so any listeners to 'editorAppended'
   * need to be added before calling this.
   */
  load: function() {
    this.editorDeferred = promise.defer();
    this.editorLoaded = this.editorDeferred.promise;
    this.editor.load(this.resource);
  },

  /**
   * Destroy the shell and its associated editor
   */
  destroy: function() {
    this.editor.destroy();
    this.resource.destroy();
  },

  /**
   * Make sure the correct editor is selected for the resource.
   * @returns Type:Editor
   */
  _editorTypeForResource: function() {
    let resource = this.resource;
    let constructor = EditorTypeForResource(resource);

    if (this.host.plugins) {
      this.host.plugins.forEach(plugin => {
        if (plugin.editorForResource) {
          let pluginEditor = plugin.editorForResource(resource);
          if (pluginEditor) {
            constructor = pluginEditor;
          }
        }
      });
    }

    return constructor;
  }
});

/**
 * The ShellDeck is in charge of managing the list of active Shells for
 * the current ProjectEditor instance (aka host).
 *
 * This object emits the following events:
 *   - "editor-created": When an editor is initially created
 *   - "editor-activated": When an editor is ready to use
 *   - "editor-deactivated": When an editor is ready to use
 */
var ShellDeck = Class({
  extends: EventTarget,

  /**
   * @param ProjectEditor host
   * @param Document document
   */
  initialize: function(host, document) {
    this.doc = document;
    this.host = host;
    this.deck = this.doc.createElement("deck");
    this.deck.setAttribute("flex", "1");
    this.elt = this.deck;

    this.shells = new Map();

    this._activeShell = null;
  },

  /**
   * Open a resource in a Shell.  Will create the Shell
   * if it doesn't exist yet.
   *
   * @param Resource resource
   *                 The file to be opened
   * @returns Shell
   */
  open: function(defaultResource) {
    let shell = this.shellFor(defaultResource);
    if (!shell) {
      shell = this._createShell(defaultResource);
      this.shells.set(defaultResource, shell);
    }
    this.selectShell(shell);
    return shell;
  },

  /**
   * Create a new Shell for a resource.  Called by `open`.
   *
   * @returns Shell
   */
  _createShell: function(defaultResource) {
    let shell = Shell(this.host, defaultResource);

    shell.editorAppended.then(() => {
      this.shells.set(shell.resource, shell);
      emit(this, "editor-created", shell.editor);
      if (this.currentShell === shell) {
        this.selectShell(shell);
      }

    });

    shell.load();
    this.deck.appendChild(shell.elt);
    return shell;
  },

  /**
   * Remove the shell for a given resource.
   *
   * @param Resource resource
   */
  removeResource: function(resource) {
    let shell = this.shellFor(resource);
    if (shell) {
      this.shells.delete(resource);
      shell.destroy();
    }
  },

  destroy: function() {
    for (let [resource, shell] of this.shells.entries()) {
      this.shells.delete(resource);
      shell.destroy();
    }
  },

  /**
   * Select a given shell and open its editor.
   * Will fire editor-deactivated on the old selected Shell (if any),
   * and editor-activated on the new one once it is ready
   *
   * @param Shell shell
   */
  selectShell: function(shell) {
    // Don't fire another activate if this is already the active shell
    if (this._activeShell != shell) {
      if (this._activeShell) {
        emit(this, "editor-deactivated", this._activeShell.editor, this._activeShell.resource);
      }
      this.deck.selectedPanel = shell.elt;
      this._activeShell = shell;

      // Only reload the shell if the editor doesn't have local changes.
      if (shell.editor.isClean()) {
        shell.load();
      }
      shell.editorLoaded.then(() => {
        // Handle case where another shell has been requested before this
        // one is finished loading.
        if (this._activeShell === shell) {
          emit(this, "editor-activated", shell.editor, shell.resource);
        }
      });
    }
  },

  /**
   * Find a Shell for a Resource.
   *
   * @param Resource resource
   * @returns Shell
   */
  shellFor: function(resource) {
    return this.shells.get(resource);
  },

  /**
   * The currently active Shell.  Note: the editor may not yet be available
   * on the current shell.  Best to wait for the 'editor-activated' event
   * instead.
   *
   * @returns Shell
   */
  get currentShell() {
    return this._activeShell;
  },

  /**
   * The currently active Editor, or null if it is not ready.
   *
   * @returns Editor
   */
  get currentEditor() {
    let shell = this.currentShell;
    return shell ? shell.editor : null;
  },

});
exports.ShellDeck = ShellDeck;
