/*************************************************************
 *
 *  Copyright (c) 2019-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  The annotations submenus.
 *
 * @author dpvc@mathjax.org (Davide Cervone)
 * @author v.sorge@mathjax.org (Volker Sorge)
 */

import { SubMenu, Submenu } from './mj-context-menu.js';
import {
  MJContextMenu,
  DynamicSubmenu,
  SubmenuCallback,
} from './MJContextMenu.js';
import { MmlNode } from '../../core/MmlTree/MmlNode.js';
import * as MenuUtil from './MenuUtil.js';

/**
 * The annotation types to look for in a MathItem. These are options set in the
 * Menu object.
 */
type AnnotationTypes = { [type: string]: string[] };

/*======================================================================*/

/**
 * Returns a method to create the dynamic submenu for showing annotations.
 *
 * @param {() => void} box The info box in which to post annotation info.
 * @param {AnnotationTypes} types The legitimate annotation types.
 * @param {[string, string][]} cache We cache annotations of a math item, so we
 *    only have to compute them once for the two annotation menus.
 * @returns {DynamicSubmenu} Method generating the show annotations submenu.
 */
export function showAnnotations(
  box: () => void,
  types: AnnotationTypes,
  cache: [string, string][]
): DynamicSubmenu {
  return (menu: MJContextMenu, sub: Submenu, callback: SubmenuCallback) => {
    getAnnotation(getSemanticNode(menu), types, cache);
    callback(createAnnotationMenu(menu, sub, cache, box));
  };
}

/**
 * Returns a method to create the dynamic submenu for copying annotations.
 * Clears the annotation cache parameter.
 *
 * @param {[string, string][]} cache The annotation cache.
 * @returns {DynamicSubmenu} Method generating
 *    the copy annotations submenu.
 */
export function copyAnnotations(cache: [string, string][]): DynamicSubmenu {
  return (menu: MJContextMenu, sub: Submenu, callback: SubmenuCallback) => {
    const annotations = cache.slice();
    cache.length = 0;
    callback(
      createAnnotationMenu(menu, sub, annotations, () =>
        MenuUtil.copyToClipboard(annotation.trim())
      )
    );
  };
}

/**
 * Find the top-most semantics element that encloses the contents of the expression (if any)
 *
 * @param {MJContextMenu} menu The MathJax context menu
 * @returns {MmlNode | null} The semantics node that was found (or null)
 */
function getSemanticNode(menu: MJContextMenu): MmlNode | null {
  let node: MmlNode = menu.mathItem?.root;
  while (node && !node.isKind('semantics')) {
    if (node.isToken || node.childNodes.length !== 1) return null;
    node = node.childNodes[0];
  }
  return node;
}

/**
 * Computes the annotations for the math item the context menu is opened on.
 *
 * @param {MmlNode} node           The semantics node whose annotations are to be obtained
 * @param {AnnotationTypes} types The legitimate annotation types.
 * @param {[string, string][]} annotations The annoations cache of [type, text]
 *    where the type is the annotation type and text is the content of the
 *    annotation of that type.
 */
function getAnnotation(
  node: MmlNode,
  types: AnnotationTypes,
  annotations: [string, string][]
) {
  if (!node) return;
  for (const child of node.childNodes) {
    if (child.isKind('annotation')) {
      const match = annotationMatch(child, types);
      if (match) {
        const value = child.childNodes.reduce(
          (text, chars) => text + chars.toString(),
          ''
        );
        annotations.push([match, value]);
      }
    }
  }
}

/**
 * @param {MmlNode} child The annotation node to check if its encoding is one
 *     of the displayable ones.
 * @param {AnnotationTypes} types The annotation types.
 * @returns {string | null} The annotation type if it exists, o/w null.
 */
function annotationMatch(
  child: MmlNode,
  types: AnnotationTypes
): string | null {
  const encoding = child.attributes.get('encoding') as string;
  for (const type of Object.keys(types)) {
    if (types[type].includes(encoding)) {
      return type;
    }
  }
  return null;
}

/**
 * The annotation selected in the Annotation submenu (needed for the info box to
 * be able to show it)
 */
export let annotation: string = '';

/**
 * Create a submenu from the available annotations and attach it to the proper menu item
 *
 * @param {MJContextMenu} menu The context menu for which the submenu is constructed.
 * @param {Submenu} submenu The submenu to attach elements to.
 * @param {[string, string][]} annotations The annotations to use for the submenu.
 * @param {() => void} action The action to perform when the annotation is selected.
 * @returns {Submenu} The newly created submenu.
 */
function createAnnotationMenu(
  menu: MJContextMenu,
  submenu: Submenu,
  annotations: [string, string][],
  action: () => void
): SubMenu {
  return menu.factory.get('subMenu')(
    menu.factory,
    {
      items: annotations.map(([type, value]) => {
        return {
          type: 'command',
          id: type,
          content: type,
          action: () => {
            annotation = value;
            action();
          },
        };
      }),
      id: 'annotations',
    },
    submenu
  );
}
