/* 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 {
  createFactory,
  PureComponent,
} = require("resource://devtools/client/shared/vendor/react.mjs");
const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.mjs");
const {
  connect,
} = require("resource://devtools/client/shared/vendor/react-redux.js");

const CSSDeclaration = createFactory(
  require("resource://devtools/client/inspector/changes/components/CSSDeclaration.js")
);
const {
  getChangesTree,
} = require("resource://devtools/client/inspector/changes/selectors/changes.js");
const {
  getSourceForDisplay,
} = require("resource://devtools/client/inspector/changes/utils/changes-utils.js");
const {
  getStr,
} = require("resource://devtools/client/inspector/changes/utils/l10n.js");

class ChangesApp extends PureComponent {
  static get propTypes() {
    return {
      // Nested CSS rule tree structure of CSS changes grouped by source (stylesheet)
      changesTree: PropTypes.object.isRequired,
      // Event handler for "contextmenu" event
      onContextMenu: PropTypes.func.isRequired,
      // Event handler for click on "Copy All Changes" button
      onCopyAllChanges: PropTypes.func.isRequired,
      // Event handler for click on "Copy Rule" button
      onCopyRule: PropTypes.func.isRequired,
    };
  }

  constructor(props) {
    super(props);
  }

  renderCopyAllChangesButton() {
    const button = dom.button(
      {
        className: "changes__copy-all-changes-button",
        onClick: e => {
          e.stopPropagation();
          this.props.onCopyAllChanges();
        },
        title: getStr("changes.contextmenu.copyAllChangesDescription"),
      },
      getStr("changes.contextmenu.copyAllChanges")
    );

    return dom.div({ className: "changes__toolbar" }, button);
  }

  renderCopyButton(ruleId) {
    return dom.button(
      {
        className: "changes__copy-rule-button",
        onClick: e => {
          e.stopPropagation();
          this.props.onCopyRule(ruleId);
        },
        title: getStr("changes.contextmenu.copyRuleDescription"),
      },
      getStr("changes.contextmenu.copyRule")
    );
  }

  renderDeclarations(remove = [], add = []) {
    const removals = remove
      // Sorting changed declarations in the order they appear in the Rules view.
      .sort((a, b) => a.index > b.index)
      .map(({ property, value, index }) => {
        return CSSDeclaration({
          key: "remove-" + property + index,
          className: "level diff-remove",
          property,
          value,
        });
      });

    const additions = add
      // Sorting changed declarations in the order they appear in the Rules view.
      .sort((a, b) => a.index > b.index)
      .map(({ property, value, index }) => {
        return CSSDeclaration({
          key: "add-" + property + index,
          className: "level diff-add",
          property,
          value,
        });
      });

    return [removals, additions];
  }

  renderRule(ruleId, rule, level = 0) {
    const diffClass = rule.isNew ? "diff-add" : "";
    return dom.div(
      {
        key: ruleId,
        className: "changes__rule devtools-monospace",
        "data-rule-id": ruleId,
        style: {
          "--diff-level": level,
        },
      },
      this.renderSelectors(rule.selectors, rule.isNew),
      this.renderCopyButton(ruleId),
      // Render any nested child rules if they exist.
      rule.children.map(childRule => {
        return this.renderRule(childRule.ruleId, childRule, level + 1);
      }),
      // Render any changed CSS declarations.
      this.renderDeclarations(rule.remove, rule.add),
      // Render the closing bracket with a diff marker if necessary.
      dom.div({ className: `level ${diffClass}` }, "}")
    );
  }

  /**
   * Return an array of React elements for the rule's selector.
   *
   * @param  {Array} selectors
   *         List of strings as versions of this rule's selector over time.
   * @param  {Boolean} isNewRule
   *         Whether the rule was created at runtime.
   * @return {Array}
   */
  renderSelectors(selectors, isNewRule) {
    const selectorDiffClassMap = new Map();

    // The selectors array has just one item if it hasn't changed. Render it as-is.
    // If the rule was created at runtime, mark the single selector as added.
    // If it has two or more items, the first item was the original selector (mark as
    // removed) and the last item is the current selector (mark as added).
    if (selectors.length === 1) {
      selectorDiffClassMap.set(selectors[0], isNewRule ? "diff-add" : "");
    } else if (selectors.length >= 2) {
      selectorDiffClassMap.set(selectors[0], "diff-remove");
      selectorDiffClassMap.set(selectors[selectors.length - 1], "diff-add");
    }

    const elements = [];

    for (const [selector, diffClass] of selectorDiffClassMap) {
      elements.push(
        dom.div(
          {
            key: selector,
            className: `level changes__selector ${diffClass}`,
            title: selector,
          },
          selector,
          dom.span({}, " {")
        )
      );
    }

    return elements;
  }

  renderDiff(changes = {}) {
    // Render groups of style sources: stylesheets and element style attributes.
    return Object.entries(changes).map(([sourceId, source]) => {
      const path = getSourceForDisplay(source);
      const { href, rules, isFramed } = source;

      return dom.div(
        {
          key: sourceId,
          "data-source-id": sourceId,
          className: "source",
        },
        dom.div(
          {
            className: "href",
            title: href,
          },
          dom.span({}, path),
          isFramed && this.renderFrameBadge(href)
        ),
        // Render changed rules within this source.
        Object.entries(rules).map(([ruleId, rule]) => {
          return this.renderRule(ruleId, rule);
        })
      );
    });
  }

  renderFrameBadge(href = "") {
    return dom.span(
      {
        className: "inspector-badge",
        title: href,
      },
      getStr("changes.iframeLabel")
    );
  }

  renderEmptyState() {
    return dom.div(
      { className: "devtools-sidepanel-no-result" },
      dom.p({}, getStr("changes.noChanges")),
      dom.p({}, getStr("changes.noChangesDescription"))
    );
  }

  render() {
    const hasChanges = !!Object.keys(this.props.changesTree).length;
    return dom.div(
      {
        className: "theme-sidebar inspector-tabpanel",
        id: "sidebar-panel-changes",
        role: "document",
        tabIndex: "0",
        onContextMenu: this.props.onContextMenu,
      },
      !hasChanges && this.renderEmptyState(),
      hasChanges && this.renderCopyAllChangesButton(),
      hasChanges && this.renderDiff(this.props.changesTree)
    );
  }
}

const mapStateToProps = state => {
  return {
    changesTree: getChangesTree(state.changes),
  };
};

module.exports = connect(mapStateToProps)(ChangesApp);
