/*************************************************************
 *
 *  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 Class for generating tags, references, etc.
 *
 * @author v.sorge@mathjax.org (Volker Sorge)
 */

import TexParser from './TexParser.js';
import { MmlNode } from '../../core/MmlTree/MmlNode.js';
import { MathItem } from '../../core/MathItem.js';
import { EnvList } from './StackItem.js';
import ParseOptions from './ParseOptions.js';

/**
 *  Simple class for label objects.
 */
export class Label {
  /**
   * @class
   * @param {string=} tag The tag that's displayed.
   * @param {string=} id The id that serves as reference.
   */
  constructor(
    public tag: string = '???',
    public id: string = ''
  ) {}
}

/**
 * A simple class for keeping track of tag information.
 */
export class TagInfo {
  /**
   * @class
   * @param {string} env The environment name (e.g., align).
   * @param {boolean} taggable Environment supports tags (e.g., align* does, but
   *     split does not.)
   * @param {boolean} defaultTags Environment is tagged by default (e.g., align
   *     is, but align* is not).
   * @param {string} tag The tag name (e.g., 1).
   * @param {string} tagId The unique id for that tag (e.g., mjx-eqn:1).
   * @param {string|string[]} tagFormat The formatted tag (e.g., "$\bullet$" or ['(','1',')']).
   * @param {boolean} noTag A no tagging command has been set (e.g., \notag,
   *     \nonumber).
   * @param {string} labelId The label referring to the tag.
   */
  constructor(
    readonly env: string = '',
    readonly taggable: boolean = false,
    readonly defaultTags: boolean = false,
    public tag: string = null,
    public tagId: string = '',
    public tagFormat: string | string[] = '',
    public noTag: boolean = false,
    public labelId: string = ''
  ) {}
}

export interface Tags {
  /**
   * The global configurations in which the parsing takes place.
   *
   * @type {ParseOptions}
   */
  configuration: ParseOptions;

  /**
   * IDs used in this equation.
   *
   * @type {{ [key: string]: boolean }}
   */
  ids: { [key: string]: boolean };

  /**
   * IDs used in previous equations.
   *
   * @type {{ [key: string]: boolean }}
   */
  allIds: { [key: string]: boolean };

  /**
   * Labels in the current equation.
   *
   * @type {{ [key: string]: Label }}
   */
  labels: { [key: string]: Label };

  /**
   * Labels in previous equations.
   *
   * @type {{ [key: string]: Label }}
   */
  allLabels: { [key: string]: Label };

  /**
   * The label to use for the next tag.
   *
   * @type {string}
   */
  label: string;

  /**
   * True if the equation contains an undefined label and must be reprocessed later.
   *
   * @type {boolean}
   */
  redo: boolean;

  /**
   * True when recompiling to update undefined references
   *
   * @type {boolean}
   */
  refUpdate: boolean;

  /**
   * The environment that is currently tagged.
   *
   * @type {string}
   */
  env: string;

  /**
   * The currently active tag.
   *
   * @type {TagInfo}
   */
  currentTag: TagInfo;

  /**
   * How to format tags.
   *
   * @param {string} tag The tag string.
   * @returns {string} The formatted numbered tag.
   */
  formatTag(tag: string): string | string[];

  /**
   * How to format references to tags.
   *
   * @param {string} tag The tag string.
   * @returns {string} The formatted numbered tag.
   */
  formatRef(tag: string): string | string[];

  /**
   * How to format URLs for references.
   *
   * @param {string} id The reference id.
   * @param {string} base The base URL in the reference.
   * @returns {}
   */
  formatUrl(id: string, base: string): string;

  /**
   * Set the tag automatically, by incrementing equation number.
   */
  autoTag(): void;

  /**
   * @returns {MmlNode|void} Generates and returns the tag node.
   */
  getTag(): MmlNode | void;

  /**
   * Clears tagging information.
   */
  clearTag(): void;

  /**
   * Resets the tag structure after an expression has been typeset.
   */
  resetTag(): void;

  /**
   * Fully resets the tag structure, in particular all the tagging and label
   * history.
   *
   * @param {number} offset A new offset value to start counting ids from.
   */
  reset(offset?: number): void;

  /**
   * Initialise tagging for a MathItem
   * (clear equation-specific labels and ids, set counter
   * and check for recompile)
   *
   * @param {MathItem} math   The MathItem for the current equation
   */
  startEquation(math: MathItem<any, any, any>): void;

  /**
   * Move equation-specific labels and ids to global ones,
   * save the counter, and mark the MathItem for redos
   */
  finishEquation(math: MathItem<any, any, any>): void;

  /**
   * Finalizes tag creation.
   *
   * @param {MmlNode} node
   * @param {EnvList} env List of environment properties.
   * @returns {MmlNode} The newly created tag.
   */
  finalize(node: MmlNode, env: EnvList): MmlNode;

  /**
   * Starts tagging on a given environment.
   *
   * @param {string} env The name of the environment.
   * @param {boolean} taggable True if taggable.
   * @param {boolean} defaultTags True if tagged by default.
   */
  start(env: string, taggable: boolean, defaultTags: boolean): void;

  /**
   * End tagging.
   */
  end(): void;

  /**
   * Computes the next tag.
   *
   * @param {string} tag The tag content.
   * @param {boolean} noFormat True if tag should not be formatted.
   */
  tag(tag: string, noFormat: boolean): void;

  /**
   * Call an explicit no tag.
   */
  notag(): void;

  /**
   * Entag an element by creating a table around it.
   *
   * @param {MmlNode} node The node to be tagged.
   * @param {MmlNode} tag The tag node.
   * @returns {MmlNode} The table node containing the original node and tag.
   */
  enTag(node: MmlNode, tag: MmlNode): MmlNode;
}

export class AbstractTags implements Tags {
  /**
   * Current equation number.
   *
   * @type {number}
   */
  protected counter: number = 0;

  /**
   * Equation number as equation begins.
   *
   * @type {number}
   */
  protected allCounter: number = 0;

  /**
   * @override
   */
  public configuration: ParseOptions = null;

  /**
   * @override
   */
  public ids: { [key: string]: boolean } = {};

  /**
   * @override
   */
  public allIds: { [key: string]: boolean } = {};

  /**
   * @override
   */
  public labels: { [key: string]: Label } = {};

  /**
   * @override
   */
  public allLabels: { [key: string]: Label } = {};

  /**
   * @override
   */
  public redo: boolean = false;

  /**
   * @override
   */
  public refUpdate: boolean = false;

  /**
   * @override
   */
  public currentTag: TagInfo = new TagInfo();

  /**
   * Chronology of all previous tags, in case we need to look something up in
   * the finalize method.
   *
   * @type {TagInfo[]}
   */
  protected history: TagInfo[] = [];

  private stack: TagInfo[] = [];

  /**
   * @override
   */
  public start(env: string, taggable: boolean, defaultTags: boolean) {
    if (this.currentTag) {
      this.stack.push(this.currentTag);
    }
    const label = this.label;
    this.currentTag = new TagInfo(env, taggable, defaultTags);
    this.label = label;
  }

  public get env() {
    return this.currentTag.env;
  }

  /**
   * @override
   */
  public end() {
    this.history.push(this.currentTag);
    const label = this.label;
    this.currentTag = this.stack.pop();
    if (label && !this.label) {
      this.label = label;
    }
  }

  /**
   * @override
   */
  public tag(tag: string, noFormat: boolean) {
    this.currentTag.tag = tag;
    this.currentTag.tagFormat = noFormat ? tag : this.formatTag(tag);
    this.currentTag.noTag = false;
  }

  /**
   * @override
   */
  public notag() {
    this.tag('', true);
    this.currentTag.noTag = true;
  }

  protected get noTag(): boolean {
    return this.currentTag.noTag;
  }

  public set label(label: string) {
    this.currentTag.labelId = label;
  }

  public get label() {
    return this.currentTag.labelId;
  }

  /**
   * @override
   */
  public formatUrl(id: string, base: string) {
    return base + '#' + encodeURIComponent(id);
  }

  /**
   * @override
   */
  public formatTag(tag: string) {
    return ['(', tag, ')'];
  }

  /**
   * @override
   */
  public formatRef(tag: string) {
    return this.formatTag(tag);
  }

  /**
   * How to format ids for labelling equations.
   *
   * @param {string} id The unique part of the id (e.g., label or number).
   * @returns {string} The formatted id.
   */
  protected formatId(id: string): string {
    return 'mjx-eqn:' + id.replace(/\s/g, '_');
  }

  /**
   * How to format numbers in tags.
   *
   * @param {number} n The tag number.
   * @returns {string} The formatted number.
   */
  protected formatNumber(n: number): string {
    return n.toString();
  }

  // Tag handling functions.
  /**
   * @override
   */
  public autoTag() {
    if (this.currentTag.tag == null) {
      this.counter++;
      this.tag(this.formatNumber(this.counter), false);
    }
  }

  /**
   * @override
   */
  public clearTag() {
    this.tag(null, true);
    this.currentTag.tagId = '';
  }

  /**
   * @override
   */
  public getTag(force: boolean = false) {
    if (force) {
      this.autoTag();
      return this.makeTag();
    }
    const ct = this.currentTag;
    if (ct.taggable && !ct.noTag) {
      if (ct.defaultTags) {
        this.autoTag();
      }
      if (ct.tag) {
        return this.makeTag();
      }
    }
    return null;
  }

  /**
   * @override
   */
  public resetTag() {
    this.history = [];
    this.redo = false;
    this.refUpdate = false;
    this.clearTag();
  }

  /**
   * @override
   */
  public reset(offset: number = 0) {
    this.resetTag();
    this.counter = this.allCounter = offset;
    this.allLabels = {};
    this.allIds = {};
    this.label = '';
  }

  /**
   * @override
   */
  public startEquation(math: MathItem<any, any, any>) {
    this.history = [];
    this.stack = [];
    this.clearTag();
    this.currentTag = new TagInfo('', undefined, undefined);
    this.labels = {};
    this.ids = {};
    this.counter = this.allCounter;
    this.redo = false;
    const recompile = math.inputData.recompile;
    if (recompile) {
      this.refUpdate = true;
      this.counter = recompile.counter;
    }
  }

  /**
   * @override
   */
  public finishEquation(math: MathItem<any, any, any>) {
    if (this.redo) {
      math.inputData.recompile = {
        state: math.state(),
        counter: this.allCounter,
      };
    }
    if (!this.refUpdate) {
      this.allCounter = this.counter;
    }
    Object.assign(this.allIds, this.ids);
    Object.assign(this.allLabels, this.labels);
  }

  /**
   * @override
   */
  public finalize(node: MmlNode, env: EnvList): MmlNode {
    if (!env.display || this.currentTag.env || this.currentTag.tag == null) {
      return node;
    }
    const tag = this.makeTag();
    const table = this.enTag(node, tag);
    return table;
  }

  /**
   * @override
   */
  public enTag = function (node: MmlNode, tag: MmlNode): MmlNode {
    const nf = this.configuration.nodeFactory;
    const cell = nf.create('node', 'mtd', [node]);
    const row = nf.create('node', 'mlabeledtr', [tag, cell]);
    const table = nf.create('node', 'mtable', [row], {
      side: this.configuration.options['tagSide'],
      minlabelspacing: this.configuration.options['tagIndent'],
      displaystyle: true,
    });
    return table;
  };

  /**
   * Sets the tag id.
   */
  private makeId() {
    this.currentTag.tagId = this.formatId(
      this.configuration.options['useLabelIds']
        ? this.label || this.currentTag.tag
        : this.currentTag.tag
    );
  }

  /**
   * @returns {MmlNode} The actual tag node as an mtd.
   */
  private makeTag(): MmlNode {
    this.makeId();
    if (this.label) {
      this.labels[this.label] = new Label(
        this.currentTag.tag,
        this.currentTag.tagId
      );
      this.label = '';
    }
    const format = this.currentTag.tagFormat;
    const tag = Array.isArray(format)
      ? format
      : format.match(/^(\(|\[|\{)(.*)(\}|\]|\))$/)?.slice(1) || [format];
    const mml = new TexParser(
      tag.map((part) => (part ? `\\text{${part}}` : '')).join(''),
      {},
      this.configuration
    ).mml();
    return this.configuration.nodeFactory.create('node', 'mtd', [mml], {
      id: this.currentTag.tagId,
      rowalign: this.configuration.options.tagAlign,
    });
  }
}

/**
 * No tags, except where explicitly set.
 *
 * @class
 * @augments {AbstractTags}
 */
export class NoTags extends AbstractTags {
  /**
   * @override
   */
  public autoTag() {}

  /**
   * @override
   */
  public getTag() {
    return !this.currentTag.tag ? null : super.getTag();
  }
}

/**
 * Tags every display formula. Exceptions are: Environments that explicitly
 * disallow tags, e.g., equation*.
 *
 * @class
 * @augments {AbstractTags}
 */
export class AllTags extends AbstractTags {
  /**
   * @override
   */
  public finalize(node: MmlNode, env: EnvList) {
    if (
      !env.display ||
      this.history.find(function (x: TagInfo) {
        return x.taggable;
      })
    ) {
      return node;
    }
    const tag = this.getTag(true);
    return this.enTag(node, tag);
  }
}

/**
 * Class interface for factory.
 *
 * @interface
 */
export interface TagsClass {
  new (): Tags;
}

const tagsMapping = new Map<string, TagsClass>([
  ['none', NoTags],
  ['all', AllTags],
]);

let defaultTags = 'none';

export const TagsFactory = {
  /**
   * The default options for tagging
   */
  OPTIONS: {
    // Tagging style, used to be autonumber in v2.
    tags: defaultTags,
    // This specifies the side on which \tag{} macros will place the tags.
    // Set to 'left' to place on the left-hand side.
    tagSide: 'right',
    // This is the amount of indentation (from right or left) for the tags.
    tagIndent: '0.8em',
    // make element ID's use \label name rather than equation number
    // MJ puts in an equation prefix: mjx-eqn
    // When true it uses the label name XXX as mjx-eqn:XXX
    // If false it uses the actual number N that is displayed: mjx-eqn:N
    useLabelIds: true,
    // Set to true in order to prevent error messages for duplicate label ids
    ignoreDuplicateLabels: false,
    // The rowalign value to use for tag cells.
    tagAlign: 'baseline',
  },

  /**
   * Add a tagging object.
   *
   * @param {string} name Name of the tagging object.
   * @param {TagsClass} constr The class of the Tagging object.
   */
  add(name: string, constr: TagsClass) {
    tagsMapping.set(name, constr);
  },

  /**
   * Adds a list of tagging objects to the factory.
   *
   * @param {{[name: string]: TagsClass}} tags The list of tagging objects.
   */
  addTags(tags: { [name: string]: TagsClass }) {
    for (const key of Object.keys(tags)) {
      TagsFactory.add(key, tags[key]);
    }
  },

  /**
   * Creates a new tagging object.
   *
   * @param {string} name The name of the tagging object.
   * @returns {Tags} The newly created object.
   */
  create(name: string): Tags {
    const constr = tagsMapping.get(name) || tagsMapping.get(defaultTags);
    if (!constr) {
      throw Error('Unknown tags class');
    }
    return new constr();
  },

  /**
   * Set the name of the default tagging object.
   *
   * @param {string} name The default.
   */
  setDefault(name: string) {
    defaultTags = name;
  },

  /**
   * @returns {Tags} The default tagging object.
   */
  getDefault(): Tags {
    return TagsFactory.create(defaultTags);
  },
};
