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

import {
  html,
  LitElement,
  classMap,
} from "chrome://global/content/vendor/lit.all.mjs";
import { tokensTable, variableLookupTable } from "../dist/tokens-table.mjs";
import styles from "./tokens-table.css";

export default {
  title: "Docs/Tokens Table",
  parameters: {
    options: {
      showPanel: false,
    },
  },
};

const HCM_MAP = {
  ActiveText: "#8080FF",
  ButtonBorder: "#000000",
  ButtonFace: "#000000",
  ButtonText: "#FFEE32",
  Canvas: "#000000",
  CanvasText: "#ffffff",
  Field: "#000000",
  FieldText: "#ffffff",
  GrayText: "#A6A6A6",
  Highlight: "#D6B4FD",
  HighlightText: "#2B2B2B",
  LinkText: "#8080FF",
  Mark: "#000000",
  MarkText: "#000000",
  SelectedItem: "#D6B4FD",
  SelectedItemText: "#2B2B2B",
  AccentColor: "AccentColor",
  AccentColorText: "#2B2B2B",
  VisitedText: "#8080FF",
};

const THEMED_TABLES = [
  "background-color",
  "border",
  "box-shadow",
  "border-color",
  "opacity",
  "text-color",
  "color",
  "outline",
  "icon-color",
  "icon-fill",
  "icon-stroke",
  "link",
];

/**
 *
 */
class TablesPage extends LitElement {
  #query = "";

  static properties = {
    surface: { type: String, state: true },
    tokensData: { type: Object, state: true },
    filteredTokens: { type: Object, state: true },
    tableFilter: { type: String, state: true },
  };

  constructor() {
    super();
    this.surface = "brand";
    this.tokensData = tokensTable;
  }

  handleSurfaceChange(e) {
    this.surface = e.target.value;
  }

  handleInput(e) {
    this.#query = e.originalTarget.value.trim().toLowerCase();
    e.preventDefault();
    this.handleSearch();
  }

  debounce(fn, delayMs = 300) {
    let timeout;
    return function () {
      clearTimeout(timeout);
      timeout = setTimeout(() => {
        fn.apply(this, arguments);
      }, delayMs);
    };
  }

  handleSearch() {
    // Clear filteredTokens to show all data.
    if (!this.#query) {
      this.filteredTokens = null;
    }

    let filteredTokens = Object.entries(this.tokensData).reduce(
      (acc, [key, tokens]) => {
        if (key.includes(this.#query)) {
          return { ...acc, [key]: tokens };
        }
        let filteredItems = tokens.filter(({ name: tokenName, value }) =>
          this.filterTokens(this.#query, tokenName, value)
        );
        if (filteredItems.length) {
          return { ...acc, [key]: filteredItems };
        }
        return acc;
      },
      {}
    );
    this.filteredTokens = filteredTokens;
  }

  filterTokens(searchTerm, tokenName, tokenVal) {
    if (
      tokenName.includes(searchTerm) ||
      (typeof tokenVal == "string" && tokenVal.includes(searchTerm))
    ) {
      return true;
    }
    if (typeof tokenVal == "object") {
      return Object.entries(tokenVal).some(([key, val]) =>
        this.filterTokens(searchTerm, key, val)
      );
    }
    return false;
  }

  handleClearSearch(e) {
    this.#query = "";
    e.preventDefault();
    this.handleSearch();
  }

  handleTableFilterClick(e) {
    if (!e.target.dataset.filter) {
      this.tableFilter = "";
    }
    this.tableFilter = e.target.dataset.filter;
  }

  render() {
    if (!this.tokensData) {
      return "";
    }

    return html`
      <link rel="stylesheet" href=${styles} />
      <div class="page-wrapper">
        <div class="filters-wrapper">
          <div class="top-filters">
            <div class="search-wrapper">
              <div class="search-icon"></div>
              <input
                type="search"
                placeholder="Filter tokens"
                @input=${this.debounce(this.handleInput)}
                .value=${this.#query}
              />
              <div
                class="clear-icon"
                role="button"
                title="Clear"
                ?hidden=${!this.#query}
                @click=${this.handleClearSearch}
              ></div>
            </div>
            <fieldset id="surface" @change=${this.handleSurfaceChange}>
              <label>
                <input
                  type="radio"
                  name="surface"
                  value="brand"
                  ?checked=${this.surface == "brand"}
                />
                In-content
              </label>
              <label>
                <input
                  type="radio"
                  name="surface"
                  value="platform"
                  ?checked=${this.surface == "platform"}
                />
                Chrome
              </label>
            </fieldset>
          </div>
          <div class="table-filter" @click=${this.handleTableFilterClick}>
            <moz-button type=${!this.tableFilter ? "primary" : "default"}>
              All tokens
            </moz-button>
            ${Object.keys(tokensTable).map(table => {
              return html`<moz-button
                type=${this.tableFilter == table ? "primary" : "default"}
                data-filter=${table}
              >
                ${table
                  .split("-")
                  .map(word => word.charAt(0).toUpperCase() + word.slice(1))
                  .join(" ")}
              </moz-button>`;
            })}
          </div>
        </div>
        <div class="tables-wrapper">
          ${Object.entries(this.filteredTokens ?? this.tokensData).map(
            ([tableName, tableEntries]) => {
              if (this.tableFilter && tableName !== this.tableFilter) {
                return "";
              }
              return html`
                <tokens-table
                  name=${tableName}
                  surface=${this.surface}
                  .tokens=${tableEntries}
                >
                </tokens-table>
              `;
            }
          )}
        </div>
      </div>
    `;
  }
}
customElements.define("tables-page", TablesPage);

/**
 *
 */
class TokensTable extends LitElement {
  TEMPLATES = {
    "font-size": this.fontTemplate,
    "font-weight": this.fontTemplate,
    "icon-color": this.iconTemplate,
    "icon-fill": this.iconTemplate,
    "icon-size": this.iconTemplate,
    "icon-stroke": this.iconTemplate,
    link: this.linkTemplate,
    margin: this.spaceAndSizeTemplate,
    "min-height": this.spaceAndSizeTemplate,
    outline: this.outlineTemplate,
    padding: this.paddingTemplate,
    "box-shadow": this.shadowTemplate,
    size: this.spaceAndSizeTemplate,
    space: this.spaceAndSizeTemplate,
    dimension: this.spaceAndSizeTemplate,
    "text-color": this.fontTemplate,
  };

  static properties = {
    name: { type: String },
    tokens: { type: Array },
    surface: { type: String },
  };

  createRenderRoot() {
    return this;
  }

  getDisplayValues(theme, token) {
    let value = this.getResolvedValue(theme, token);
    let raw = this.getRawValue(theme, value);
    return { value, ...(raw !== value && { raw }) };
  }

  // Return the value with variable references.
  // e.g. var(--color-white)
  getResolvedValue(theme, token) {
    if (typeof token == "string" || typeof token == "number") {
      return token;
    }

    if (theme == "hcm") {
      return (
        token.forcedColors ||
        token.prefersContrast ||
        token[this.surface]?.default ||
        token.default
      );
    }

    if (token[this.surface]) {
      return this.getResolvedValue(theme, token[this.surface]);
    }

    if (token[theme]) {
      return token[theme];
    }

    return token.default;
  }

  // Return the value with any variables replaced by what they represent.
  // e.g. #ffffff
  getRawValue(theme, token) {
    let cssRegex = /(?<var>var\(--(?<lookupKey>[a-z-0-9,\s]+)\))/;
    let matches = cssRegex.exec(token);
    if (matches) {
      let variableValue = variableLookupTable[matches.groups?.lookupKey];
      if (typeof variableValue == "undefined") {
        return token;
      }
      if (typeof variableValue == "object") {
        variableValue = this.getResolvedValue(theme, variableValue);
      }
      let rawValue = token.replace(matches.groups?.var, variableValue);
      if (rawValue.match(cssRegex)) {
        return this.getRawValue(theme, rawValue);
      }
      return rawValue;
    }
    return token;
  }

  getTemplate(value, tokenName, category = this.name) {
    // 0 is a valid value
    if (value == undefined) {
      return "N/A";
    }

    if (
      category.match("box-shadow") &&
      tokenName.startsWith("--box-shadow-color")
    ) {
      category = "color";
    }

    let templateFn = this.TEMPLATES[category]?.bind(this);
    if (templateFn) {
      return templateFn(category, value, tokenName);
    }

    return html`
      <div
        class="default-preview"
        style="${this.getDisplayProperty(category)}: ${HCM_MAP[value] ?? value}"
      ></div>
    `;
  }

  outlineTemplate(_, value, tokenName) {
    let property = tokenName.replaceAll(/--|focus-|-error/g, "");
    if (property == "outline-inset") {
      property = "outline-offset";
    }
    return html`
      <div
        class="outline-preview"
        style="${property}: ${HCM_MAP[value] ?? value};"
      ></div>
    `;
  }

  fontTemplate(category, value) {
    return html`
      <div class="text-wrapper">
        <p
          style="${this.getDisplayProperty(category)}: ${HCM_MAP[value] ??
          value};"
        >
          The quick brown fox jumps over the lazy dog
        </p>
      </div>
    `;
  }

  iconTemplate(_, value, tokenName) {
    const pattern = /color|fill|stroke/;
    let property = pattern.test(tokenName) ? "background-color" : "height";
    return html`
      <div
        class="icon-preview"
        style="${property}: ${HCM_MAP[value] ?? value};"
      ></div>
    `;
  }

  linkTemplate(_, value, tokenName) {
    let hasOutline = tokenName.includes("outline");
    return html`
      <a
        class=${classMap({ "link-preview": true, outline: hasOutline })}
        style="color: ${HCM_MAP[value] ?? value};"
      >
        Click me!
      </a>
    `;
  }

  spaceAndSizeTemplate(_, value, tokenName) {
    if (tokenName.includes("page-main")) {
      return "";
    }

    let isSize = tokenName.includes("size") || tokenName.includes("height");
    let items = isSize
      ? html`
          <div class="item" style="height: ${value};width: ${value};"></div>
        `
      : html`
          <div class="item"></div>
          <div class="item"></div>
        `;

    return html`
      <div
        class="space-size-preview space-size-background"
        style="gap: ${value};"
      >
        ${items}
      </div>
    `;
  }

  paddingTemplate(_, value) {
    return html`
      <div class="space-size-background">
        <div class="padding-item outer" style="padding: ${value}">
          <div class="padding-item inner"></div>
        </div>
      </div>
    `;
  }

  shadowTemplate(_, value) {
    return html`
      <div
        class="shadow-preview"
        style="box-shadow: ${HCM_MAP[value] ?? value};"
      ></div>
    `;
  }

  getDisplayProperty(category) {
    switch (category) {
      case "attention-dot":
      case "color":
      case "box-shadow":
        return "background-color";
      case "text-color":
        return "color";
      default:
        return category.replace("button", "");
    }
  }

  cellTemplate(type, tokenValue, tokenName) {
    let { value, raw } = this.getDisplayValues("default", tokenValue);
    return html`
      <td>
        <div class="preview-wrapper">
          ${type == "preview"
            ? html`${this.getTemplate(raw ?? value, tokenName)}`
            : html`<p class="value">${value}</p>
                ${raw ? html`<p class="value">${raw}</p>` : ""}`}
        </div>
      </td>
    `;
  }

  themeCellTemplate(theme, tokenValue, tokenName) {
    let { value, raw } = this.getDisplayValues(theme, tokenValue);
    return html`
      <td class="${theme}-theme">
        <div class="preview-wrapper">
          ${this.getTemplate(raw ?? value, tokenName)}
          <p class="value">${value}</p>
          ${raw ? html`<p class="value">${raw}</p>` : ""}
        </div>
      </td>
    `;
  }

  render() {
    let themedTable = THEMED_TABLES.includes(this.name);
    return html`
      <details class="table-wrapper" open="">
        <summary class="table-heading">
          <h3>${this.name}</h3>
        </summary>
        <table>
          <thead>
            <tr>
              <th>Name</th>
              ${themedTable
                ? html`
                    <th>Light</th>
                    <th>Dark</th>
                    <th>High contrast</th>
                  `
                : html`
                    <th>Value</th>
                    <th>Preview</th>
                  `}
            </tr>
          </thead>
          <tbody>
            ${this.tokens.map(({ name: tokenName, value }) => {
              return html`<tr id=${tokenName}>
                <td>${tokenName}</td>
                ${themedTable
                  ? html`
                      ${this.themeCellTemplate("light", value, tokenName)}
                      ${this.themeCellTemplate("dark", value, tokenName)}
                      ${this.themeCellTemplate("hcm", value, tokenName)}
                    `
                  : html`
                      ${this.cellTemplate("value", value, tokenName)}
                      ${this.cellTemplate("preview", value, tokenName)}
                    `}
              </tr>`;
            })}
          </tbody>
        </table>
      </details>
    `;
  }
}
customElements.define("tokens-table", TokensTable);

export const Default = () => {
  return html`<tables-page></tables-page>`;
};
