/*************************************************************
 *
 *  Copyright (c) 2009-2026 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 Explorers based on mouse events.
 *
 * @author v.sorge@mathjax.org (Volker Sorge)
 */

import { A11yDocument, DummyRegion, Region } from './Region.js';
import { Explorer, AbstractExplorer } from './Explorer.js';
import { ExplorerPool } from './ExplorerPool.js';
import '../sre.js';

/**
 * Interface for mouse explorers. Adds the necessary mouse events.
 *
 * @interface
 * @augments {Explorer}
 */
export interface MouseExplorer extends Explorer {
  /**
   * Function to be executed on mouse over.
   *
   * @param {MouseEvent} event The mouse event.
   */
  MouseOver(event: MouseEvent): void;

  /**
   * Function to be executed on mouse out.
   *
   * @param {MouseEvent} event The mouse event.
   */
  MouseOut(event: MouseEvent): void;
}

/**
 * @class
 * @augments {AbstractExplorer}
 *
 * @template T  The type that is consumed by the Region of this explorer.
 */
export abstract class AbstractMouseExplorer<T>
  extends AbstractExplorer<T>
  implements MouseExplorer
{
  /**
   * @override
   */
  protected events: [string, (x: Event) => void][] = super.Events().concat([
    ['mouseover', this.MouseOver.bind(this)],
    ['mouseout', this.MouseOut.bind(this)],
  ]);

  /**
   * @override
   */
  public MouseOver(_event: MouseEvent) {
    this.Start();
  }

  /**
   * @override
   */
  public MouseOut(_event: MouseEvent) {
    this.Stop();
  }
}

/**
 * Exploration via hovering.
 *
 * @class
 * @augments {AbstractMouseExplorer}
 *
 * @template T
 */
export abstract class Hoverer<T> extends AbstractMouseExplorer<T> {
  /**
   * @class
   * @augments {AbstractMouseExplorer<T>}
   *
   * @param {A11yDocument} document The current document.
   * @param {ExplorerPool} pool The explorer pool.
   * @param {Region<T>} region A region to display results.
   * @param {HTMLElement} node The node on which the explorer works.
   * @param {(node: HTMLElement) => boolean} nodeQuery Predicate on nodes that
   *    will fire the hoverer.
   * @param {(node: HTMLElement) => T} nodeAccess Accessor to extract node value
   *    that is passed to the region.
   */
  protected constructor(
    public document: A11yDocument,
    public pool: ExplorerPool,
    public region: Region<T>,
    protected node: HTMLElement,
    protected nodeQuery: (node: HTMLElement) => boolean,
    protected nodeAccess: (node: HTMLElement) => T
  ) {
    super(document, pool, region, node);
  }

  /**
   * @override
   */
  public MouseOut(event: MouseEvent) {
    this.highlighter.unhighlight();
    this.region.Hide();
    super.MouseOut(event);
  }

  /**
   * @override
   */
  public MouseOver(event: MouseEvent) {
    super.MouseOver(event);
    const target = event.target as HTMLElement;
    const [node, kind] = this.getNode(target);
    if (!node) {
      return;
    }
    this.highlighter.unhighlight();
    this.highlighter.highlight([node]);
    this.region.Update(kind);
    this.region.Show(node);
  }

  /**
   * Retrieves the closest node on which the node query fires. Thereby closest
   * is defined as:
   * 1. The node or its ancestor on which the query is true.
   * 2. In case 1 does not exist the left-most child on which query is true.
   * 3. Otherwise fails.
   *
   * @param {HTMLElement} node The node on which the mouse event fired.
   * @returns {[HTMLElement, T]} Node and output pair if successful.
   */
  public getNode(node: HTMLElement): [HTMLElement, T] {
    const original = node;
    while (node && node !== this.node) {
      if (this.nodeQuery(node)) {
        return [node, this.nodeAccess(node)];
      }
      node = node.parentNode as HTMLElement;
    }
    node = original;
    while (node) {
      if (this.nodeQuery(node)) {
        return [node, this.nodeAccess(node)];
      }
      const child = node.childNodes[0] as HTMLElement;
      node =
        child && child.tagName === 'defs' // This is for SVG.
          ? (node.childNodes[1] as HTMLElement)
          : child;
    }
    return [null, null];
  }
}

/**
 * Hoverer that displays information on nodes (e.g., as tooltips).
 *
 * @class
 * @augments {Hoverer}
 */
export class ValueHoverer extends Hoverer<string> {}

/**
 * Hoverer that displays node content (e.g., for magnification).
 *
 * @class
 * @augments {Hoverer}
 */
export class ContentHoverer extends Hoverer<HTMLElement> {}

/**
 * Highlights maction nodes on hovering.
 *
 * @class
 * @augments {Hoverer}
 */
export class FlameHoverer extends Hoverer<void> {
  /**
   * @override
   */
  protected constructor(
    public document: A11yDocument,
    public pool: ExplorerPool,
    _ignore: any,
    protected node: HTMLElement
  ) {
    super(
      document,
      pool,
      new DummyRegion(document),
      node,
      (x) => this.highlighter.isMactionNode(x),
      () => {}
    );
  }
}
