/**
 * @mergeTarget
 * @module Src/Controllers
 */

import { LisioNumericParameterNames } from "@lisio/lisio-profils";
import { CoreErrorCodes } from "@lisio/lisio-engine";

/**
 * Class representing a daltonism preview controller.\
 * It aims to preview the daltonism color adaptation (and contrast, once daltonism is active)
 * on the widget's own document.\
 * The client script (`lisio-solution`) adapts the host page by applying a CSS `filter`
 * referencing an SVG color matrix to the host page's `<html>`. Since the widget renders
 * inside its own iframe document, that ancestor filter never reaches it for the SVG-referenced
 * part of the effect. This controller rebuilds the same SVG filter locally, from the parameter
 * values the widget already holds, and applies it to the widget's own document so its colors
 * preview consistently with the page.\
 * Plain `contrast()` (no SVG reference) does compose through the iframe boundary on its own,
 * so it's left alone as long as daltonism is untouched. But once this controller sets its own
 * local `filter` for the color matrix, that override fully takes over rendering for this
 * property and the ancestor's contrast filter stops reaching through — so the current contrast
 * value is folded into the local filter too, once daltonism activates it.
 */
class LisioDaltonismPreviewController {
  /**
   * Private static attribute to store singleton instance
   * @source
   */
  private static _current: LisioDaltonismPreviewController;

  /**
   * Private attribute to store the color matrix filter primitive (R/G/B channel enhancement)
   * @source
   */
  private _colorMatrix?: SVGFEColorMatrixElement;

  /**
   * Private attribute to store the hue rotation filter primitive
   * @source
   */
  private _hueMatrix?: SVGFEColorMatrixElement;

  /**
   * Private attribute to store the saturation filter primitive
   * @source
   */
  private _saturationMatrix?: SVGFEColorMatrixElement;

  /**
   * Private attribute to store the brightness filter primitive
   * @source
   */
  private _brightnessMatrix?: SVGFEComponentTransferElement;

  /**
   * Private attribute to store which numeric daltonism parameters are at their default value
   * @source
   */
  /**
   * Private attribute to store the CSS-ready contrast value (1 = no change), mirroring the
   * `numericValue` conversion done by the client script's contrast adapter
   * @source
   */
  private _contrastValue = 1;

  private _isInDefaultState: Partial<
    Record<LisioNumericParameterNames, boolean>
  > = {
    [LisioNumericParameterNames.DALTONISM_R]: true,
    [LisioNumericParameterNames.DALTONISM_G]: true,
    [LisioNumericParameterNames.DALTONISM_B]: true,
    [LisioNumericParameterNames.DALTONISM_HUE]: true,
    [LisioNumericParameterNames.DALTONISM_SATURATION]: true,
    [LisioNumericParameterNames.DALTONISM_BRIGHTNESS]: true,
  };

  /**
   * @static
   * Getter for attribute {@link _current | _current}
   * @returns Returns _current attribute
   * @source
   */
  public static get current(): LisioDaltonismPreviewController {
    return LisioDaltonismPreviewController._current;
  }

  /**
   * Private method to lazily build the SVG filter defs in the widget's own document
   * @returns Returns nothing
   * @source
   */
  private ensureFilter(): void {
    if (this._colorMatrix != undefined) {
      return;
    }
    const svgNamespace = "http://www.w3.org/2000/svg";
    const svg = document.createElementNS(svgNamespace, "svg");
    svg.setAttribute("height", "0");
    svg.setAttribute("width", "0");
    (svg as unknown as HTMLElement).style.position = "absolute";

    const filter = document.createElementNS(svgNamespace, "filter");
    filter.id = "lisio-widget-daltonism-filter";

    this._colorMatrix = document.createElementNS(
      svgNamespace,
      "feColorMatrix",
    ) as SVGFEColorMatrixElement;
    this._colorMatrix.setAttribute(
      "values",
      "1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0",
    );

    this._hueMatrix = document.createElementNS(
      svgNamespace,
      "feColorMatrix",
    ) as SVGFEColorMatrixElement;
    this._hueMatrix.setAttribute("type", "hueRotate");
    this._hueMatrix.setAttribute("values", "0");

    this._saturationMatrix = document.createElementNS(
      svgNamespace,
      "feColorMatrix",
    ) as SVGFEColorMatrixElement;
    this._saturationMatrix.setAttribute("type", "saturate");
    this._saturationMatrix.setAttribute("values", "1");

    this._brightnessMatrix = document.createElementNS(
      svgNamespace,
      "feComponentTransfer",
    ) as SVGFEComponentTransferElement;
    for (const tagName of ["feFuncR", "feFuncG", "feFuncB"] as const) {
      const transferFunction = document.createElementNS(svgNamespace, tagName);
      transferFunction.setAttribute("type", "linear");
      transferFunction.setAttribute("slope", "1");
      this._brightnessMatrix.append(transferFunction);
    }

    filter.append(
      this._colorMatrix,
      this._hueMatrix,
      this._saturationMatrix,
      this._brightnessMatrix,
    );
    svg.append(filter);
    document.body.append(svg);
  }

  /**
   * Private method to check if every numeric daltonism parameter is at its default value
   * @returns Returns true if every numeric daltonism parameter is at its default value
   * @source
   */
  private isAllDefault(): boolean {
    return Object.values(this._isInDefaultState).every(
      (isDefault) => isDefault,
    );
  }

  /**
   * Private method to apply or remove the preview filter on the widget's own document
   * @returns Returns nothing
   * @source
   */
  private applyFilter(): void {
    if (this.isAllDefault()) {
      // No local override needed: the client script's own page-level contrast filter
      // (a plain `contrast()` function, no SVG reference) composites through the iframe
      // boundary on its own, so it already shows correctly here without any help.
      document.documentElement.style.removeProperty("filter");
    } else {
      // Once we set our own local filter (needed for the SVG color matrix), it fully
      // replaces the widget's rendering pipeline for this property, so the ancestor's
      // contrast filter no longer reaches through. Fold the current contrast value into
      // our own filter so both effects still show together.
      document.documentElement.style.setProperty(
        "filter",
        `url(#lisio-widget-daltonism-filter) contrast(${this._contrastValue})`,
      );
    }
  }

  /**
   * Public method updating the preview filter's contrast value
   * @param {number} value - Raw contrast slider value (1-20 scale, 10 = default/no change)
   * @returns Returns nothing
   * @source
   */
  public updateContrast(value: number): void {
    this._contrastValue = value >= 1 ? value / 10 : value;
    this.applyFilter();
  }

  /**
   * Public method updating the preview filter for a numeric daltonism parameter
   * @param {LisioNumericParameterNames} feature - Numeric daltonism parameter that changed
   * @param {number} value - New value of the parameter
   * @param {number} defaultValue - Default value of the parameter
   * @returns Returns nothing
   * @source
   */
  public updateParameter(
    feature: LisioNumericParameterNames,
    value: number,
    defaultValue: number,
  ): void {
    this.ensureFilter();
    this._isInDefaultState[feature] = value === defaultValue;
    if (feature === LisioNumericParameterNames.DALTONISM_R) {
      this.updateColorMatrix(0, value);
    } else if (feature === LisioNumericParameterNames.DALTONISM_G) {
      this.updateColorMatrix(6, value);
    } else if (feature === LisioNumericParameterNames.DALTONISM_B) {
      this.updateColorMatrix(12, value);
    } else if (feature === LisioNumericParameterNames.DALTONISM_HUE) {
      this._hueMatrix!.setAttribute("values", value.toString());
    } else if (feature === LisioNumericParameterNames.DALTONISM_SATURATION) {
      this._saturationMatrix!.setAttribute("values", (value / 100).toString());
    } else if (feature === LisioNumericParameterNames.DALTONISM_BRIGHTNESS) {
      const slope = (value / 100).toString();
      for (const transferFunction of Array.from(
        this._brightnessMatrix!.children,
      )) {
        transferFunction.setAttribute("slope", slope);
      }
    }
    this.applyFilter();
  }

  /**
   * Private method updating one diagonal entry of the color matrix
   * @param {number} index - Index of the diagonal entry to update
   * @param {number} value - New value (0-190 scale) to normalize and apply
   * @returns Returns nothing
   * @source
   */
  private updateColorMatrix(index: number, value: number): void {
    const matrixValues = this._colorMatrix!.getAttribute("values")!.split(" ");
    matrixValues[index] = (value / 100).toString();
    this._colorMatrix!.setAttribute("values", matrixValues.join(" "));
  }

  /**
   * Constructor of class {@link LisioDaltonismPreviewController | LisioDaltonismPreviewController}\
   * Throws {@link https://env-preprod-docs.lisio.fr/lisio-engine/enums/Src_Core.CoreErrorCodes.html#SINGLETON_NOT_UNIQUE | CoreErrorCodes.SINGLETON_NOT_UNIQUE} if singleton already initialized
   * @source
   */
  constructor() {
    if (LisioDaltonismPreviewController._current == undefined) {
      LisioDaltonismPreviewController._current = this;
    } else {
      throw new Error(
        `Code : ${CoreErrorCodes.SINGLETON_NOT_UNIQUE}. Singleton already initialize`,
      );
    }
  }
}

export { LisioDaltonismPreviewController };
