/*************************************************************
 *
 *  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 Mappings for TeX parsing for the bussproofs package.
 *
 * @author v.sorge@mathjax.org (Volker Sorge)
 */

import { ParseMethod } from '../Types.js';
import TexError from '../TexError.js';
import TexParser from '../TexParser.js';
import { ParseUtil } from '../ParseUtil.js';
import { UnitUtil } from '../UnitUtil.js';
import { StackItem } from '../StackItem.js';
import { MmlNode } from '../../../core/MmlTree/MmlNode.js';
import * as BussproofsUtil from './BussproofsUtil.js';

/**
 * Pads content of an inference rule.
 *
 * @param {TexParser} parser The calling parser.
 * @param {string} content The content to be padded.
 * @returns {MmlNode} The mrow element with padded content.
 */
function paddedContent(parser: TexParser, content: string): MmlNode {
  // Add padding on either site.
  const nodes = ParseUtil.internalMath(parser, UnitUtil.trimSpaces(content), 0);
  if (!nodes[0].childNodes[0].childNodes.length) {
    return parser.create('node', 'mrow', []);
  }
  const lpad = parser.create('node', 'mspace', [], { width: '.5ex' });
  const rpad = parser.create('node', 'mspace', [], { width: '.5ex' });
  return parser.create('node', 'mrow', [lpad, ...nodes, rpad]);
}

/**
 * Creates a ND style inference rule.
 *
 * @param {TexParser} parser The calling parser.
 * @param {MmlNode} premise The premise (a single table).
 * @param {MmlNode[]} conclusions Elements that are combined into the conclusion.
 * @param {MmlNode|null} left The left label if it exists.
 * @param {MmlNode|null} right The right label if it exists.
 * @param {string} style Style of inference rule line.
 * @param {boolean} rootAtTop Direction of inference rule: true for root at top.
 * @returns {MmlNode} The create rule node.
 */
function createRule(
  parser: TexParser,
  premise: MmlNode,
  conclusions: MmlNode[],
  left: MmlNode | null,
  right: MmlNode | null,
  style: string,
  rootAtTop: boolean
): MmlNode {
  const upper = parser.create(
    'node',
    'mtr',
    [parser.create('node', 'mtd', [premise], {})],
    {}
  );
  const lower = parser.create(
    'node',
    'mtr',
    [parser.create('node', 'mtd', conclusions, {})],
    {}
  );
  let rule = parser.create(
    'node',
    'mtable',
    rootAtTop ? [lower, upper] : [upper, lower],
    { align: 'top 2', rowlines: style, framespacing: '0 0' }
  );
  BussproofsUtil.setProperty(rule, 'inferenceRule', rootAtTop ? 'up' : 'down');
  let leftLabel, rightLabel;
  if (left) {
    leftLabel = parser.create('node', 'mpadded', [left], {
      height: '.25em',
      depth: '+.25em',
      width: '+.5ex',
      voffset: '-.25em',
    });
    BussproofsUtil.setProperty(leftLabel, 'prooflabel', 'left');
  }
  if (right) {
    rightLabel = parser.create('node', 'mpadded', [right], {
      height: '-.25em',
      depth: '+.25em',
      width: '+.5ex',
      voffset: '-.25em',
      lspace: '.5ex',
    });
    BussproofsUtil.setProperty(rightLabel, 'prooflabel', 'right');
  }
  let children, label;
  if (left && right) {
    children = [leftLabel, rule, rightLabel];
    label = 'both';
  } else if (left) {
    children = [leftLabel, rule];
    label = 'left';
  } else if (right) {
    children = [rule, rightLabel];
    label = 'right';
  } else {
    return rule;
  }
  rule = parser.create('node', 'mrow', children);
  BussproofsUtil.setProperty(rule, 'labelledRule', label);
  return rule;
}

/**
 * Parses a line with a sequent (i.e., one containing \\fcenter).
 *
 * @param {TexParser} parser The current parser.
 * @param {string} name The name of the calling command.
 * @returns {MmlNode} The parsed line.
 */
function parseFCenterLine(parser: TexParser, name: string): MmlNode {
  const dollar = parser.GetNext();
  if (dollar !== '$') {
    throw new TexError(
      'IllegalUseOfCommand',
      'Use of %1 does not match its definition.',
      name
    );
  }
  parser.i++;
  const axiom = parser.GetUpTo(name, '$');
  if (!axiom.includes('\\fCenter')) {
    throw new TexError(
      'MissingProofCommand',
      'Missing %1 in %2.',
      '\\fCenter',
      name
    );
  }
  // Check for fCenter and throw error?
  const [prem, conc] = axiom.split('\\fCenter');
  const premise = new TexParser(
    prem,
    parser.stack.env,
    parser.configuration
  ).mml();
  const conclusion = new TexParser(
    conc,
    parser.stack.env,
    parser.configuration
  ).mml();
  const fcenter = new TexParser(
    '\\fCenter',
    parser.stack.env,
    parser.configuration
  ).mml();
  const left = parser.create('node', 'mtd', [premise], {});
  const middle = parser.create('node', 'mtd', [fcenter], {});
  const right = parser.create('node', 'mtd', [conclusion], {});
  const row = parser.create('node', 'mtr', [left, middle, right], {});
  const table = parser.create('node', 'mtable', [row], {
    columnspacing: '.5ex',
    columnalign: 'center 2',
  });
  BussproofsUtil.setProperty(table, 'sequent', true);
  parser.configuration.addNode('sequent', row);
  return table;
}

// Namespace
const BussproofsMethods: { [key: string]: ParseMethod } = {
  /**
   * Implements the proof tree environment.
   *
   * @param {TexParser} parser The current parser.
   * @param {StackItem} begin The opening element of the environment.
   * @returns {StackItem} The proof tree stackitem.
   */
  // TODO: Error handling if we have leftover elements or elements are not in the
  // required order.
  Prooftree(parser: TexParser, begin: StackItem): StackItem {
    parser.Push(begin);
    // TODO: Check if opening a proof tree is legal.
    const newItem = parser.itemFactory.create('proofTree').setProperties({
      name: begin.getName(),
      line: 'solid',
      currentLine: 'solid',
      rootAtTop: false,
    });
    // parser.Push(item);
    return newItem;
  },

  /**
   * Implements the Axiom command.
   *
   * @param {TexParser} parser The current parser.
   * @param {string} name The name of the command.
   */
  Axiom(parser: TexParser, name: string) {
    const top = parser.stack.Top();
    // TODO: Label error
    if (top.kind !== 'proofTree') {
      throw new TexError(
        'IllegalProofCommand',
        'Proof commands only allowed in prooftree environment.'
      );
    }
    const content = paddedContent(parser, parser.GetArgument(name));
    BussproofsUtil.setProperty(content, 'axiom', true);
    top.Push(content);
  },

  /**
   * Implements the Inference rule commands.
   *
   * @param {TexParser} parser The current parser.
   * @param {string} name The name of the command.
   * @param {number} n Number of premises for this inference rule.
   */
  Inference(parser: TexParser, name: string, n: number) {
    const top = parser.stack.Top();
    if (top.kind !== 'proofTree') {
      throw new TexError(
        'IllegalProofCommand',
        'Proof commands only allowed in prooftree environment.'
      );
    }
    if (top.Size() < n) {
      throw new TexError('BadProofTree', 'Proof tree badly specified.');
    }
    const rootAtTop = top.getProperty('rootAtTop') as boolean;
    const childCount = n === 1 && !top.Peek()[0].childNodes.length ? 0 : n;
    const children: MmlNode[] = [];
    do {
      if (children.length) {
        children.unshift(parser.create('node', 'mtd', [], {}));
      }
      children.unshift(
        parser.create('node', 'mtd', [top.Pop()], {
          rowalign: rootAtTop ? 'top' : 'bottom',
        })
      );
      n--;
    } while (n > 0);
    const row = parser.create('node', 'mtr', children, {});
    const table = parser.create('node', 'mtable', [row], {
      framespacing: '0 0',
    });
    const conclusion = paddedContent(parser, parser.GetArgument(name));
    const style = top.getProperty('currentLine') as string;
    if (style !== top.getProperty('line')) {
      top.setProperty('currentLine', top.getProperty('line'));
    }
    const rule = createRule(
      parser,
      table,
      [conclusion],
      top.getProperty('left') as MmlNode,
      top.getProperty('right') as MmlNode,
      style,
      rootAtTop
    );
    top.setProperty('left', null);
    top.setProperty('right', null);
    BussproofsUtil.setProperty(rule, 'inference', childCount);
    parser.configuration.addNode('inference', rule);
    top.Push(rule);
  },

  /**
   * Implements the label command.
   *
   * @param {TexParser} parser The current parser.
   * @param {string} name The name of the command.
   * @param {string} side The side of the label.
   */
  Label(parser: TexParser, name: string, side: string) {
    const top = parser.stack.Top();
    // Label error
    if (top.kind !== 'proofTree') {
      throw new TexError(
        'IllegalProofCommand',
        'Proof commands only allowed in prooftree environment.'
      );
    }
    const content = ParseUtil.internalMath(parser, parser.GetArgument(name), 0);
    const label =
      content.length > 1
        ? parser.create('node', 'mrow', content, {})
        : content[0];
    top.setProperty(side, label);
  },

  /**
   * Sets line style for inference rules.
   *
   * @param {TexParser} parser The current parser.
   * @param {string} _name The name of the command.
   * @param {string} style The line style to set.
   * @param {boolean} always Set as permanent style.
   */
  SetLine(parser: TexParser, _name: string, style: string, always: boolean) {
    const top = parser.stack.Top();
    // Label error
    if (top.kind !== 'proofTree') {
      throw new TexError(
        'IllegalProofCommand',
        'Proof commands only allowed in prooftree environment.'
      );
    }
    top.setProperty('currentLine', style);
    if (always) {
      top.setProperty('line', style);
    }
  },

  /**
   * Implements commands indicating where the root of the proof tree is.
   *
   * @param {TexParser} parser The current parser.
   * @param {string} _name The name of the command.
   * @param {string} where If true root is at top, otherwise at bottom.
   */
  RootAtTop(parser: TexParser, _name: string, where: boolean) {
    const top = parser.stack.Top();
    if (top.kind !== 'proofTree') {
      throw new TexError(
        'IllegalProofCommand',
        'Proof commands only allowed in prooftree environment.'
      );
    }
    top.setProperty('rootAtTop', where);
  },

  /**
   * Implements Axiom command for sequents.
   *
   * @param {TexParser} parser The current parser.
   * @param {string} name The name of the command.
   */
  AxiomF(parser: TexParser, name: string) {
    const top = parser.stack.Top();
    if (top.kind !== 'proofTree') {
      throw new TexError(
        'IllegalProofCommand',
        'Proof commands only allowed in prooftree environment.'
      );
    }
    const line = parseFCenterLine(parser, name);
    BussproofsUtil.setProperty(line, 'axiom', true);
    top.Push(line);
  },

  /**
   * Placeholder for Fcenter macro that can be overwritten with renewcommand.
   *
   * @param {TexParser} _parser The current parser.
   * @param {string} _name The name of the command.
   */
  FCenter(_parser: TexParser, _name: string) {},

  /**
   * Implements inference rules for sequents.
   *
   * @param {TexParser} parser The current parser.
   * @param {string} name The name of the command.
   * @param {number} n Number of premises for this inference rule.
   */
  InferenceF(parser: TexParser, name: string, n: number) {
    const top = parser.stack.Top();
    if (top.kind !== 'proofTree') {
      throw new TexError(
        'IllegalProofCommand',
        'Proof commands only allowed in prooftree environment.'
      );
    }
    if (top.Size() < n) {
      throw new TexError('BadProofTree', 'Proof tree badly specified.');
    }
    const rootAtTop = top.getProperty('rootAtTop') as boolean;
    const childCount = n === 1 && !top.Peek()[0].childNodes.length ? 0 : n;
    const children: MmlNode[] = [];
    do {
      if (children.length) {
        children.unshift(parser.create('node', 'mtd', [], {}));
      }
      children.unshift(
        parser.create('node', 'mtd', [top.Pop()], {
          rowalign: rootAtTop ? 'top' : 'bottom',
        })
      );
      n--;
    } while (n > 0);
    const row = parser.create('node', 'mtr', children, {});
    const table = parser.create('node', 'mtable', [row], {
      framespacing: '0 0',
    });

    const conclusion = parseFCenterLine(parser, name); // TODO: Padding
    const style = top.getProperty('currentLine') as string;
    if (style !== top.getProperty('line')) {
      top.setProperty('currentLine', top.getProperty('line'));
    }
    const rule = createRule(
      parser,
      table,
      [conclusion],
      top.getProperty('left') as MmlNode,
      top.getProperty('right') as MmlNode,
      style,
      rootAtTop
    );
    top.setProperty('left', null);
    top.setProperty('right', null);
    BussproofsUtil.setProperty(rule, 'inference', childCount);
    parser.configuration.addNode('inference', rule);
    top.Push(rule);
  },
};

export default BussproofsMethods;
