/*************************************************************
 *
 *  Copyright (c) 2017-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  Implements the interface and abstract class for MathDocument objects
 *
 * @author dpvc@mathjax.org (Davide Cervone)
 */

import {
  userOptions,
  defaultOptions,
  OptionList,
  expandable,
} from '../util/Options.js';
import { InputJax, AbstractInputJax } from './InputJax.js';
import { OutputJax, AbstractOutputJax } from './OutputJax.js';
import { MathList, AbstractMathList } from './MathList.js';
import { MathItem, AbstractMathItem, STATE } from './MathItem.js';
import { MmlNode, TextNode } from './MmlTree/MmlNode.js';
import { MmlFactory } from '../core/MmlTree/MmlFactory.js';
import { DOMAdaptor } from '../core/DOMAdaptor.js';
import { BitField, BitFieldClass } from '../util/BitField.js';
import { PrioritizedList } from '../util/PrioritizedList.js';
import { handleRetriesFor } from '../util/Retries.js';

/*****************************************************************/

/**
 * A function to call while rendering a document (usually calls a MathDocument method)
 *
 * @template N  The HTMLElement node class
 * @template T  The Text node class
 * @template D  The Document class
 */
export type RenderDoc<N, T, D> = (document: MathDocument<N, T, D>) => boolean;

/**
 * A function to call while rendering a MathItem (usually calls one of its methods)
 *
 * @template N  The HTMLElement node class
 * @template T  The Text node class
 * @template D  The Document class
 */
export type RenderMath<N, T, D> = (
  math: MathItem<N, T, D>,
  document: MathDocument<N, T, D>
) => boolean;

/**
 * The data for an action to perform during rendering or conversion
 *
 * @template N  The HTMLElement node class
 * @template T  The Text node class
 * @template D  The Document class
 */
/* prettier-ignore */
export type RenderData<N, T, D> = {
  id: string,                           //  The name for the action
  renderDoc: RenderDoc<N, T, D>,        //  The action to take during a render() call
  renderMath: RenderMath<N, T, D>,      //  The action to take during a rerender() or convert() call
  convert: boolean                      //  Whether the action is to be used during convert()
};

/**
 * The data used to define a render action in configurations and options objects
 *   (the key is used as the id, the number in the data below is the priority, and
 *    the remainind data is as described below; if no boolean is given, convert = true
 *    by default)
 *
 * @template N  The HTMLElement node class
 * @template T  The Text node class
 * @template D  The Document class
 */
/* prettier-ignore */
export type RenderAction<N, T, D> =
  [number] |                                                     // id (i.e., key) is method name to use
  [number, string] |                                             // string is method to call
  [number, string, string] |                                     // the strings are methods names for doc and math
  [number, RenderDoc<N, T, D>, RenderMath<N, T, D>] |            // explicit functions for doc and math
  [number, boolean] |                                            // same as first above, with boolean for convert
  [number, string, boolean] |                                    // same as second above, with boolean for convert
  [number, string, string, boolean] |                            // same as third above, with boolean for convert
  [number, RenderDoc<N, T, D>, RenderMath<N, T, D>, boolean]; // same as forth above, with boolean for convert

/**
 * An object representing a collection of rendering actions (id's tied to priority-and-method data)
 *
 * @template N  The HTMLElement node class
 * @template T  The Text node class
 * @template D  The Document class
 */
export type RenderActions<N, T, D> = { [id: string]: RenderAction<N, T, D> };

/**
 * Implements a prioritized list of render actions.  Extensions can add actions to the list
 *   to make it easy to extend the normal typesetting and conversion operations.
 *
 * @template N  The HTMLElement node class
 * @template T  The Text node class
 * @template D  The Document class
 */
export class RenderList<N, T, D> extends PrioritizedList<RenderData<N, T, D>> {
  /**
   * Creates a new RenderList from an initial list of rendering actions
   *
   * @param {RenderActions} actions The list of actions to take during render(), rerender(), and convert() calls
   * @returns {RenderList}    The newly created prioritied list
   */
  public static create<N, T, D>(
    actions: RenderActions<N, T, D>
  ): RenderList<N, T, D> {
    const list = new this<N, T, D>();
    for (const id of Object.keys(actions)) {
      const [action, priority] = this.action<N, T, D>(id, actions[id]);
      if (priority) {
        list.add(action, priority);
      }
    }
    return list;
  }

  /**
   * Parses a RenderAction to produce the correspinding RenderData item
   *  (e.g., turn method names into actual functions that call the method)
   *
   * @param {string} id               The id of the action
   * @param {RenderAction} action     The RenderAction defining the action
   * @returns {[RenderData,number]}   The corresponding RenderData definition for the action and its priority
   */
  public static action<N, T, D>(
    id: string,
    action: RenderAction<N, T, D>
  ): [RenderData<N, T, D>, number] {
    let renderDoc, renderMath;
    let convert = true;
    const priority = action[0];
    if (action.length === 1 || typeof action[1] === 'boolean') {
      if (action.length === 2) {
        convert = action[1] as boolean;
      }
      [renderDoc, renderMath] = this.methodActions(id);
    } else if (typeof action[1] === 'string') {
      if (typeof action[2] === 'string') {
        if (action.length === 4) {
          convert = action[3] as boolean;
        }
        const [method1, method2] = action.slice(1) as [string, string];
        [renderDoc, renderMath] = this.methodActions(method1, method2);
      } else {
        if (action.length === 3) {
          convert = action[2] as boolean;
        }
        [renderDoc, renderMath] = this.methodActions(action[1] as string);
      }
    } else {
      if (action.length === 4) {
        convert = action[3] as boolean;
      }
      [renderDoc, renderMath] = action.slice(1) as [
        RenderDoc<N, T, D>,
        RenderMath<N, T, D>,
      ];
    }
    return [
      { id, renderDoc, renderMath, convert } as RenderData<N, T, D>,
      priority,
    ];
  }

  /**
   * Produces the doc and math actions for the given method name(s)
   *   (a blank name is a no-op)
   *
   * @param {string} method1    The method to use for the render() call
   * @param {string} method2    The method to use for the rerender() and convert() calls
   *
   * @returns {[(document: any) => boolean, (math: any, document: any) => boolean]}
   *     Two render action methods wrapping the parameter methods.
   */
  protected static methodActions(
    method1: string,
    method2: string = method1
  ): [(document: any) => boolean, (math: any, document: any) => boolean] {
    return [
      (document: any) => {
        if (method1) {
          document[method1]();
        }
        return false;
      },
      (math: any, document: any) => {
        if (method2) {
          math[method2](document);
        }
        return false;
      },
    ];
  }

  /**
   * Perform the document-level rendering functions
   *
   * @param {MathDocument} document   The MathDocument whose methods are to be called
   * @param {number=} start           The state at which to start rendering (default is UNPROCESSED)
   */
  public renderDoc(
    document: MathDocument<N, T, D>,
    start: number = STATE.UNPROCESSED
  ) {
    for (const item of this.items) {
      if (item.priority >= start) {
        if (item.item.renderDoc(document)) return;
      }
    }
  }

  /**
   * Perform the MathItem-level rendering functions
   *
   * @param {MathItem} math           The MathItem whose methods are to be called
   * @param {MathDocument} document   The MathDocument to pass to the MathItem methods
   * @param {number=} start           The state at which to start rendering (default is UNPROCESSED)
   */
  public renderMath(
    math: MathItem<N, T, D>,
    document: MathDocument<N, T, D>,
    start: number = STATE.UNPROCESSED
  ) {
    for (const item of this.items) {
      if (item.priority >= start) {
        if (item.item.renderMath(math, document)) return;
      }
    }
  }

  /**
   * Perform the MathItem-level conversion functions
   *
   * @param {MathItem} math           The MathItem whose methods are to be called
   * @param {MathDocument} document   The MathDocument to pass to the MathItem methods
   * @param {number=} end             The state at which to end rendering (default is LAST)
   */
  public renderConvert(
    math: MathItem<N, T, D>,
    document: MathDocument<N, T, D>,
    end: number = STATE.LAST
  ) {
    for (const item of this.items) {
      if (item.priority > end) return;
      if (item.item.convert) {
        if (item.item.renderMath(math, document)) return;
      }
    }
  }

  /**
   * Find an entry in the list with a given ID
   *
   * @param {string} id            The id to search for
   * @returns {RenderData|null}   The data for the given id, if found, or null
   */
  public findID(id: string): RenderData<N, T, D> | null {
    for (const item of this.items) {
      if (item.item.id === id) {
        return item.item;
      }
    }
    return null;
  }
}

/*****************************************************************/

/**
 * The ways of specifying a container (a selector string, an actual node,
 * or an array of those (e.g., the result of document.getElementsByTagName())
 *
 * @template N  The HTMLElement node class
 */
export type ContainerList<N> = string | N | (string | N | N[])[];

/**
 * The options allowed for the reset() method
 */
export type ResetList = {
  all?: boolean;
  processed?: boolean;
  inputJax?: any[];
  outputJax?: any[];
};

/**
 * The default option list for the reset() method
 */
export const resetOptions: ResetList = {
  all: false,
  processed: false,
  inputJax: null,
  outputJax: null,
};

/**
 * The option list for when all options are to be reset
 */
export const resetAllOptions: ResetList = {
  all: true,
  processed: true,
  inputJax: [],
  outputJax: [],
};

/*****************************************************************/
/**
 *  The MathDocument interface
 *
 *  The MathDocument is created by MathJax.Document() and holds the
 *  document, the math found in it, and so on.  The methods of the
 *  MathDocument all return the MathDocument itself, so you can
 *  chain the method calls.  E.g.,
 *
 *    const html = MathJax.Document('<html>...</html>');
 *    html.findMath()
 *        .compile()
 *        .getMetrics()
 *        .typeset()
 *        .updateDocument();
 *
 *  The MathDocument is the main interface for page authors to
 *  interact with MathJax.
 *
 * @template N  The HTMLElement node class
 * @template T  The Text node class
 * @template D  The Document class
 */
export interface MathDocument<N, T, D> {
  /**
   * The document being processed (e.g., DOM document, or Markdown string)
   */
  document: D;

  /**
   * The kind of MathDocument (e.g., "HTML")
   */
  kind: string;

  /**
   * The options for the document
   */
  options: OptionList;

  /**
   * The list of MathItems found in this page
   */
  math: MathList<N, T, D>;

  /**
   * The list of actions to take during a render() or convert() call
   */
  renderActions: RenderList<N, T, D>;

  /**
   * This object tracks what operations have been performed, so that (when
   *  asynchronous operations are used), the ones that have already been
   *  completed won't be performed again.
   */
  processed: BitField;

  /**
   * An array of input jax to run on the document
   */
  inputJax: InputJax<N, T, D>[];

  /**
   * The output jax to use for the document
   */
  outputJax: OutputJax<N, T, D>;

  /**
   * The DOM adaptor to use for input and output
   */
  adaptor: DOMAdaptor<N, T, D>;

  /**
   * The MmlFactory to be used for input jax and error processing
   */
  mmlFactory: MmlFactory;

  /**
   * @param {string} id      The id of the action to add
   * @param {any[]} action   The RenderAction to take
   */
  addRenderAction(id: string, ...action: any[]): void;

  /**
   * @param {string} id   The id of the action to remove
   */
  removeRenderAction(id: string): void;

  /**
   * Perform the renderActions on the document
   *
   * @returns {MathDocument}    The math document instance
   */
  render(): MathDocument<N, T, D>;

  /**
   * Perform the renderActions on the document with retry handling
   *
   * @returns {Promise<MathDocument>}   A promise that resolves when the render is complete
   */
  renderPromise(): Promise<MathDocument<N, T, D>>;

  /**
   * Rerender the MathItems on the page
   *
   * @param {number} start      The state to start rerendering at
   * @returns {MathDocument}    The math document instance
   */
  rerender(start?: number): MathDocument<N, T, D>;

  /**
   * Rerender the MathItems on the page
   *
   * @param {number} start              The state to start rerendering at
   * @returns {Promise<MathDocument>}   A promise that resolves when the rerender is complete
   */
  rerenderPromise(start?: number): Promise<MathDocument<N, T, D>>;

  /**
   * Convert a math string to the document's output format
   *
   * @param {string} math           The math string to convert
   * @param {OptionList} options    The options for the conversion (e.g., format, ex, em, etc.)
   * @returns {MmlNode|N}           The MmlNode or N node for the converted content
   */
  convert(math: string, options?: OptionList): MmlNode | N;

  /**
   * Convert a math string to the document's output format
   *
   * @param {string} math           The math string to convert
   * @param {OptionList} options    The options for the conversion (e.g., format, ex, em, etc.)
   * @returns {Promise<MmlNode|N>}  A promise that resolves when the conversion is complete
   */
  convertPromise(math: string, options?: OptionList): Promise<MmlNode | N>;

  /**
   * Perform an action when previous actions are complete.
   * (Used to chain promise-based typeset and conversion actions.)
   */
  whenReady(action: () => any): Promise<any>;

  /**
   * Return a promise that resolves when all of the  action promises have been resolved
   */
  actionPromises(): Promise<any[]>;

  /**
   * Clear the action promises
   */
  clearPromises(): void;

  /**
   * Save a promise in the action romises list
   */
  savePromise(promise: Promise<any>): void;

  /**
   * Locates the math in the document and constructs the MathList
   *  for the document.
   *
   * @param {OptionList} options  The options for locating the math
   * @returns {MathDocument}      The math document instance
   */
  findMath(options?: OptionList): MathDocument<N, T, D>;

  /**
   * Calls the input jax to process the MathItems in the MathList
   *
   * @returns {MathDocument}  The math document instance
   */
  compile(): MathDocument<N, T, D>;

  /**
   * Gets the metric information for the MathItems
   *
   * @returns {MathDocument}  The math document instance
   */
  getMetrics(): MathDocument<N, T, D>;

  /**
   * Calls the output jax to process the compiled math in the MathList
   *
   * @returns {MathDocument}  The math document instance
   */
  typeset(): MathDocument<N, T, D>;

  /**
   * Updates the document to include the typeset math
   *
   * @returns {MathDocument}  The math document instance
   */
  updateDocument(): MathDocument<N, T, D>;

  /**
   * Removes the typeset math from the document
   *
   * @param {boolean} restore  True if the original math should be put
   *                            back into the document as well
   * @returns {MathDocument}    The math document instance
   */
  removeFromDocument(restore?: boolean): MathDocument<N, T, D>;

  /**
   * Set the state of the document (allowing you to roll back
   *  the state to a previous one, if needed).
   *
   * @param {number} state     The new state of the document
   * @param {boolean} restore  True if the original math should be put
   *                            back into the document during the rollback
   * @returns {MathDocument}    The math document instance
   */
  state(state: number, restore?: boolean): MathDocument<N, T, D>;

  /**
   * Clear the processed values so that the document can be reprocessed
   *
   * @param {ResetList} options   The things to be reset
   * @returns {MathDocument}       The math document instance
   */
  reset(options?: ResetList): MathDocument<N, T, D>;

  /**
   * Reset the processed values and clear the MathList (so that new math
   * can be processed in the document).
   *
   * @returns {MathDocument}  The math document instance
   */
  clear(): MathDocument<N, T, D>;

  /**
   * Indicate that the MathDocument is no longer needed.
   */
  done(): Promise<void>;

  /**
   * Merges a MathList into the list for this document.
   *
   * @param {MathList} list   The MathList to be merged into this document's list
   * @returns {MathDocument}   The math document instance
   */
  concat(list: MathList<N, T, D>): MathDocument<N, T, D>;

  /**
   * Clear the typeset MathItems that are within the given container
   *   from the document's MathList.  (E.g., when the content of the
   *   container has been updated and you want to remove the
   *   associated MathItems)
   *
   * @param {ContainerList<N>} containers  The container DOM elements whose math items are to be removed
   * @returns {MathItem<N,T,D>[]}          The removed MathItems
   */
  clearMathItemsWithin(containers: ContainerList<N>): MathItem<N, T, D>[];

  /**
   * Get the typeset MathItems that are within a given container.
   *
   * @param {ContainerList<N>} elements   The container DOM elements whose math items are to be found
   * @returns {MathItem<N,T,D>[]}          The list of MathItems within that container
   */
  getMathItemsWithin(elements: ContainerList<N>): MathItem<N, T, D>[];
}

/*****************************************************************/

/**
 * Defaults used when input jax isn't specified
 *
 * @template N  The HTMLElement node class
 * @template T  The Text node class
 * @template D  The Document class
 */
class DefaultInputJax<N, T, D> extends AbstractInputJax<N, T, D> {
  /**
   * @override
   */
  public compile(_math: MathItem<N, T, D>) {
    return null as MmlNode;
  }
}

/**
 * Defaults used when ouput jax isn't specified
 *
 * @template N  The HTMLElement node class
 * @template T  The Text node class
 * @template D  The Document class
 */
class DefaultOutputJax<N, T, D> extends AbstractOutputJax<N, T, D> {
  /**
   * @override
   */
  public typeset(
    _math: MathItem<N, T, D>,
    _document: MathDocument<N, T, D> = null
  ) {
    return null as N;
  }
  /**
   * @override
   */
  public escaped(_math: MathItem<N, T, D>, _document?: MathDocument<N, T, D>) {
    return null as N;
  }
}

/**
 * Default for the MathList when one isn't specified
 *
 * @template N  The HTMLElement node class
 * @template T  The Text node class
 * @template D  The Document class
 */
class DefaultMathList<N, T, D> extends AbstractMathList<N, T, D> {}

/**
 * Default for the Mathitem when one isn't specified
 *
 * @template N  The HTMLElement node class
 * @template T  The Text node class
 * @template D  The Document class
 */
class DefaultMathItem<N, T, D> extends AbstractMathItem<N, T, D> {}

/*****************************************************************/
/**
 *  Implements the abstract MathDocument class
 *
 * @template N  The HTMLElement node class
 * @template T  The Text node class
 * @template D  The Document class
 */
export abstract class AbstractMathDocument<N, T, D> implements MathDocument<
  N,
  T,
  D
> {
  /**
   * The type of MathDocument
   */
  public static KIND: string = 'MathDocument';

  /**
   * The default options for the document
   */
  public static OPTIONS: OptionList = {
    OutputJax: null, // instance of an OutputJax for the document
    InputJax: null, // instance of an InputJax or an array of them
    MmlFactory: null, // instance of a MmlFactory for this document
    MathList: DefaultMathList, // constructor for a MathList to use for the document
    MathItem: DefaultMathItem, // constructor for a MathItem to use for the MathList
    compileError: (
      doc: AbstractMathDocument<any, any, any>,
      math: MathItem<any, any, any>,
      err: Error
    ) => {
      doc.compileError(math, err);
    },
    typesetError: (
      doc: AbstractMathDocument<any, any, any>,
      math: MathItem<any, any, any>,
      err: Error
    ) => {
      doc.typesetError(math, err);
    },
    renderActions: expandable({
      find: [STATE.FINDMATH, 'findMath', '', false],
      compile: [STATE.COMPILED],
      metrics: [STATE.METRICS, 'getMetrics', '', false],
      typeset: [STATE.TYPESET],
      update: [STATE.INSERTED, 'updateDocument', false],
    }) as RenderActions<any, any, any>,
  };

  /**
   * A bit-field for the actions that have been processed
   */
  public static ProcessBits = BitFieldClass(
    'findMath',
    'compile',
    'getMetrics',
    'typeset',
    'updateDocument'
  );

  /**
   * The document managed by this MathDocument
   */
  public document: D;
  /**
   * The actual options for this document (with user-supplied ones merged in)
   */
  public options: OptionList;

  /**
   * The list of MathItems for this document
   */
  public math: MathList<N, T, D>;

  /**
   * The list of render actions
   */
  public renderActions: RenderList<N, T, D>;

  /**
   * The render action promise list
   */
  protected _actionPromises: Promise<void>[];

  /**
   * Promise for the current typeset or conversion action
   * (used to chain the promise-based calls so they don't
   * overlap).
   */
  protected _readyPromise: Promise<any>;

  /**
   * The bit-field used to tell what steps have been taken on the document (for retries)
   */
  public processed: BitField;

  /**
   * The list of input jax for the document
   */
  public inputJax: InputJax<N, T, D>[];

  /**
   * The output jax for the document
   */
  public outputJax: OutputJax<N, T, D>;

  /**
   * The DOM adaptor for the document
   */
  public adaptor: DOMAdaptor<N, T, D>;

  /**
   * The MathML node factory for the internal MathML representation
   */
  public mmlFactory: MmlFactory;

  /**
   * @param {any} document           The document (HTML string, parsed DOM, etc.) to be processed
   * @param {DOMAdaptor} adaptor     The DOM adaptor for this document
   * @param {OptionList} options     The options for this document
   * @class
   */
  constructor(document: D, adaptor: DOMAdaptor<N, T, D>, options: OptionList) {
    const CLASS = this.constructor as typeof AbstractMathDocument;
    this.document = document;
    this.options = userOptions(defaultOptions({}, CLASS.OPTIONS), options);
    this.math = new (this.options['MathList'] || DefaultMathList)();
    this.renderActions = RenderList.create<N, T, D>(
      this.options['renderActions']
    );
    this._actionPromises = [];
    this._readyPromise = Promise.resolve();
    this.processed = new AbstractMathDocument.ProcessBits();
    this.outputJax =
      this.options['OutputJax'] || new DefaultOutputJax<N, T, D>();
    let inputJax = this.options['InputJax'] || [new DefaultInputJax<N, T, D>()];
    if (!Array.isArray(inputJax)) {
      inputJax = [inputJax];
    }
    this.inputJax = inputJax;
    //
    // Pass the DOM adaptor to the jax
    //
    this.adaptor = adaptor;
    this.outputJax.setAdaptor(adaptor);
    this.inputJax.map((jax) => jax.setAdaptor(adaptor));
    //
    // Pass the MmlFactory to the jax
    //
    this.mmlFactory = this.options['MmlFactory'] || new MmlFactory();
    this.inputJax.map((jax) => jax.setMmlFactory(this.mmlFactory));
    //
    // Do any initialization that requires adaptors or factories
    //
    this.outputJax.initialize();
    this.inputJax.map((jax) => jax.initialize());
  }

  /**
   * @returns {string}  The kind of document
   */
  public get kind(): string {
    return (this.constructor as typeof AbstractMathDocument).KIND;
  }

  /**
   * @override
   */
  public addRenderAction(id: string, ...action: any[]) {
    const [fn, p] = RenderList.action<N, T, D>(
      id,
      action as RenderAction<N, T, D>
    );
    this.renderActions.add(fn, p);
  }

  /**
   * @override
   */
  public removeRenderAction(id: string) {
    const action = this.renderActions.findID(id);
    if (action) {
      this.renderActions.remove(action);
    }
  }

  /**
   * @override
   */
  public render() {
    this.clearPromises();
    this.renderActions.renderDoc(this);
    return this;
  }

  /**
   * @override
   */
  public renderPromise() {
    return this.whenReady(() =>
      handleRetriesFor(async () => {
        this.render();
        await this.actionPromises();
        this.clearPromises();
        return this;
      })
    );
  }

  /**
   * @override
   */
  public rerender(start: number = STATE.RERENDER) {
    this.state(start - 1);
    this.render();
    return this;
  }

  /**
   * @override
   */
  public rerenderPromise(start: number = STATE.RERENDER) {
    return this.whenReady(() =>
      handleRetriesFor(async () => {
        this.rerender(start);
        await this.actionPromises();
        this.clearPromises();
        return this;
      })
    );
  }

  /**
   * @override
   */
  public convert(math: string, options: OptionList = {}) {
    let { format, display, end, ex, em, containerWidth, scale, family } =
      userOptions(
        {
          format: this.inputJax[0].name,
          display: true,
          end: STATE.LAST,
          em: 16,
          ex: 8,
          containerWidth: null,
          scale: 1,
          family: '',
        },
        options
      );
    if (containerWidth === null) {
      containerWidth = 80 * ex;
    }
    const jax = this.inputJax.reduce(
      (jax, ijax) => (ijax.name === format ? ijax : jax),
      null
    );
    const mitem = new this.options.MathItem(math, jax, display);
    mitem.start.node = this.adaptor.body(this.document);
    mitem.setMetrics(em, ex, containerWidth, scale);
    if (family && this.outputJax.options.mtextInheritFont) {
      mitem.outputData.mtextFamily = family;
    }
    if (family && this.outputJax.options.merrorInheritFont) {
      mitem.outputData.merrorFamily = family;
    }
    this.clearPromises();
    mitem.convert(this, end);
    return mitem.typesetRoot || mitem.root;
  }

  /**
   * @override
   */
  public convertPromise(math: string, options: OptionList = {}) {
    return this.whenReady(() =>
      handleRetriesFor(async () => {
        const node = this.convert(math, options);
        await this.actionPromises();
        this.clearPromises();
        return node;
      })
    );
  }

  /**
   * @override
   */
  public whenReady(action: () => any): Promise<any> {
    return (this._readyPromise = this._readyPromise
      .catch((_) => {})
      .then(() => {
        //
        // Cache old _readyPromise and replace it with a resolved
        // promise in case action() calls whenReady(), so we don't get
        // a circular dependency where the action is waiting on itself.
        //
        const ready = this._readyPromise;
        this._readyPromise = Promise.resolve();
        //
        // Do the action and save its result.
        //
        const result = action();
        //
        // Get a promise that returns the result after
        // any new _readyPromise resolves (in case action
        // called whenReady() or another function that does).
        //
        const promise = this._readyPromise.then(() => result);
        //
        // Put back the original promise.
        //
        this._readyPromise = ready;
        //
        // Return promise that returns the result.  The original
        // _readyPromise will wait on it to complete before it resolves,
        // since promises that return promises automatically chain.
        // This inserts any new _readyPromise promises into the
        // original _readyPromise chain at this point.
        //
        return promise;
      }));
  }

  /**
   * @override
   */
  public actionPromises() {
    return Promise.all(this._actionPromises);
  }

  /**
   * @override
   */
  public clearPromises() {
    this._actionPromises = [];
  }

  /**
   * @override
   */
  public savePromise(promise: Promise<any>) {
    this._actionPromises.push(promise);
  }

  /**
   * @override
   */
  public findMath(_options: OptionList = null) {
    this.processed.set('findMath');
    return this;
  }

  /**
   * @override
   */
  public compile() {
    if (!this.processed.isSet('compile')) {
      //
      //  Compile all the math in the list
      //
      const recompile = [];
      for (const math of this.math) {
        this.compileMath(math);
        if (math.inputData.recompile !== undefined) {
          recompile.push(math);
        }
      }
      //
      //  If any were added to the recompile list,
      //    compile them again
      //
      for (const math of recompile) {
        const data = math.inputData.recompile;
        math.state(data.state);
        math.inputData.recompile = data;
        this.compileMath(math);
      }
      this.processed.set('compile');
    }
    return this;
  }

  /**
   * @param {MathItem} math   The item to compile
   */
  protected compileMath(math: MathItem<N, T, D>) {
    try {
      math.compile(this);
    } catch (err) {
      if (err.retry || err.restart) {
        throw err;
      }
      this.options['compileError'](this, math, err);
      math.inputData['error'] = err;
    }
  }

  /**
   * Produce an error using MmlNodes
   *
   * @param {MathItem} math  The MathItem producing the error
   * @param {Error} err      The Error object for the error
   */
  public compileError(math: MathItem<N, T, D>, err: Error) {
    math.root = this.mmlFactory.create('math', null, [
      this.mmlFactory.create(
        'merror',
        { 'data-mjx-error': err.message, title: err.message },
        [
          this.mmlFactory.create('mtext', null, [
            (this.mmlFactory.create('text') as TextNode).setText(
              'Math input error'
            ),
          ]),
        ]
      ),
    ]);
    if (math.display) {
      math.root.attributes.set('display', 'block');
    }
    math.inputData.error = err.message;
  }

  /**
   * @override
   */
  public typeset() {
    if (!this.processed.isSet('typeset')) {
      for (const math of this.math) {
        try {
          math.typeset(this);
        } catch (err) {
          if (err.retry || err.restart) {
            throw err;
          }
          this.options['typesetError'](this, math, err);
          math.outputData['error'] = err;
        }
      }
      this.processed.set('typeset');
    }
    return this;
  }

  /**
   * Produce an error using HTML
   *
   * @param {MathItem} math  The MathItem producing the error
   * @param {Error} err      The Error object for the error
   */
  public typesetError(math: MathItem<N, T, D>, err: Error) {
    math.typesetRoot = this.adaptor.node(
      'mjx-container',
      {
        class: 'MathJax mjx-output-error',
        jax: this.outputJax.name,
      },
      [
        this.adaptor.node(
          'span',
          {
            'data-mjx-error': err.message,
            title: err.message,
            style: {
              color: 'red',
              'background-color': 'yellow',
              'line-height': 'normal',
            },
          },
          [this.adaptor.text('Math output error')]
        ),
      ]
    );
    if (math.display) {
      this.adaptor.setAttributes(math.typesetRoot, {
        style: {
          display: 'block',
          margin: '1em 0',
          'text-align': 'center',
        },
      });
    }
    math.outputData.error = err.message;
  }

  /**
   * @override
   */
  public getMetrics() {
    if (!this.processed.isSet('getMetrics')) {
      this.outputJax.getMetrics(this);
      this.processed.set('getMetrics');
    }
    return this;
  }

  /**
   * @override
   */
  public updateDocument() {
    if (!this.processed.isSet('updateDocument')) {
      for (const math of this.math.reversed()) {
        math.updateDocument(this);
      }
      this.processed.set('updateDocument');
    }
    return this;
  }

  /**
   * @override
   */
  public removeFromDocument(_restore: boolean = false) {
    return this;
  }

  /**
   * @override
   */
  public state(state: number, restore: boolean = false) {
    for (const math of this.math) {
      math.state(state, restore);
    }
    if (state < STATE.INSERTED) {
      this.processed.clear('updateDocument');
    }
    if (state < STATE.TYPESET) {
      this.processed.clear('typeset');
      this.processed.clear('getMetrics');
    }
    if (state < STATE.COMPILED) {
      this.processed.clear('compile');
    }
    if (state < STATE.FINDMATH) {
      this.processed.clear('findMath');
    }
    return this;
  }

  /**
   * @override
   */
  public reset(options: ResetList = { processed: true }) {
    options = userOptions(Object.assign({}, resetOptions), options);
    if (options.all) {
      Object.assign(options, resetAllOptions);
    }
    if (options.processed) {
      this.processed.reset();
    }
    if (options.inputJax) {
      this.inputJax.forEach((jax) => jax.reset(...options.inputJax));
    }
    if (options.outputJax) {
      this.outputJax.reset(...options.outputJax);
    }
    return this;
  }

  /**
   * @override
   */
  public clear() {
    this.reset();
    this.math.clear();
    return this;
  }

  /**
   * @override
   */
  public done() {
    return Promise.resolve();
  }

  /**
   * @override
   */
  public concat(list: MathList<N, T, D>) {
    this.math.merge(list);
    return this;
  }

  /**
   * @override
   */
  public clearMathItemsWithin(containers: ContainerList<N>) {
    const items = this.getMathItemsWithin(containers);
    for (const item of items.slice(0).reverse()) {
      item.clear();
    }
    this.math.remove(...items);
    return items;
  }

  /**
   * @override
   */
  public getMathItemsWithin(elements: ContainerList<N>) {
    if (!Array.isArray(elements)) {
      elements = [elements];
    }
    const adaptor = this.adaptor;
    const items = [] as MathItem<N, T, D>[];
    const containers = adaptor.getElements(elements, this.document);
    ITEMS: for (const item of this.math) {
      for (const container of containers) {
        if (item.start.node && adaptor.contains(container, item.start.node)) {
          items.push(item);
          continue ITEMS;
        }
      }
    }
    return items;
  }
}

/**
 * The constructor type for a MathDocument
 *
 * @template D    The MathDocument type this constructor is for
 */
export interface MathDocumentConstructor<
  D extends MathDocument<any, any, any>,
> {
  KIND: string;
  OPTIONS: OptionList;
  ProcessBits: typeof BitField;
  new (...args: any[]): D;
}
