/* 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 {
  FrontClassWithSpec,
  registerFront,
} = require("resource://devtools/shared/protocol.js");
const {
  styleRuleSpec,
} = require("resource://devtools/shared/specs/style-rule.js");

loader.lazyRequireGetter(
  this,
  "RuleRewriter",
  "resource://devtools/client/fronts/inspector/rule-rewriter.js"
);

/**
 * StyleRuleFront, the front for the StyleRule actor.
 */
class StyleRuleFront extends FrontClassWithSpec(styleRuleSpec) {
  constructor(client, targetFront, parentFront) {
    super(client, targetFront, parentFront);

    this.before("location-changed", this._locationChangedPre.bind(this));
  }

  form(form) {
    this.actorID = form.actor;
    this._form = form;
    this.traits = form.traits || {};
  }

  /**
   * Ensure _form is updated when location-changed is emitted.
   */
  _locationChangedPre(line, column) {
    this._form.line = line;
    this._form.column = column;
  }

  /**
   * Return a new RuleModificationList or RuleRewriter for this node.
   * A RuleRewriter will be returned when the rule's canSetRuleText
   * trait is true; otherwise a RuleModificationList will be
   * returned.
   *
   * @param {Window} win
   *                 This is needed by the RuleRewriter.
   * @param {CssPropertiesFront} cssProperties
   *                             This is needed by the RuleRewriter.
   * @return {RuleModificationList}
   */
  startModifyingProperties(win, cssProperties) {
    if (this.canSetRuleText) {
      return new RuleRewriter(
        win,
        cssProperties.isKnown,
        this,
        this.authoredText
      );
    }
    return new RuleModificationList(this);
  }

  get type() {
    return this._form.type;
  }
  get className() {
    return this._form.className;
  }
  get line() {
    return this._form.line || -1;
  }
  get column() {
    return this._form.column || -1;
  }
  get cssText() {
    return this._form.cssText;
  }
  get isNestedDeclarations() {
    return !!this._form.isNestedDeclarations;
  }
  get authoredText() {
    return typeof this._form.authoredText === "string"
      ? this._form.authoredText
      : this._form.cssText;
  }
  get declarations() {
    return this._form.declarations || [];
  }
  get keyText() {
    return this._form.keyText;
  }
  get name() {
    return this._form.name;
  }
  get selectors() {
    return this._form.selectors;
  }
  get selectorsSpecificity() {
    return this._form.selectorsSpecificity;
  }

  /**
   * Returns a concatenation of the rule's selector and all its ancestor "selectors".
   * This is different from a "desugared" selector as what's returned is not an
   * actual selector, but some kind of key that represent the rule selectors.
   * This is used for the selector highlighter, where we need to know what's
   * being highlighted.
   *
   * @returns {string}
   */
  get computedSelector() {
    let selector = "";
    for (const ancestor of this.ancestorData) {
      let ancestorSelector;
      if (ancestor.selectors) {
        ancestorSelector = ancestor.selectors.join(",");
      } else if (ancestor.type === "container") {
        ancestorSelector =
          ancestor.containerName + " " + ancestor.containerQuery;
      } else if (ancestor.type === "supports") {
        ancestorSelector = ancestor.conditionText;
      } else if (ancestor.value) {
        ancestorSelector = ancestor.value;
      }
      selector +=
        "/" + (ancestor.type ? ancestor.type + " " : "") + ancestorSelector;
    }

    return (selector ? selector + "/" : "") + this._form.selectors.join(",");
  }

  get selectorWarnings() {
    return this._form.selectorWarnings;
  }

  get parentStyleSheet() {
    const resourceCommand = this.targetFront.commands.resourceCommand;
    return resourceCommand.getResourceById(
      resourceCommand.TYPES.STYLESHEET,
      this._form.parentStyleSheet
    );
  }

  get element() {
    return this.conn.getFrontByID(this._form.element);
  }

  get href() {
    if (this._form.href) {
      return this._form.href;
    }
    const sheet = this.parentStyleSheet;
    return sheet ? sheet.href : "";
  }

  get nodeHref() {
    const sheet = this.parentStyleSheet;
    return sheet ? sheet.nodeHref : "";
  }

  get canSetRuleText() {
    return this._form.traits && this._form.traits.canSetRuleText;
  }

  get location() {
    return {
      source: this.parentStyleSheet,
      href: this.href,
      line: this.line,
      column: this.column,
    };
  }

  get ancestorData() {
    return this._form.ancestorData;
  }

  get userAdded() {
    return this._form.userAdded;
  }

  async modifySelector(node, value) {
    const response = await super.modifySelector(
      node,
      value,
      this.canSetRuleText
    );

    if (response.ruleProps) {
      response.ruleProps = response.ruleProps.entries[0];
    }
    return response;
  }

  setRuleText(newText, modifications) {
    this._form.authoredText = newText;
    return super.setRuleText(newText, modifications);
  }
}

registerFront(StyleRuleFront);

/**
 * Convenience API for building a list of attribute modifications
 * for the `modifyProperties` request.  A RuleModificationList holds a
 * list of modifications that will be applied to a StyleRuleActor.
 * The modifications are processed in the order in which they are
 * added to the RuleModificationList.
 *
 * Objects of this type expose the same API as @see RuleRewriter.
 * This lets the inspector use (mostly) the same code, regardless of
 * whether the server implements setRuleText.
 */
class RuleModificationList {
  /**
   * Initialize a RuleModificationList.
   *
   * @param {StyleRuleFront} rule the associated rule
   */
  constructor(rule) {
    this.rule = rule;
    this.modifications = [];
  }

  /**
   * Apply the modifications in this object to the associated rule.
   *
   * @return {Promise} A promise which will be resolved when the modifications
   *         are complete; @see StyleRuleActor.modifyProperties.
   */
  apply() {
    return this.rule.modifyProperties(this.modifications);
  }

  /**
   * Add a "set" entry to the modification list.
   *
   * @param {number} index index of the property in the rule.
   *                       This can be -1 in the case where
   *                       the rule does not support setRuleText;
   *                       generally for setting properties
   *                       on an element's style.
   * @param {string} name the property's name
   * @param {string} value the property's value
   * @param {string} priority the property's priority, either the empty
   *                          string or "important"
   */
  setProperty(index, name, value, priority) {
    this.modifications.push({ type: "set", index, name, value, priority });
  }

  /**
   * Add a "remove" entry to the modification list.
   *
   * @param {number} index index of the property in the rule.
   *                       This can be -1 in the case where
   *                       the rule does not support setRuleText;
   *                       generally for setting properties
   *                       on an element's style.
   * @param {string} name the name of the property to remove
   */
  removeProperty(index, name) {
    this.modifications.push({ type: "remove", index, name });
  }

  /**
   * Rename a property.  This implementation acts like
   * |removeProperty|, because |setRuleText| is not available.
   *
   * @param {number} index index of the property in the rule.
   *                       This can be -1 in the case where
   *                       the rule does not support setRuleText;
   *                       generally for setting properties
   *                       on an element's style.
   * @param {string} name current name of the property
   *
   * This parameter is also passed, but as it is not used in this
   * implementation, it is omitted.  It is documented here as this
   * code also defined the interface implemented by @see RuleRewriter.
   * @param {string} newName new name of the property
   */
  renameProperty(index, name) {
    this.removeProperty(index, name);
  }

  /**
   * Enable or disable a property.  This implementation acts like
   * a no-op when enabling, because |setRuleText| is not available.
   *
   * @param {number} index index of the property in the rule.
   *                       This can be -1 in the case where
   *                       the rule does not support setRuleText;
   *                       generally for setting properties
   *                       on an element's style.
   * @param {string} name current name of the property
   * @param {boolean} isEnabled true if the property should be enabled;
   *                        false if it should be disabled
   */
  setPropertyEnabled(index, name, isEnabled) {
    if (!isEnabled) {
      this.modifications.push({ type: "disable", index, name });
    }
  }

  /**
   * Create a new property.  This implementation does nothing, because
   * |setRuleText| is not available.
   *
   * These parameters are passed, but as they are not used in this
   * implementation, they are omitted.  They are documented here as
   * this code also defined the interface implemented by @see
   * RuleRewriter.
   *
   * @param {number} index index of the property in the rule.
   *                       This can be -1 in the case where
   *                       the rule does not support setRuleText;
   *                       generally for setting properties
   *                       on an element's style.
   * @param {string} name name of the new property
   * @param {string} value value of the new property
   * @param {string} priority priority of the new property; either
   *                          the empty string or "important"
   * @param {boolean} enabled True if the new property should be
   *                          enabled, false if disabled
   */
  createProperty() {
    // Nothing.
  }
}
