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

this.EXPORTED_SYMBOLS = ["StyleEditorDebuggee", "StyleSheet"];

const Cc = Components.classes;
const Ci = Components.interfaces;
const Cu = Components.utils;

Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource:///modules/devtools/shared/event-emitter.js");

XPCOMUtils.defineLazyModuleGetter(this, "Promise",
    "resource://gre/modules/commonjs/sdk/core/promise.js");

/**
 * A StyleEditorDebuggee represents the document the style editor is debugging.
 * It maintains a list of StyleSheet objects that represent the stylesheets in
 * the target's document. It wraps remote debugging protocol comunications.
 *
 * It emits these events:
 *   'document-load': debuggee's document is loaded, style sheets are argument
 *   'stylesheets-cleared': The debuggee's stylesheets have been reset (e.g. the
 *                          page navigated)
 *
 * @param {Target} target
 *         The target the debuggee is listening to
 */
let StyleEditorDebuggee = function(target) {
  EventEmitter.decorate(this);

  this.styleSheets = [];

  this.clear = this.clear.bind(this);
  this._onNewDocument = this._onNewDocument.bind(this);
  this._onDocumentLoad = this._onDocumentLoad.bind(this);

  this._target = target;
  this._actor = this.target.form.styleEditorActor;

  this.client.addListener("documentLoad", this._onDocumentLoad);
  this._target.on("navigate", this._onNewDocument);

  this._onNewDocument();
}

StyleEditorDebuggee.prototype = {
  /**
   * list of StyleSheet objects for this target
   */
  styleSheets: null,

  /**
   * baseURIObject for the current document
   */
  baseURI: null,

  /**
   * The target we're debugging
   */
  get target() {
    return this._target;
  },

  /**
   * Client for communicating with server with remote debug protocol.
   */
  get client() {
    return this._target.client;
  },

  /**
   * Get the StyleSheet object with the given href.
   *
   * @param  {string} href
   *         Url of the stylesheet to find
   * @return {StyleSheet}
   *         StyleSheet with the matching href
   */
  styleSheetFromHref: function(href) {
    for (let sheet of this.styleSheets) {
      if (sheet.href == href) {
        return sheet;
      }
    }
    return null;
  },

  /**
   * Clear stylesheets and state.
   */
  clear: function() {
    this.baseURI = null;
    this.clearStyleSheets();
  },

  /**
   * Clear stylesheets.
   */
  clearStyleSheets: function() {
    for (let stylesheet of this.styleSheets) {
      stylesheet.destroy();
    }
    this.styleSheets = [];
    this.emit("stylesheets-cleared");
  },

  /**
   * Called when target is created or has navigated.
   * Clear previous sheets and request new document's
   */
  _onNewDocument: function() {
    this.clear();

    this._getBaseURI();

    let message = { type: "newDocument" };
    this._sendRequest(message);
  },

  /**
   * request baseURIObject information from the document
   */
  _getBaseURI: function() {
    let message = { type: "getBaseURI" };
    this._sendRequest(message, (response) => {
      this.baseURI = response.baseURI;
    });
  },

  /**
   * Handler for document load, forward event with
   * all the stylesheets available on load.
   *
   * @param  {string} type
   *         Event type
   * @param  {object} request
   *         Object with 'styleSheets' array of actor forms
   */
  _onDocumentLoad: function(type, request) {
    if (this.styleSheets.length > 0) {
      this.clearStyleSheets();
    }
    let sheets = [];
    for (let form of request.styleSheets) {
      let sheet = this._addStyleSheet(form);
      sheets.push(sheet);
    }
    this.emit("document-load", sheets);
  },

  /**
   * Create a new StyleSheet object from the form
   * and add to our stylesheet list.
   *
   * @param {object} form
   *        Initial properties of the stylesheet
   */
  _addStyleSheet: function(form) {
    let sheet = new StyleSheet(form, this);
    this.styleSheets.push(sheet);
    return sheet;
  },

  /**
   * Create a new stylesheet with the given text
   * and attach it to the document.
   *
   * @param {string} text
   *        Initial text of the stylesheet
   * @param {function} callback
   *        Function to call when the stylesheet has been added to the document
   */
  createStyleSheet: function(text, callback) {
    let message = { type: "newStyleSheet", text: text };
    this._sendRequest(message, (response) => {
      let sheet = this._addStyleSheet(response.styleSheet);
      callback(sheet);
    });
  },

  /**
   * Send a request to our actor on the server
   *
   * @param {object} message
   *        Message to send to the actor
   * @param {function} callback
   *        Function to call with reponse from actor
   */
  _sendRequest: function(message, callback) {
    message.to = this._actor;
    this.client.request(message, callback);
  },

  /**
   * Clean up and remove listeners
   */
  destroy: function() {
    this.clear();

    this._target.off("navigate", this._onNewDocument);
  }
}

/**
 * A StyleSheet object represents a stylesheet on the debuggee. It wraps
 * communication with a complimentary StyleSheetActor on the server.
 *
 * It emits these events:
 *   'source-load' - The full text source of the stylesheet has been fetched
 *   'property-change' - Any property (e.g 'disabled') has changed
 *   'style-applied' - A change has been applied to the live stylesheet on the server
 *   'error' - An error occured when loading or saving stylesheet
 *
 * @param {object} form
 *        Initial properties of the stylesheet
 * @param {StyleEditorDebuggee} debuggee
 *        Owner of the stylesheet
 */
let StyleSheet = function(form, debuggee) {
  EventEmitter.decorate(this);

  this.debuggee = debuggee;
  this._client = debuggee.client;
  this._actor = form.actor;

  this._onSourceLoad = this._onSourceLoad.bind(this);
  this._onPropertyChange = this._onPropertyChange.bind(this);
  this._onError = this._onError.bind(this);
  this._onStyleApplied = this._onStyleApplied.bind(this);

  this._client.addListener("sourceLoad-" + this._actor, this._onSourceLoad);
  this._client.addListener("propertyChange-" + this._actor, this._onPropertyChange);
  this._client.addListener("error-" + this._actor, this._onError);
  this._client.addListener("styleApplied-" + this._actor, this._onStyleApplied);

  // set initial property values
  for (let attr in form) {
    this[attr] = form[attr];
  }
}

StyleSheet.prototype = {
  /**
   * Toggle the disabled attribute of the stylesheet
   */
  toggleDisabled: function() {
    let message = { type: "toggleDisabled" };
    this._sendRequest(message);
  },

  /**
   * Request that the source of the stylesheet be fetched.
   * 'source-load' event will be fired when it's been fetched.
   */
  fetchSource: function() {
    let message = { type: "fetchSource" };
    this._sendRequest(message);
  },

  /**
   * Update the stylesheet in place with the given full source.
   *
   * @param {string} sheetText
   *        Full text to update the stylesheet with
   */
  update: function(sheetText) {
    let message = { type: "update", text: sheetText, transition: true };
    this._sendRequest(message);
  },

  /**
   * Handle source load event from the client.
   *
   * @param {string} type
   *        Event type
   * @param {object} request
   *        Event details
   */
  _onSourceLoad: function(type, request) {
    this.emit("source-load", request.source);
  },

  /**
   * Handle a property change on the stylesheet
   *
   * @param {string} type
   *        Event type
   * @param {object} request
   *        Event details
   */
  _onPropertyChange: function(type, request) {
    this[request.property] = request.value;
    this.emit("property-change", request.property);
  },

  /**
   * Propogate errors from the server that relate to this stylesheet.
   *
   * @param {string} type
   *        Event type
   * @param {object} request
   *        Event details
   */
  _onError: function(type, request) {
    this.emit("error", request.errorMessage);
  },

  /**
   * Handle event when update has been successfully applied and propogate it.
   */
  _onStyleApplied: function() {
    this.emit("style-applied");
  },

  /**
   * Send a request to our actor on the server
   *
   * @param {object} message
   *        Message to send to the actor
   * @param {function} callback
   *        Function to call with reponse from actor
   */
  _sendRequest: function(message, callback) {
    message.to = this._actor;
    this._client.request(message, callback);
  },

  /**
   * Clean up and remove event listeners
   */
  destroy: function() {
    this._client.removeListener("sourceLoad-" + this._actor, this._onSourceLoad);
    this._client.removeListener("propertyChange-" + this._actor, this._onPropertyChange);
    this._client.removeListener("error-" + this._actor, this._onError);
    this._client.removeListener("styleApplied-" + this._actor, this._onStyleApplied);
  }
}
