/*************************************************************
 *
 *  Copyright (c) 2018-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 AMS Parse methods.
 *
 * @author v.sorge@mathjax.org (Volker Sorge)
 */

import { StackItem } from '../StackItem.js';
import { ParseResult, ParseMethod } from '../Types.js';
import { ParseUtil } from '../ParseUtil.js';
import { UnitUtil } from '../UnitUtil.js';
import ParseMethods from '../ParseMethods.js';
import NodeUtil from '../NodeUtil.js';
import { TexConstant } from '../TexConstants.js';
import TexParser from '../TexParser.js';
import TexError from '../TexError.js';
import { ArrayItem } from '../base/BaseItems.js';
import { FlalignItem } from './AmsItems.js';
import BaseMethods from '../base/BaseMethods.js';
import { splitAlignArray } from '../base/BaseMethods.js';
import { TEXCLASS } from '../../../core/MmlTree/MmlNode.js';
import { MmlMunderover } from '../../../core/MmlTree/MmlNodes/munderover.js';
import {
  MmlNode,
  AbstractMmlTokenNode,
} from '../../../core/MmlTree/MmlNode.js';
import { NewcommandUtil } from '../newcommand/NewcommandUtil.js';

/**
 * Utility for breaking the \sideset scripts from any other material.
 *
 * @param {MmlNode} mml The node to check.
 * @returns {[MmlNode, MmlNode]} The msubsup with the scripts together with any extra nodes.
 */
function splitSideSet(mml: MmlNode): [MmlNode, MmlNode] {
  if (!mml || (mml.isInferred && mml.childNodes.length === 0)) {
    return [null, null];
  }
  if (mml.isKind('msubsup') && checkSideSetBase(mml)) {
    return [mml, null];
  }
  const child = NodeUtil.getChildAt(mml, 0);
  if (!(mml.isInferred && child && checkSideSetBase(child))) {
    return [null, mml];
  }
  mml.childNodes.splice(0, 1); // remove first child
  return [child, mml];
}

/**
 * Utility for checking if a \sideset argument has scripts with an empty base.
 *
 * @param {MmlNode} mml The node to check.
 * @returns {boolean} True if the base is not and empty mi element.
 */
function checkSideSetBase(mml: MmlNode): boolean {
  const base = mml.childNodes[0];
  return (
    base && base.isKind('mi') && (base as AbstractMmlTokenNode).getText() === ''
  );
}

// Namespace
export const AmsMethods: { [key: string]: ParseMethod } = {
  /**
   * Handle AMS array environments.
   *
   * @param {TexParser} parser The calling parser.
   * @param {StackItem} begin The opening stackitem.
   * @param {boolean} numbered Environment numbered.
   * @param {boolean} taggable Environment taggable (e.g., align* is taggable,
   *     split is not).
   * @param {string} align Column alignment.
   * @param {string} balign Column break alignment.
   * @param {string} spacing Column spacing.
   * @param {string} style Display style indicator.
   *
   * @returns {ParseResult} The constructed array stackitem.
   */
  AmsEqnArray(
    parser: TexParser,
    begin: StackItem,
    numbered: boolean,
    taggable: boolean,
    align: string,
    balign: string,
    spacing: string,
    style: string
  ): ParseResult {
    // @test Aligned, Gathered
    const args = parser.GetBrackets('\\begin{' + begin.getName() + '}');
    const array = BaseMethods.EqnArray(
      parser,
      begin,
      numbered,
      taggable,
      align,
      balign,
      spacing,
      style
    );
    return ParseUtil.setArrayAlign(array as ArrayItem, args, parser);
  },

  /**
   * Handle AMS  alignat environments.
   *
   * @param {TexParser} parser The calling parser.
   * @param {StackItem} begin The opening stackitem.
   * @param {boolean} numbered Environment numbered.
   * @param {boolean} taggable Environment taggable (e.g., align* is taggable,
   *     split is not).
   *
   * @returns {ParseResult} The constructed array stackitem.
   */
  AlignAt(
    parser: TexParser,
    begin: StackItem,
    numbered: boolean,
    taggable: boolean
  ): ParseResult {
    const name = begin.getName();
    let valign;
    let align = '';
    let balign = '';
    const spacing = [];
    if (!taggable) {
      // @test Alignedat
      valign = parser.GetBrackets('\\begin{' + name + '}');
    }
    const n = parser.GetArgument('\\begin{' + name + '}');
    if (n.match(/[^0-9]/)) {
      // @test PositiveIntegerArg
      throw new TexError(
        'PositiveIntegerArg',
        'Argument to %1 must be a positive integer',
        '\\begin{' + name + '}'
      );
    }
    let count = parseInt(n, 10);
    while (count > 0) {
      align += 'rl';
      balign += 'bt';
      spacing.push('0em 0em');
      count--;
    }
    const spaceStr = spacing.join(' ');
    if (taggable) {
      // @test Alignat, Alignat Star
      return AmsMethods.EqnArray(
        parser,
        begin,
        numbered,
        taggable,
        align,
        balign,
        spaceStr
      );
    }
    // @test Alignedat
    const array = AmsMethods.EqnArray(
      parser,
      begin,
      numbered,
      taggable,
      align,
      balign,
      spaceStr
    );
    return ParseUtil.setArrayAlign(array as ArrayItem, valign, parser);
  },

  /**
   * Implements multline environment (mostly handled through STACKITEM below)
   *
   * @param {TexParser} parser The calling parser.
   * @param {StackItem} begin The opening stackitem.
   * @param {boolean} numbered Environment numbered.
   *
   * @returns {ParseResult} The constructed multiline stackitem.
   */
  Multline(
    parser: TexParser,
    begin: StackItem,
    numbered: boolean
  ): ParseResult {
    // @test Shove*, Multline
    ParseUtil.checkEqnEnv(parser);
    parser.Push(begin);
    const padding = parser.options.ams['multlineIndent'];
    const item = parser.itemFactory.create(
      'multline',
      numbered,
      parser.stack
    ) as ArrayItem;
    item.arraydef = {
      displaystyle: true,
      rowspacing: '.5em',
      columnspacing: '100%',
      width: parser.options.ams['multlineWidth'],
      side: parser.options['tagSide'],
      minlabelspacing: parser.options['tagIndent'],
      'data-array-padding': `${padding} ${padding}`,
      'data-width-includes-label': true, // take label space out of 100% width
    };
    return item;
  },

  /**
   * Generate an align at environment.
   *
   * @param {TexParser} parser The current TeX parser.
   * @param {StackItem} begin The begin stackitem.
   * @param {boolean} numbered Is this a numbered array.
   * @param {boolean} padded Is it padded.
   *
   * @returns {ParseResult} The constructed flalign stackitem.
   */
  XalignAt(
    parser: TexParser,
    begin: StackItem,
    numbered: boolean,
    padded: boolean
  ): ParseResult {
    const n = parser.GetArgument('\\begin{' + begin.getName() + '}');
    if (n.match(/[^0-9]/)) {
      throw new TexError(
        'PositiveIntegerArg',
        'Argument to %1 must be a positive integer',
        '\\begin{' + begin.getName() + '}'
      );
    }
    const align = padded ? 'crl' : 'rlc';
    const balign = padded ? 'mbt' : 'btm';
    const width = padded ? 'fit auto auto' : 'auto auto fit';
    const item = AmsMethods.FlalignArray(
      parser,
      begin,
      numbered,
      padded,
      false,
      align,
      balign,
      width,
      true
    ) as FlalignItem;
    item.setProperty('xalignat', 2 * parseInt(n));
    return item;
  },

  /**
   * Generate an flalign environment.
   *
   * @param {TexParser} parser The current TeX parser.
   * @param {StackItem} begin The begin stackitem.
   * @param {boolean} numbered Is this a numbered array.
   * @param {boolean} padded Is it padded.
   * @param {boolean} center Is it centered.
   * @param {string} align The horizontal alignment for columns
   * @param {string} balign The vertical break alignment for columns
   * @param {string} width The column widths of the table
   * @param {boolean} zeroWidthLabel True if the label should be in llap/rlap
   *
   * @returns {ParseResult} The constructed flalign stackitem.
   */
  FlalignArray(
    parser: TexParser,
    begin: StackItem,
    numbered: boolean,
    padded: boolean,
    center: boolean,
    align: string,
    balign: string,
    width: string,
    zeroWidthLabel: boolean = false
  ): ParseResult {
    ParseUtil.checkEqnEnv(parser);
    parser.Push(begin);
    align = align
      .split('')
      .join(' ')
      .replace(/r/g, 'right')
      .replace(/l/g, 'left')
      .replace(/c/g, 'center');
    balign = splitAlignArray(balign);
    const item = parser.itemFactory.create(
      'flalign',
      begin.getName(),
      numbered,
      padded,
      center,
      parser.stack
    ) as FlalignItem;
    item.arraydef = {
      width: '100%',
      displaystyle: true,
      columnalign: align,
      columnspacing: '0em',
      columnwidth: width,
      rowspacing: '3pt',
      'data-break-align': balign,
      side: parser.options['tagSide'],
      minlabelspacing: zeroWidthLabel ? '0' : parser.options['tagIndent'],
      'data-width-includes-label': true,
    };
    item.setProperty('zeroWidthLabel', zeroWidthLabel);
    return item;
  },

  /**
   * Handle DeclareMathOperator.
   *
   * @param {TexParser} parser The calling parser.
   * @param {string} name The macro name.
   */
  HandleDeclareOp(parser: TexParser, name: string) {
    const star = parser.GetStar() ? '*' : '';
    const cs = NewcommandUtil.GetCsNameArgument(parser, name);
    const op = parser.GetArgument(name);
    NewcommandUtil.addMacro(parser, cs, AmsMethods.Macro, [
      `\\operatorname${star}{${op}}`,
    ]);
    parser.Push(parser.itemFactory.create('null'));
  },

  /**
   * Handle operatorname.
   *
   * @param {TexParser} parser The calling parser.
   * @param {string} name The macro name.
   */
  HandleOperatorName(parser: TexParser, name: string) {
    // @test Operatorname
    const star = parser.GetStar();
    //
    //  Parse the argument using operator letters and grouping multiple letters.
    //
    const op = UnitUtil.trimSpaces(parser.GetArgument(name));
    let mml = new TexParser(
      op,
      {
        ...parser.stack.env,
        font: TexConstant.Variant.NORMAL,
        multiLetterIdentifiers: parser.options.ams.operatornamePattern,
        operatorLetters: true,
        noAutoOP: true,
      },
      parser.configuration
    ).mml();
    //
    //  If we get a single mi, remove the autoOp property
    //  (it will get that automatically if more than one letter),
    //  otherwise wrap the results in a TeXAtom.
    //
    if (mml.isKind('mi')) {
      mml.removeProperty('autoOP');
    } else {
      mml = parser.create('node', 'TeXAtom', [mml]);
    }
    //
    //  Mark the limit properties and the TeX class.
    //
    NodeUtil.setProperties(mml, {
      movesupsub: star,
      movablelimits: true,
      texClass: TEXCLASS.OP,
    });
    //
    //  Skip a following \limits macro if not a starred operator
    //
    if (!star) {
      const c = parser.GetNext();
      const i = parser.i;
      if (c === '\\' && ++parser.i && parser.GetCS() !== 'limits') {
        parser.i = i;
      }
    }
    //
    parser.Push(parser.itemFactory.create('fn', mml));
  },

  /**
   * Handle sideset.
   *
   * @param {TexParser} parser The calling parser.
   * @param {string} name The macro name.
   */
  SideSet(parser: TexParser, name: string) {
    //
    //  Get the pre- and post-scripts, and any extra material from the arguments
    //
    const [preScripts, preRest] = splitSideSet(parser.ParseArg(name));
    const [postScripts, postRest] = splitSideSet(parser.ParseArg(name));
    const base = parser.ParseArg(name);
    let mml = base;
    //
    //  If there are pre-scripts...
    //
    if (preScripts) {
      //
      //  If there is other material...
      //
      if (preRest) {
        //
        //  Replace the empty base of the prescripts with a phantom element of the
        //    original base, with width 0 (but still of the correct height and depth).
        //    so the scripts will be at the right heights.
        //
        preScripts.replaceChild(
          parser.create('node', 'mphantom', [
            parser.create(
              'node',
              'mpadded',
              [ParseUtil.copyNode(base, parser)],
              { width: 0 }
            ),
          ]),
          NodeUtil.getChildAt(preScripts, 0)
        );
      } else {
        //
        //  If there is no extra meterial, make a mmultiscripts element
        //
        mml = parser.create('node', 'mmultiscripts', [base]);
        //
        //  Add any postscripts
        //
        if (postScripts) {
          NodeUtil.appendChildren(mml, [
            NodeUtil.getChildAt(postScripts, 1) ||
              parser.create('node', 'none'),
            NodeUtil.getChildAt(postScripts, 2) ||
              parser.create('node', 'none'),
          ]);
        }
        //
        //  Add the prescripts (left aligned)
        //
        NodeUtil.setProperty(mml, 'scriptalign', 'left');
        NodeUtil.appendChildren(mml, [
          parser.create('node', 'mprescripts'),
          NodeUtil.getChildAt(preScripts, 1) || parser.create('node', 'none'),
          NodeUtil.getChildAt(preScripts, 2) || parser.create('node', 'none'),
        ]);
      }
    }
    //
    //  If there are postscripts and we didn't make a mmultiscript element above...
    //
    if (postScripts && mml === base) {
      //
      //  Replace the emtpy base with actual base, and use that as the mml
      //
      postScripts.replaceChild(base, NodeUtil.getChildAt(postScripts, 0));
      mml = postScripts;
    }
    //
    //  Put the needed pieces into a TeXAtom of class OP.
    //  Note that the postScripts are in the mml element,
    //    either as part of the mmultiscripts node, or the
    //    msubsup with the base inserted into it.
    //
    const mrow = parser.create('node', 'TeXAtom', [], {
      texClass: TEXCLASS.OP,
      movesupsub: true,
      movablelimits: true,
    });
    if (preRest) {
      if (preScripts) {
        mrow.appendChild(preScripts);
      }
      mrow.appendChild(preRest);
    }
    mrow.appendChild(mml);
    if (postRest) {
      mrow.appendChild(postRest);
    }
    parser.Push(mrow);
  },

  /**
   * Handle extra letters in \operatorname (- and *), default to normal otherwise.
   *
   * @param {TexParser} parser The calling parser.
   * @param {string} c The letter being checked
   *
   * @returns {false|void} Nothing.
   */
  operatorLetter(parser: TexParser, c: string): false | void {
    return parser.stack.env.operatorLetters
      ? ParseMethods.variable(parser, c)
      : false;
  },

  /**
   * Handle multi integral signs.
   *
   * @param {TexParser} parser The calling parser.
   * @param {string} name The macro name.
   * @param {string} integral The actual integral sign.
   */
  MultiIntegral(parser: TexParser, name: string, integral: string) {
    let next = parser.GetNext();
    if (next === '\\') {
      // @test MultiInt with Command
      const i = parser.i;
      next = parser.GetArgument(name);
      parser.i = i;
      if (next === '\\limits') {
        // @test MultiInt with Limits
        integral = '\\!\\!\\mathop{\\,\\,' + integral + '}';
      }
    }
    // @test MultiInt, MultiInt in Context
    parser.string = integral + ' ' + parser.string.slice(parser.i);
    parser.i = 0;
  },

  /**
   *  Handle stretchable arrows.
   *
   * @param {TexParser} parser The calling parser.
   * @param {string} name The macro name.
   * @param {number} chr The arrow character in hex code.
   * @param {number} l Left width.
   * @param {number} r Right width.
   * @param {number} m Min width
   */
  xArrow(
    parser: TexParser,
    name: string,
    chr: number,
    l: number,
    r: number,
    m: number = 0
  ) {
    const def = {
      width: '+' + UnitUtil.em((l + r) / 18),
      lspace: UnitUtil.em(l / 18),
    };
    const bot = parser.GetBrackets(name);
    const first = parser.ParseArg(name);
    const dstrut = parser.create('node', 'mspace', [], { depth: '.2em' });
    let arrow = parser.create(
      'token',
      'mo',
      { stretchy: true, texClass: TEXCLASS.ORD }, // REL is applied in a TeXAtom below
      String.fromCodePoint(chr)
    );
    if (m) {
      arrow.attributes.set('minsize', UnitUtil.em(m));
    }
    arrow = parser.create('node', 'mstyle', [arrow], { scriptlevel: 0 });
    const mml = parser.create('node', 'munderover', [arrow]) as MmlMunderover;
    let mpadded = parser.create('node', 'mpadded', [first, dstrut], def);
    NodeUtil.setAttribute(mpadded, 'voffset', '-.2em');
    NodeUtil.setAttribute(mpadded, 'height', '-.2em');
    NodeUtil.setChild(mml, mml.over, mpadded);
    if (bot) {
      // @test Above Below Left Arrow, Above Below Right Arrow
      const bottom = new TexParser(
        bot,
        parser.stack.env,
        parser.configuration
      ).mml();
      const bstrut = parser.create('node', 'mspace', [], { height: '.75em' });
      mpadded = parser.create('node', 'mpadded', [bottom, bstrut], def);
      NodeUtil.setAttribute(mpadded, 'voffset', '.15em');
      NodeUtil.setAttribute(mpadded, 'depth', '-.15em');
      NodeUtil.setChild(mml, mml.under, mpadded);
    }
    // @test Above Left Arrow, Above Right Arrow, Above Left Arrow in Context,
    //       Above Right Arrow in Context
    NodeUtil.setProperty(mml, 'subsupOK', true);
    //
    // Use an empty item to prevent the xarrow from further stretching (see #3457)
    // and enclose both in a TeXAtom to make the combination a REL.
    //
    parser.Push(
      parser.create(
        'node',
        'TeXAtom',
        [
          parser.create('node', 'TeXAtom', [], {
            texClass: TEXCLASS.NONE,
          }),
          mml,
        ],
        { texClass: TEXCLASS.REL }
      )
    );
  },

  /**
   * Record presence of \shoveleft and \shoveright
   *
   * @param {TexParser} parser The calling parser.
   * @param {string} _name The macro name.
   * @param {string} shove The shove value.
   */
  HandleShove(parser: TexParser, _name: string, shove: string) {
    const top = parser.stack.Top();
    // @test Shove (Left|Right) (Top|Middle|Bottom)
    if (top.kind !== 'multline') {
      // @test Shove Error Environment
      throw new TexError(
        'CommandOnlyAllowedInEnv',
        '%1 only allowed in %2 environment',
        parser.currentCS,
        'multline'
      );
    }
    if (top.Size()) {
      // @test Shove Error (Top|Middle|Bottom)
      throw new TexError(
        'CommandAtTheBeginingOfLine',
        '%1 must come at the beginning of the line',
        parser.currentCS
      );
    }
    top.setProperty('shove', shove);
  },

  /**
   * Handle \cfrac
   *
   * @param {TexParser} parser The calling parser.
   * @param {string} name The macro name.
   */
  CFrac(parser: TexParser, name: string) {
    let lr = UnitUtil.trimSpaces(parser.GetBrackets(name, ''));
    const num = parser.GetArgument(name);
    const den = parser.GetArgument(name);
    const lrMap: { [key: string]: string } = {
      l: TexConstant.Align.LEFT,
      r: TexConstant.Align.RIGHT,
      '': '',
    };
    const numNode = new TexParser(
      '\\strut\\textstyle{' + num + '}',
      parser.stack.env,
      parser.configuration
    ).mml();
    const denNode = new TexParser(
      '\\strut\\textstyle{' + den + '}',
      parser.stack.env,
      parser.configuration
    ).mml();
    const frac = parser.create('node', 'mfrac', [numNode, denNode]);
    lr = lrMap[lr];
    if (lr == null) {
      // @test Center Fraction Error
      throw new TexError(
        'IllegalAlign',
        'Illegal alignment specified in %1',
        parser.currentCS
      );
    }
    if (lr) {
      // @test Right Fraction, Left Fraction
      NodeUtil.setProperties(frac, { numalign: lr, denomalign: lr });
    }
    // @test Center Fraction
    parser.Push(frac);
  },

  /**
   * Implement AMS generalized fraction.
   *
   * @param {TexParser} parser The calling parser.
   * @param {string} name The macro name.
   * @param {string} left Left delimiter.
   * @param {string} right Right delimiter.
   * @param {string} thick Line thickness.
   * @param {string} style Math style.
   */
  Genfrac(
    parser: TexParser,
    name: string,
    left: string,
    right: string,
    thick: string,
    style: string
  ) {
    if (left == null) {
      // @test Genfrac
      left = parser.GetDelimiterArg(name);
    }
    if (right == null) {
      // @test Genfrac
      right = parser.GetDelimiterArg(name);
    }
    if (thick == null) {
      // @test Genfrac
      thick = parser.GetArgument(name);
    }
    if (style == null) {
      // @test Genfrac
      style = UnitUtil.trimSpaces(parser.GetArgument(name));
    }
    const num = parser.ParseArg(name);
    const den = parser.ParseArg(name);
    let frac = parser.create('node', 'mfrac', [num, den]);
    if (thick !== '') {
      // @test Normal Binomial, Text Binomial, Display Binomial
      NodeUtil.setAttribute(frac, 'linethickness', thick);
    }
    if (left || right) {
      // @test Normal Binomial, Text Binomial, Display Binomial
      NodeUtil.setProperty(frac, 'withDelims', true);
      frac = ParseUtil.fixedFence(parser.configuration, left, frac, right);
    }
    if (style !== '') {
      const styleDigit = parseInt(style, 10);
      const styleAlpha = ['D', 'T', 'S', 'SS'][styleDigit];
      if (styleAlpha == null) {
        // @test Genfrac Error
        throw new TexError(
          'BadMathStyleFor',
          'Bad math style for %1',
          parser.currentCS
        );
      }
      frac = parser.create('node', 'mstyle', [frac]);
      if (styleAlpha === 'D') {
        // @test Display Fraction, Display Sub Fraction, Display Binomial,
        //       Display Sub Binomial
        NodeUtil.setProperties(frac, { displaystyle: true, scriptlevel: 0 });
      } else {
        // @test Text Fraction, Text Sub Fraction, Text Binomial,
        //       Text Sub Binomial
        NodeUtil.setProperties(frac, {
          displaystyle: false,
          scriptlevel: styleDigit - 1,
        });
      }
    }
    // @test Text Fraction, Normal Sub Binomial, Normal Binomial
    parser.Push(frac);
  },

  /**
   * Add the tag to the environment (to be added to the table row later).
   *
   * @param {TexParser} parser The calling parser.
   * @param {string} name The macro name.
   */
  HandleTag(parser: TexParser, name: string) {
    if (!parser.tags.currentTag.taggable && parser.tags.env) {
      // @test Illegal Tag Error
      throw new TexError(
        'CommandNotAllowedInEnv',
        '%1 not allowed in %2 environment',
        parser.currentCS,
        parser.tags.env
      );
    }
    if (parser.tags.currentTag.tag) {
      // @test Double Tag Error
      throw new TexError('MultipleCommand', 'Multiple %1', parser.currentCS);
    }
    const star = parser.GetStar();
    const tagId = UnitUtil.trimSpaces(parser.GetArgument(name));
    parser.tags.tag(tagId, star);
    parser.Push(parser.itemFactory.create('null'));
  },

  HandleNoTag: BaseMethods.HandleNoTag,
  HandleRef: BaseMethods.HandleRef,
  Macro: BaseMethods.Macro,
  Accent: BaseMethods.Accent,
  Tilde: BaseMethods.Tilde,
  Array: BaseMethods.Array,
  Spacer: BaseMethods.Spacer,
  NamedOp: BaseMethods.NamedOp,
  EqnArray: BaseMethods.EqnArray,
  Equation: BaseMethods.Equation,
};
