/*************************************************************
 *  Copyright (c) 2021-2026 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    Utility functions for the mathtools package.
 *
 * @author dpvc@mathjax.org (Davide P. Cervone)
 */

import { EqnArrayItem } from '../base/BaseItems.js';
import { UnitUtil } from '../UnitUtil.js';
import TexParser from '../TexParser.js';
import TexError from '../TexError.js';
import { lookup } from '../../../util/Options.js';
import { MmlNode } from '../../../core/MmlTree/MmlNode.js';
import { HandlerType } from '../HandlerTypes.js';
import { NewcommandUtil } from '../newcommand/NewcommandUtil.js';

import { MathtoolsMethods } from './MathtoolsMethods.js';

/**
 * Utility functions for the Mathtools package.
 */
export const MathtoolsUtil = {
  /**
   * Set the displaystyle and scriptlevel attributes of an mstyle element
   *
   * @param {MmlNode} mml     The mstyle node to modify.
   * @param {string} style    The TeX style macro to apply.
   */
  setDisplayLevel(mml: MmlNode, style: string) {
    if (!style) return;
    const [display, script] = lookup(
      style,
      {
        '\\displaystyle': [true, 0],
        '\\textstyle': [false, 0],
        '\\scriptstyle': [false, 1],
        '\\scriptscriptstyle': [false, 2],
      },
      [null, null]
    );
    if (display !== null) {
      mml.attributes.set('displaystyle', display);
      mml.attributes.set('scriptlevel', script);
    }
  },

  /**
   * Check that the top stack item is an alignment table.
   *
   * @param {TexParser} parser   The current TeX parser.
   * @param {string} name        The name of the macro doing the checking.
   * @returns {EqnArrayItem}     The top item (an EqnArrayItem).
   */
  checkAlignment(parser: TexParser, name: string): EqnArrayItem {
    const top = parser.stack.Top() as EqnArrayItem;
    if (top.kind !== EqnArrayItem.prototype.kind) {
      throw new TexError(
        'NotInAlignment',
        '%1 can only be used in aligment environments',
        name
      );
    }
    return top;
  },

  /**
   * Add a paired delimiter to the list of them.
   *
   * @param {TexParser} parser   The current TeX parser
   * @param {string} cs          The control sequence for the paired delimiters.
   * @param {string[]} args      The definition for the paired delimiters.  One of:
   *                                [left, right]
   *                                [left, right, body, argcount]
   *                                [left, right, body, argcount, pre, post]
   */
  addPairedDelims(parser: TexParser, cs: string, args: string[]) {
    if (parser.configuration.handlers.get(HandlerType.MACRO).contains(cs)) {
      throw new TexError(
        'CommadExists',
        'Command %1 already defined',
        `\\${cs}`
      );
    }
    NewcommandUtil.addMacro(
      parser,
      cs,
      MathtoolsMethods.PairedDelimiters,
      args
    );
  },

  /**
   * Adjust the line spacing for a table.
   *
   * @param {MmlNode} mtable   The mtable node to adjust (if it is a table).
   * @param {string} spread    The dimension to change by (number-with-units).
   */
  spreadLines(mtable: MmlNode, spread: string) {
    if (!mtable.isKind('mtable')) return;
    let rowspacing = mtable.attributes.get('rowspacing') as string;
    const add = UnitUtil.dimen2em(spread);
    rowspacing = rowspacing
      .split(/ /)
      .map((s) => UnitUtil.em(Math.max(0, UnitUtil.dimen2em(s) + add)))
      .join(' ');
    mtable.attributes.set('rowspacing', rowspacing);
  },

  /**
   * Check if a string is a number and return it with an explicit plus if there isn't one.
   *
   * @param {string} name   The name of the macro doing the checking.
   * @param {string} n      The string to test as a number.
   * @returns {string}      The number with an explicit sign.
   */
  plusOrMinus(name: string, n: string): string {
    n = n.trim();
    if (!n.match(/^[-+]?(?:\d+(?:\.\d*)?|\.\d+)$/)) {
      throw new TexError('NotANumber', 'Argument to %1 is not a number', name);
    }
    return n.match(/^[-+]/) ? n : '+' + n;
  },

  /**
   * Parse a \prescript argument, with its associated format, if any.
   *
   * @param {TexParser} parser   The active tex parser.
   * @param {string} name        The name of the calling macro (\prescript).
   * @param {string} pos         The position for the argument (sub, sup, arg).
   * @returns {MmlNode}          The parsed MML version of the argument.
   */
  getScript(parser: TexParser, name: string, pos: string): MmlNode {
    let arg = UnitUtil.trimSpaces(parser.GetArgument(name));
    if (arg === '') {
      return parser.create('node', 'none');
    }
    const format = parser.options.mathtools[`prescript-${pos}-format`];
    if (format) {
      arg = `${format}{${arg}}`;
    }
    const mml = new TexParser(
      arg,
      parser.stack.env,
      parser.configuration
    ).mml();
    return mml.isKind('TeXAtom') && mml.isEmpty
      ? parser.create('node', 'none')
      : mml;
  },
};
