/* 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 {
  style: { ELEMENT_STYLE, PRES_HINTS },
} = require("resource://devtools/shared/constants.js");
const CssLogic = require("resource://devtools/shared/inspector/css-logic.js");
const TextProperty = require("resource://devtools/client/inspector/rules/models/text-property.js");

loader.lazyRequireGetter(
  this,
  "promiseWarn",
  "resource://devtools/client/inspector/shared/utils.js",
  true
);
loader.lazyRequireGetter(
  this,
  "parseNamedDeclarations",
  "resource://devtools/shared/css/parsing-utils.js",
  true
);

const STYLE_INSPECTOR_PROPERTIES =
  "devtools/shared/locales/styleinspector.properties";
const { LocalizationHelper } = require("resource://devtools/shared/l10n.js");
const STYLE_INSPECTOR_L10N = new LocalizationHelper(STYLE_INSPECTOR_PROPERTIES);

/**
 * Rule is responsible for the following:
 *   Manages a single style declaration or rule.
 *   Applies changes to the properties in a rule.
 *   Maintains a list of TextProperty objects.
 */

/**
 * @typedef AppliedStyle
 *    This described a rule currently applying to a given DOM Element.
 *    This object comes from the backend and is defined by the "appliedstyle"
 *    protocol.js data type (Keep in sync with style-types.js spec file).
 * @property {StyleRuleFront} rule
 *     The main front to get data about the rule to describe.
 * @property {string} pseudoElement
 *     If this rule is about a pseudo element, its name (e.g. `::before`).
 * @property {boolean} isSystem
 *     Is this a user agent style?
 * @property {NodeFront} inherited
 *     An NodeFront for the element this rule was inherited from.
 *     If omitted, the rule applies directly to the current element.
 * @property {boolean} darkColorScheme
 *     True if dark color scheme is enabled.
 * @property {Array<number>} matchedSelectorIndexes
 *     To report which ones of the many selectors the rule may have
 *     that matches the selected element.
 * @property {StyleRuleFront} keyframes
 *     If this rule relate to a @keyframes rule, the parent keyframes rule.
 *
 * Note that the following attribute isn't related to "appliedstyle"
 * protocol.js type. This is a pure frontend attribute, only used when
 * creating a Rule from `ElementStyle.modifySelector()`)
 *
 * @property {boolean} isUnmatched
 *        True if the rule does not match the current selected
 *        element, otherwise, false.
 */
class Rule {
  /**
   * @param {ElementStyle} elementStyle
   *        The ElementStyle to which this rule belongs.
   *
   * @param {AppliedStyle} appliedStyle
   *        The information used to construct this rule.
   */
  constructor(elementStyle, appliedStyle) {
    this.elementStyle = elementStyle;
    this.domRule = appliedStyle.rule;
    this.compatibilityIssues = null;

    this.matchedSelectorIndexes = appliedStyle.matchedSelectorIndexes || [];
    this.isSystem = appliedStyle.isSystem;
    this.isUnmatched = appliedStyle.isUnmatched || false;
    this.darkColorScheme = appliedStyle.darkColorScheme;
    this.siblingCount = appliedStyle.siblingCount;
    this.siblingIndex = appliedStyle.siblingIndex;

    this.inherited = appliedStyle.inherited || null;
    this.pseudoElement = appliedStyle.pseudoElement || "";
    this.keyframes = appliedStyle.keyframes || null;

    this.cssProperties = this.elementStyle.ruleView.cssProperties;
    this.inspector = this.elementStyle.ruleView.inspector;
    this.store = this.elementStyle.ruleView.store;

    // Populate the text properties with the style's current authoredText
    // value, and add in any disabled properties from the store.
    this.textProps = this.#getTextProperties();
    this.textProps = this.textProps.concat(this.#getDisabledProperties());

    this.domRule.on("rule-updated", this.#onStyleRuleFrontUpdated);
  }

  destroy() {
    this.domRule.off("rule-updated", this.#onStyleRuleFrontUpdated);
    this.compatibilityIssues = null;
    this.destroyed = true;
  }

  get title() {
    let title = CssLogic.shortSource(this.sheet);
    if (this.domRule.type !== ELEMENT_STYLE && this.ruleLine > 0) {
      title += ":" + this.ruleLine;
    }

    return title;
  }

  #inheritedSectionLabel;
  get inheritedSectionLabel() {
    if (this.#inheritedSectionLabel) {
      return this.#inheritedSectionLabel;
    }
    this.#inheritedSectionLabel = "";
    if (this.inherited) {
      let eltText = this.inherited.displayName;
      if (this.inherited.id) {
        eltText += "#" + this.inherited.id;
      }
      let pseudo = this.pseudoElement;
      // In some cases, the pseudo element is a function (e.g. `::picker(select)`),
      // but in ELEMENT_BACKED_PSEUDO_ELEMENTS, we store the "short" version (e.g. `::picker`).
      // So drop anything after the opening parenthesis to check if the pseudo element
      // is a backed pseudo element.
      if (pseudo.includes("(")) {
        pseudo = pseudo.substring(0, pseudo.indexOf("("));
      }
      if (CssLogic.ELEMENT_BACKED_PSEUDO_ELEMENTS.has(pseudo)) {
        eltText += this.pseudoElement;
      }
      this.#inheritedSectionLabel = STYLE_INSPECTOR_L10N.getFormatStr(
        "rule.inheritedFrom",
        eltText
      );
    }
    return this.#inheritedSectionLabel;
  }

  #keyframesName;
  get keyframesName() {
    if (this.#keyframesName) {
      return this.#keyframesName;
    }
    this.#keyframesName = "";
    if (this.keyframes) {
      this.#keyframesName = STYLE_INSPECTOR_L10N.getFormatStr(
        "rule.keyframe",
        this.keyframes.name
      );
    }
    return this.#keyframesName;
  }

  get keyframesRule() {
    if (!this.keyframes) {
      return null;
    }

    return {
      id: this.keyframes.actorID,
      keyframesName: this.keyframesName,
    };
  }

  get selectorText() {
    if (Array.isArray(this.domRule.selectors)) {
      return this.domRule.selectors.join(", ");
    }

    if (this.domRule.type === PRES_HINTS) {
      return CssLogic.l10n("rule.sourceElementAttributesStyle");
    }

    return CssLogic.l10n("rule.sourceElement");
  }

  get selectorsSpecificity() {
    return this.domRule.selectorsSpecificity;
  }

  /**
   * The rule's stylesheet.
   */
  get sheet() {
    return this.domRule ? this.domRule.parentStyleSheet : null;
  }

  /**
   * The rule's line within a stylesheet
   */
  get ruleLine() {
    return this.domRule ? this.domRule.line : -1;
  }

  /**
   * The rule's column within a stylesheet
   */
  get ruleColumn() {
    return this.domRule ? this.domRule.column : null;
  }

  /**
   * Get the declaration block issues from the compatibility actor
   *
   * @returns A promise that resolves with an array of objects in following form:
   *    {
   *      // Type of compatibility issue
   *      type: <string>,
   *      // The CSS declaration that has compatibility issues
   *      property: <string>,
   *      // Alias to the given CSS property
   *      alias: <Array>,
   *      // Link to MDN documentation for the particular CSS rule
   *      url: <string>,
   *      deprecated: <boolean>,
   *      experimental: <boolean>,
   *      // An array of all the browsers that don't support the given CSS rule
   *      unsupportedBrowsers: <Array>,
   *    }
   */
  async getCompatibilityIssues() {
    if (!this.compatibilityIssues) {
      this.compatibilityIssues =
        this.inspector.commands.inspectorCommand.getCSSDeclarationBlockIssues(
          this.domRule.declarations
        );
    }

    return this.compatibilityIssues;
  }

  /**
   * Returns the TextProperty with the given id or undefined if it cannot be found.
   *
   * @param {string | null} id
   *        A TextProperty id.
   * @return {TextProperty|undefined} with the given id in the current Rule or undefined
   * if it cannot be found.
   */
  getDeclaration(id) {
    return id ? this.textProps.find(textProp => textProp.id === id) : undefined;
  }

  /**
   * Returns true if the rule matches the same rule front.
   * specified.
   *
   * @param {object} appliedStyle
   *        Applied style object. See the Rule constructor for documentation.
   */
  matches(appliedStyle) {
    return this.domRule === appliedStyle.rule;
  }

  /**
   * Create a new TextProperty to include in the rule.
   *
   * @param {string} name
   *        The text property name (such as "background" or "border-top").
   * @param {string} value
   *        The property's value (not including priority).
   * @param {string} priority
   *        The property's priority (either "important" or an empty string).
   * @param {boolean} enabled
   *        True if the property should be enabled.
   * @param {TextProperty} siblingProp
   *        Optional, property next to which the new property will be added.
   */
  createProperty(name, value, priority, enabled, siblingProp) {
    const prop = new TextProperty({
      rule: this,
      name,
      value,
      priority,
      enabled,
    });

    let ind;
    if (siblingProp) {
      ind = this.textProps.indexOf(siblingProp) + 1;
      this.textProps.splice(ind, 0, prop);
    } else {
      ind = this.textProps.length;
      this.textProps.push(prop);
    }

    this.applyProperties(modifications => {
      modifications.createProperty(ind, name, value, priority, enabled);

      this.store.userProperties.setProperty(this.domRule, name, value);

      // Now that the rule has been updated, the server might have given us data
      // that changes the state of the property. Update it now.
      prop.updateEditor();
    });

    return prop;
  }

  /**
   * Helper function for applyProperties that is called when the actor
   * does not support as-authored styles.  Store disabled properties
   * in the element style's store.
   */
  async #applyPropertiesNoAuthored(modifications) {
    this.elementStyle.onRuleUpdated();

    const disabledProps = [];

    for (const prop of this.textProps) {
      if (prop.invisible) {
        continue;
      }
      if (!prop.enabled) {
        disabledProps.push({
          name: prop.name,
          value: prop.value,
          priority: prop.priority,
        });
        continue;
      }
      if (prop.value.trim() === "") {
        continue;
      }

      modifications.setProperty(-1, prop.name, prop.value, prop.priority);

      prop.updateComputed();
    }

    // Store disabled properties in the disabled store.
    const disabled = this.elementStyle.store.disabled;
    if (disabledProps.length) {
      disabled.set(this.domRule, disabledProps);
    } else {
      disabled.delete(this.domRule);
    }

    await modifications.apply();

    const cssProps = {};
    // Note that even though StyleRuleActors normally provide parsed
    // declarations already, #applyPropertiesNoAuthored is only used when
    // connected to older backend that do not provide them. So parse here.
    for (const cssProp of parseNamedDeclarations(
      this.cssProperties.isKnown,
      this.domRule.authoredText
    )) {
      cssProps[cssProp.name] = cssProp;
    }

    for (const textProp of this.textProps) {
      if (!textProp.enabled) {
        continue;
      }
      let cssProp = cssProps[textProp.name];

      if (!cssProp) {
        cssProp = {
          name: textProp.name,
          value: "",
          priority: "",
        };
      }

      textProp.priority = cssProp.priority;
    }
  }

  /**
   * A helper for applyProperties that applies properties in the "as
   * authored" case; that is, when the StyleRuleActor supports
   * setRuleText.
   */
  async #applyPropertiesAuthored(modifications) {
    await modifications.apply();

    // The rewriting may have required some other property values to
    // change, e.g., to insert some needed terminators.  Update the
    // relevant properties here.
    for (const index in modifications.changedDeclarations) {
      const newValue = modifications.changedDeclarations[index];
      this.textProps[index].updateValue(newValue);
    }
    // Recompute and redisplay the computed properties.
    for (const prop of this.textProps) {
      if (!prop.invisible && prop.enabled) {
        prop.updateComputed();
        prop.updateEditor();
      }
    }

    this.elementStyle.onRuleUpdated();
  }

  /**
   * Reapply all the properties in this rule, and update their
   * computed styles.  Will re-mark overridden properties.  Sets the
   * |applyingModifications| property to a promise which will resolve
   * when the edit has completed.
   *
   * @param {Function} modifier a function that takes a RuleModificationList
   *        (or RuleRewriter) as an argument and that modifies it
   *        to apply the desired edit
   * @return {Promise} a promise which will resolve when the edit
   *        is complete
   */
  applyProperties(modifier) {
    // If there is already a pending modification, we have to wait
    // until it settles before applying the next modification.
    const resultPromise = Promise.resolve(this.applyingModifications)
      .then(() => {
        const modifications = this.domRule.startModifyingProperties(
          this.inspector.panelWin,
          this.cssProperties
        );
        modifier(modifications);
        if (this.domRule.canSetRuleText) {
          return this.#applyPropertiesAuthored(modifications);
        }
        return this.#applyPropertiesNoAuthored(modifications);
      })
      .then(() => {
        if (resultPromise === this.applyingModifications) {
          this.applyingModifications = null;
          this.elementStyle.notifyChanged();
        }
      })
      .catch(promiseWarn);

    // Expose as a public field as this is queried from CssRuleView class,
    // as well as tests
    this.applyingModifications = resultPromise;
    return resultPromise;
  }

  /**
   * Renames a property.
   *
   * @param {TextProperty} property
   *        The property to rename.
   * @param {string} name
   *        The new property name (such as "background" or "border-top").
   * @return {Promise}
   */
  setPropertyName(property, name) {
    if (name === property.name) {
      return Promise.resolve();
    }

    const oldName = property.name;
    property.name = name;
    const index = this.textProps.indexOf(property);
    return this.applyProperties(modifications => {
      modifications.renameProperty(index, oldName, name);
    });
  }

  /**
   * Sets the value and priority of a property, then reapply all properties.
   *
   * @param {TextProperty} property
   *        The property to manipulate.
   * @param {string} value
   *        The property's value (not including priority).
   * @param {string} priority
   *        The property's priority (either "important" or an empty string).
   * @return {Promise}
   */
  setPropertyValue(property, value, priority) {
    if (value === property.value && priority === property.priority) {
      return Promise.resolve();
    }

    property.value = value;
    property.priority = priority;

    this.elementStyle.ruleView.emitForTests("start-set-property-value");

    const index = this.textProps.indexOf(property);
    return this.applyProperties(modifications => {
      modifications.setProperty(index, property.name, value, priority);
    });
  }

  /**
   * Disables or enables given TextProperty.
   *
   * @param {TextProperty} property
   *        The property to enable/disable
   * @param {boolean} value
   */
  setPropertyEnabled(property, value) {
    if (property.enabled === !!value) {
      return;
    }
    property.enabled = !!value;
    const index = this.textProps.indexOf(property);
    this.applyProperties(modifications => {
      modifications.setPropertyEnabled(index, property.name, property.enabled);
    });
  }

  /**
   * Remove a given TextProperty from the rule and update the rule
   * accordingly.
   *
   * @param {TextProperty} property
   *        The property to be removed
   */
  removeProperty(property) {
    const index = this.textProps.indexOf(property);
    this.textProps.splice(index, 1);
    // Need to re-apply properties in case removing this TextProperty
    // exposes another one.
    this.applyProperties(modifications => {
      modifications.removeProperty(index, property.name);
    });
  }

  /**
   * Event handler for "rule-updated" event fired by StyleRuleActor.
   *
   * @param {StyleRuleFront} front
   */
  #onStyleRuleFrontUpdated = front => {
    // Overwritting this reference is not required, but it's here to avoid confusion.
    // Whenever an actor is passed over the protocol, either as a return value or as
    // payload on an event, the `form` of its corresponding front will be automatically
    // updated. No action required.
    // Even if this `domRule` reference here is not explicitly updated, lookups of
    // `this.domRule.declarations` will point to the latest state of declarations set
    // on the actor. Everything on `StyleRuleForm.form` will point to the latest state.
    this.domRule = front;
  };

  /**
   * Get the list of TextProperties from the style. Needs
   * to parse the style's authoredText.
   */
  #getTextProperties() {
    const textProps = [];
    const store = this.elementStyle.store;

    for (const prop of this.domRule.declarations) {
      const name = prop.name;
      // In an inherited rule, we only show inherited properties.
      // However, we must keep all properties in order for rule
      // rewriting to work properly.  So, compute the "invisible"
      // property here.
      const inherits = prop.isCustomProperty
        ? prop.inherits
        : this.cssProperties.isInherited(name);
      const invisible = this.inherited && !inherits;

      const value = store.userProperties.getProperty(
        this.domRule,
        name,
        prop.value
      );

      const textProp = new TextProperty({
        rule: this,
        name,
        value,
        priority: prop.priority,
        enabled: !("commentOffsets" in prop),
        invisible,
      });
      textProps.push(textProp);
    }

    return textProps;
  }

  /**
   * Return the list of disabled properties from the store for this rule.
   */
  #getDisabledProperties() {
    const store = this.elementStyle.store;

    // Include properties from the disabled property store, if any.
    const disabledProps = store.disabled.get(this.domRule);
    if (!disabledProps) {
      return [];
    }

    const textProps = [];

    for (const prop of disabledProps) {
      const value = store.userProperties.getProperty(
        this.domRule,
        prop.name,
        prop.value
      );
      const textProp = new TextProperty({
        rule: this,
        name: prop.name,
        value,
        priority: prop.priority,
      });
      textProp.enabled = false;
      textProps.push(textProp);
    }

    return textProps;
  }

  /**
   * Reread the current state of the rules and rebuild text
   * properties as needed.
   */
  refresh(appliedStyle) {
    this.matchedSelectorIndexes = appliedStyle.matchedSelectorIndexes || [];
    const colorSchemeChanged =
      this.darkColorScheme !== appliedStyle.darkColorScheme;
    this.darkColorScheme = appliedStyle.darkColorScheme;
    const siblingCountChanged = this.siblingCount !== appliedStyle.siblingCount;
    this.siblingCountChanged = appliedStyle.siblingCountChanged;
    const siblingIndexChanged =
      this.siblingIndexChanged !== appliedStyle.siblingIndexChanged;
    this.siblingIndexChanged = appliedStyle.siblingIndexChanged;

    // The `domRule`'s StyleRule actor doesn't change (this.domRule == appliedStyle.rule)
    // but the appliedStyle may object may update any of its other attributes.
    // Here we may select two distinct elements, matching the same CSS Rule,
    // but the inheritance may be different.
    // (this is covered by browser_inspector_pseudoclass-lock.js)
    if (appliedStyle.inherited != this.inherited) {
      this.inherited = appliedStyle.inherited;
      this.#inheritedSectionLabel = null;
    }

    const newTextProps = this.#getTextProperties();

    // The element style rule behaves differently on refresh. We basically need to update
    // it to reflect the new text properties exactly. The order might have changed, some
    // properties might have been removed, etc. And we don't need to mark anything as
    // disabled here. The element style rule should always reflect the content of the
    // style attribute.
    if (this.domRule.type === ELEMENT_STYLE) {
      this.textProps = newTextProps;

      if (this.editor) {
        this.editor.populate(true);
      }

      return;
    }

    // Update current properties for each property present on the style.
    // Also keep track of properties that didn't exist in the current set of properties.
    const brandNewProps = [];
    for (const newProp of newTextProps) {
      if (!this.#updateTextProperty(newProp)) {
        brandNewProps.push(newProp);
      }
    }

    // Refresh editors and disabled state for all the properties that
    // were updated.
    for (const prop of this.textProps) {
      // Valid properties that aren't disabled might need to get updated in some condition
      if (
        prop.enabled &&
        prop.isValid() &&
        // Update if:
        // - it's using light-dark() and the color scheme changed
        ((colorSchemeChanged && prop.value.includes("light-dark(")) ||
          // - it's using attr() (we don't check if the attribute changed as it would be
          //   cumbersome and this is unlikely to be perf sensitive as the function might
          //   not be used that much)
          prop.value.includes("attr(") ||
          (siblingCountChanged && prop.value.includes("sibling-count(")) ||
          (siblingIndexChanged && prop.value.includes("sibling-index(")))
      ) {
        prop.updateEditor();
      }
    }

    // Add brand new properties.
    this.textProps = this.textProps.concat(brandNewProps);

    // Refresh the editor if one already exists.
    if (this.editor) {
      this.editor.populate();
    }
  }

  /**
   * Update the current TextProperties that match a given property
   * from the authoredText.  Will choose one existing TextProperty to update
   * with the new property's value, and will disable all others.
   *
   * When choosing the best match to reuse, properties will be chosen
   * by assigning a rank and choosing the highest-ranked property:
   *   Name, value, and priority match, enabled. (6)
   *   Name, value, and priority match, disabled. (5)
   *   Name and value match, enabled. (4)
   *   Name and value match, disabled. (3)
   *   Name matches, enabled. (2)
   *   Name matches, disabled. (1)
   *
   * If no existing properties match the property, nothing happens.
   *
   * @param {TextProperty} newProp
   *        The current version of the property, as parsed from the
   *        authoredText in Rule.#getTextProperties().
   * @return {boolean} true if a property was updated, false if no properties
   *         were updated.
   */
  #updateTextProperty(newProp) {
    const match = { rank: 0, prop: null };

    for (const prop of this.textProps) {
      if (prop.name !== newProp.name) {
        continue;
      }

      // Start at rank 1 for matching name.
      let rank = 1;

      // Value and Priority matches add 2 to the rank.
      // Being enabled adds 1.  This ranks better matches higher,
      // with priority breaking ties.
      if (prop.value === newProp.value) {
        rank += 2;
        if (prop.priority === newProp.priority) {
          rank += 2;
        }
      }

      if (prop.enabled) {
        rank += 1;
      }

      if (rank > match.rank) {
        match.rank = rank;
        match.prop = prop;
      }
    }

    // If we found a match, update its value with the new text property
    // value.
    if (match.prop) {
      match.prop.set(newProp);
      return true;
    }

    return false;
  }

  /**
   * Jump between editable properties in the UI. If the focus direction is
   * forward, begin editing the next property name if available or focus the
   * new property editor otherwise. If the focus direction is backward,
   * begin editing the previous property value or focus the selector editor if
   * this is the first element in the property list.
   *
   * @param {TextProperty} textProperty
   *        The text property that will be left to focus on a sibling.
   * @param {number} direction
   *        The move focus direction number.
   */
  editClosestTextProperty(textProperty, direction) {
    let index = this.textProps.indexOf(textProperty);

    if (direction === Services.focus.MOVEFOCUS_FORWARD) {
      for (++index; index < this.textProps.length; ++index) {
        // The prop could be invisible or a hidden unused variable
        if (this.textProps[index].editor) {
          break;
        }
      }
      if (index === this.textProps.length) {
        textProperty.rule.editor.closeBrace.click();
      } else {
        this.textProps[index].editor.nameSpan.click();
      }
    } else if (direction === Services.focus.MOVEFOCUS_BACKWARD) {
      for (--index; index >= 0; --index) {
        // The prop could be invisible or a hidden unused variable
        if (this.textProps[index].editor) {
          break;
        }
      }
      if (index < 0) {
        textProperty.editor.ruleEditor.selectorText.click();
      } else {
        this.textProps[index].editor.valueSpan.click();
      }
    }
  }

  /**
   * Return a string representation of the rule.
   */
  stringifyRule() {
    const selectorText = this.selectorText;
    let cssText = "";
    const terminator = Services.appinfo.OS === "WINNT" ? "\r\n" : "\n";

    for (const textProp of this.textProps) {
      if (!textProp.invisible) {
        cssText += "\t" + textProp.stringifyProperty() + terminator;
      }
    }

    return selectorText + " {" + terminator + cssText + "}";
  }

  /**
   * @returns {boolean} Whether or not the rule is in a layer
   */
  isInLayer() {
    return this.domRule.ancestorData.some(({ type }) => type === "layer");
  }

  /**
   * Return whether this rule and the one passed are in the same layer,
   * (as in described in the spec; this is not checking that the 2 rules are children
   * of the same CSSLayerBlockRule)
   *
   * @param {Rule} otherRule: The rule we want to compare with
   * @returns {boolean}
   */
  isInDifferentLayer(otherRule) {
    const filterLayer = ({ type }) => type === "layer";
    const thisLayers = this.domRule.ancestorData.filter(filterLayer);
    const otherRuleLayers = otherRule.domRule.ancestorData.filter(filterLayer);

    if (thisLayers.length !== otherRuleLayers.length) {
      return true;
    }

    return thisLayers.some((layer, i) => {
      const otherRuleLayer = otherRuleLayers[i];
      // For named layers, we can compare the layer name directly, since we want to identify
      // the actual layer, not the specific CSSLayerBlockRule.
      // For nameless layers though, we don't have a choice and we can only identify them
      // via their CSSLayerBlockRule, so we're using the rule actorID.
      return (
        (layer.value || layer.actorID) !==
        (otherRuleLayer.value || otherRuleLayer.actorID)
      );
    });
  }

  /**
   * @returns {boolean} Whether or not the rule is in a @starting-style rule
   */
  isInStartingStyle() {
    return this.domRule.ancestorData.some(
      ({ type }) => type === "starting-style"
    );
  }

  /**
   * @returns {boolean} Whether or not the rule can be edited
   */
  isEditable() {
    return (
      !this.isSystem &&
      this.domRule.type !== PRES_HINTS &&
      // FIXME: Should be removed as part of Bug 2004046
      this.domRule.className !== "CSSPositionTryRule"
    );
  }

  /**
   * See whether this rule has any non-invisible properties.
   *
   * @return {boolean} true if there is any visible property, or false
   *         if all properties are invisible
   */
  hasAnyVisibleProperties() {
    for (const prop of this.textProps) {
      if (!prop.invisible) {
        return true;
      }
    }
    return false;
  }
}

module.exports = Rule;
