/*************************************************************
 *
 *  COPYRIGHT (c) 2022-2024 The MathJax Consortium
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

/**
 * @file Class for handling all explorers on a single Math Item.
 *
 * @author v.sorge@mathjax.org (Volker Sorge)
 */

import { LiveRegion, SpeechRegion, ToolTip, HoverRegion } from './Region.js';
import type { ExplorerMathDocument, ExplorerMathItem } from '../explorer.js';

import { Explorer } from './Explorer.js';
import { SpeechExplorer } from './KeyExplorer.js';
import * as me from './MouseExplorer.js';
import { TreeColorer, FlameColorer } from './TreeExplorer.js';

import { Highlighter, getHighlighter } from './Highlighter.js';

/**
 * The regions objects needed for the explorers.
 */
export class RegionPool {
  /**
   * The speech region.
   */
  public speechRegion: SpeechRegion = new SpeechRegion(this.document);

  /**
   * The Braille region.
   */
  public brailleRegion: LiveRegion = new LiveRegion(this.document);

  /**
   * Hover region for all magnifiers.
   */
  public magnifier: HoverRegion = new HoverRegion(this.document);

  /**
   * A tooltip region.
   */
  public tooltip1: ToolTip = new ToolTip(this.document);

  /**
   * A tooltip region.
   */
  public tooltip2: ToolTip = new ToolTip(this.document);

  /**
   * A tooltip region.
   */
  public tooltip3: ToolTip = new ToolTip(this.document);

  /**
   * @param {ExplorerMathDocument} document The document the handler belongs to.
   */
  constructor(public document: ExplorerMathDocument) {}
}

/**
 * Type of explorer initialization methods.
 *
 * @type {(doc: ExplorerMathDocument,
 *         pool: ExplorerPool,
 *         node: HTMLElement,
 *         ...rest: any[]
 *        ) => Explorer}
 */
type ExplorerInit = (
  doc: ExplorerMathDocument,
  pool: ExplorerPool,
  node: HTMLElement,
  ...rest: any[]
) => Explorer;

/**
 *  Generation methods for all MathJax explorers available via option settings.
 */
const allExplorers: { [options: string]: ExplorerInit } = {
  speech: (
    doc: ExplorerMathDocument,
    pool: ExplorerPool,
    node: HTMLElement,
    ...rest: any[]
  ) => {
    const explorer = SpeechExplorer.create(
      doc,
      pool,
      doc.explorerRegions.speechRegion,
      node,
      doc.explorerRegions.brailleRegion,
      doc.explorerRegions.magnifier,
      rest[0],
      rest[1]
    ) as SpeechExplorer;
    explorer.sound = true;
    return explorer;
  },
  mouseMagnifier: (
    doc: ExplorerMathDocument,
    pool: ExplorerPool,
    node: HTMLElement,
    ..._rest: any[]
  ) =>
    me.ContentHoverer.create(
      doc,
      pool,
      doc.explorerRegions.magnifier,
      node,
      (x: HTMLElement) => x.hasAttribute('data-semantic-type'),
      (x: HTMLElement) => x
    ),
  hover: (
    doc: ExplorerMathDocument,
    pool: ExplorerPool,
    node: HTMLElement,
    ..._rest: any[]
  ) => me.FlameHoverer.create(doc, pool, null, node),
  infoType: (
    doc: ExplorerMathDocument,
    pool: ExplorerPool,
    node: HTMLElement,
    ..._rest: any[]
  ) =>
    me.ValueHoverer.create(
      doc,
      pool,
      doc.explorerRegions.tooltip1,
      node,
      (x: HTMLElement) => x.hasAttribute('data-semantic-type'),
      (x: HTMLElement) => x.getAttribute('data-semantic-type')
    ),
  infoRole: (
    doc: ExplorerMathDocument,
    pool: ExplorerPool,
    node: HTMLElement,
    ..._rest: any[]
  ) =>
    me.ValueHoverer.create(
      doc,
      pool,
      doc.explorerRegions.tooltip2,
      node,
      (x: HTMLElement) => x.hasAttribute('data-semantic-role'),
      (x: HTMLElement) => x.getAttribute('data-semantic-role')
    ),
  infoPrefix: (
    doc: ExplorerMathDocument,
    pool: ExplorerPool,
    node: HTMLElement,
    ..._rest: any[]
  ) =>
    me.ValueHoverer.create(
      doc,
      pool,
      doc.explorerRegions.tooltip3,
      node,
      (x: HTMLElement) => x.hasAttribute?.('data-semantic-prefix-none'),
      (x: HTMLElement) => x.getAttribute?.('data-semantic-prefix-none')
    ),
  flame: (
    doc: ExplorerMathDocument,
    pool: ExplorerPool,
    node: HTMLElement,
    ..._rest: any[]
  ) => FlameColorer.create(doc, pool, null, node),
  treeColoring: (
    doc: ExplorerMathDocument,
    pool: ExplorerPool,
    node: HTMLElement,
    ...rest: any[]
  ) => TreeColorer.create(doc, pool, null, node, ...rest),
};

/**
 * Class to bundle and handle all explorers on a Math item. This in particular
 * means that all explorer share the same highlighter, meaning that there is no
 * uncontrolled interaction between highlighting of different explorers.
 */
export class ExplorerPool {
  /**
   * A highlighter that is used to mark nodes during auto voicing.
   */
  public secondaryHighlighter: Highlighter;

  /**
   * The explorer dictionary.
   */
  public explorers: { [key: string]: Explorer } = {};

  /**
   * The currently attached explorers
   */
  protected attached: string[] = [];

  /**
   * The target document.
   */
  protected document: ExplorerMathDocument;

  /**
   * The node explorers will be attached to.
   */
  protected node: HTMLElement;

  /**
   * The corresponding Mathml node as a string.
   */
  protected mml: string;

  /**
   * The primary highlighter shared by all explorers.
   */
  private _highlighter: Highlighter;

  /**
   * The name of the current output jax.
   */
  private _renderer: string;

  /**
   * All explorers that need to be restarted on a rerendered element.
   */
  private _restart: string[] = [];

  /**
   * @returns {Highlighter} The primary highlighter shared by all explorers.
   */
  public get highlighter(): Highlighter {
    if (this._renderer !== this.document.outputJax.name) {
      this._renderer = this.document.outputJax.name;
      this.setPrimaryHighlighter();
      return this._highlighter;
    }
    const [foreground, background] = this.colorOptions();
    this._highlighter.setColor(background, foreground);
    return this._highlighter;
  }

  /**
   * @param {ExplorerMathDocument} document The target document.
   * @param {HTMLElement} node The node explorers will be attached to.
   * @param {string} mml The corresponding Mathml node as a string.
   * @param {ExplorerMathItem} item The current math item.
   */
  public init(
    document: ExplorerMathDocument,
    node: HTMLElement,
    mml: string,
    item: ExplorerMathItem
  ) {
    this.document = document;
    this.mml = mml;
    this.node = node;
    this.setPrimaryHighlighter();
    for (const key of Object.keys(allExplorers)) {
      this.explorers[key] = allExplorers[key](
        this.document,
        this,
        this.node,
        this.mml,
        item
      );
    }
    this.setSecondaryHighlighter();
    this.attach();
  }

  /**
   * A11y options keys associated with the speech explorer.
   */
  private speechExplorerKeys = ['speech', 'braille', 'keyMagnifier'];

  /**
   * Attaches the explorers that are currently meant to be active given
   * the document options. Detaches all others.
   */
  public attach() {
    this.attached = [];
    const keyExplorers = [];
    const a11y = this.document.options.a11y;
    for (const [key, explorer] of Object.entries(this.explorers)) {
      if (explorer instanceof SpeechExplorer) {
        explorer.stoppable = false;
        keyExplorers.unshift(explorer);
        if (
          this.speechExplorerKeys.some(
            (exKey) => this.document.options.a11y[exKey]
          )
        ) {
          explorer.Attach();
          this.attached.push(key);
        } else {
          explorer.Detach();
        }
        continue;
      }
      if (
        a11y[key] ||
        (key === 'speech' && (a11y.braille || a11y.keyMagnifier))
      ) {
        explorer.Attach();
        this.attached.push(key);
      } else {
        explorer.Detach();
      }
    }
    // Ensure that the last currently attached key explorer stops propagating
    // key events.
    for (const explorer of keyExplorers) {
      if (explorer.attached) {
        explorer.stoppable = true;
        break;
      }
    }
  }

  /**
   * Computes the explorers that need to be reattached after a MathItem is
   * rerendered.
   */
  public reattach() {
    for (const key of this.attached) {
      const explorer = this.explorers[key];
      if (explorer.active) {
        this._restart.push(key);
        explorer.Stop();
      }
    }
  }

  /**
   * Restarts explorers after a MathItem is rerendered.
   */
  public restart() {
    this._restart.forEach((x) => {
      this.explorers[x].Start();
    });
    this._restart = [];
  }

  /**
   * A highlighter for the explorer.
   */
  protected setPrimaryHighlighter() {
    const [foreground, background] = this.colorOptions();
    this._highlighter = getHighlighter(
      LiveRegion.priority.primary,
      background,
      foreground,
      this.document.outputJax.name
    );
  }

  /**
   * Sets the secondary highlighter for marking nodes during autovoicing.
   */
  protected setSecondaryHighlighter() {
    this.secondaryHighlighter = getHighlighter(
      LiveRegion.priority.secondary,
      { color: 'red', alpha: 0.8 },
      { color: 'black' },
      this.document.outputJax.name
    );
    (this.speech.region as SpeechRegion).highlighter =
      this.secondaryHighlighter;
  }

  /**
   * Highlights a set of DOM nodes.
   *
   * @param {HTMLElement[]} nodes The array of HTML nodes to be highlighted.
   */
  public highlight(nodes: HTMLElement[]) {
    this.highlighter.highlight(nodes);
  }

  /**
   * Unhighlights the currently highlighted DOM nodes.
   */
  public unhighlight() {
    this.secondaryHighlighter.unhighlight();
    this.highlighter.unhighlight();
  }

  /**
   * Convenience method to return the speech explorer of the pool with the
   * correct type.
   *
   * @returns {SpeechExplorer} The speech explorer.
   */
  public get speech(): SpeechExplorer {
    return this.explorers['speech'] as SpeechExplorer;
  }

  /**
   * Retrieves color assignment for the document options.
   *
   * @returns {[ { color: string; alpha: number }, { color: string; alpha:
   *             number } ]} Color assignments for fore and background colors.
   */
  private colorOptions(): [
    { color: string; alpha: number },
    { color: string; alpha: number },
  ] {
    const opts = this.document.options.a11y;
    const foreground = {
      color: opts.foregroundColor.toLowerCase(),
      alpha: opts.foregroundOpacity / 100,
    };
    const background = {
      color: opts.backgroundColor.toLowerCase(),
      alpha: opts.backgroundOpacity / 100,
    };
    return [foreground, background];
  }
}
