<?php

declare( strict_types = 1 );

namespace MediaWiki\Extension\Math\WikiTexVC;

use Exception;
use LogicException;
use MediaWiki\Extension\Math\WikiTexVC\Mhchem\MhchemParser;
use MediaWiki\Extension\Math\WikiTexVC\MMLmappings\Util\MMLParsingUtil;
use MediaWiki\Extension\Math\WikiTexVC\MMLmappings\Util\MMLutil;
use MediaWiki\Extension\Math\WikiTexVC\Nodes\Fun2;
use MediaWiki\Extension\Math\WikiTexVC\Nodes\TexArray;
use MediaWiki\Extension\Math\WikiTexVC\Nodes\TexNode;
use stdClass;

/**
 * A TeX/LaTeX validator and MathML converter.
 * WikiTexVC takes user input and validates it while replacing
 * MediaWiki-specific functions. The validator component is a PHP port of the JavaScript port of texvc,
 * which was originally written in Ocaml for the Math extension.
 *
 * @author Johannes Stegmüller
 */
class TexVC {
	private Parser $parser;
	private TexUtil $tu;

	private const PKGS = [
		'ams',
		'cancel',
		'color',
		'euro',
		'teubner',
		'mhchem',
		'mathoid',
		'mhchemtexified',
		'intent'
	];

	public function __construct() {
		$this->parser = new Parser();
		$this->tu = TexUtil::getInstance();
	}

	/**
	 * Usually this step is done implicitly within the check-method.
	 * @param string $input tex-string as input for the grammar
	 * @param null|array $options array options for the grammar.
	 * @return mixed output of the grammar.
	 * @throws SyntaxError when SyntaxError in the input
	 */
	public function parse( $input, $options = [] ) {
		return $this->parser->parse( $input, $options );
	}

	/** status is one character:
	 *  + : success! result is in 'output'
	 *  E : Lexer exception raised
	 *  F : TeX function not recognized
	 *  S : Parsing error
	 *  C : Content requires disabled package
	 *  I : Intent syntax error
	 *  - : Generic/Default failure code. Might be an invalid argument,
	 *      output file already exist, a problem with an external
	 *      command ...
	 * @param string|TexArray|stdClass $input tex to be checked as string,
	 * can also be the output of former parser call
	 * @param array $options array options for settings of the check
	 * @param array &$warnings reference on warnings occurring during the check
	 * @param bool $texifyMhchem create TeX for mhchem in input before checking further
	 * @return array|string[] output with information status (see above)
	 * @throws Exception in case of a major problem with the check and activated debug option.
	 */
	public function check( $input, $options = [], &$warnings = [], bool $texifyMhchem = false ) {
		$options = ParserUtil::createOptions( $options );
		try {
			$input = $this->preProcessInput( $texifyMhchem, $options, $input );
		} catch ( Exception $ex ) {
			if (
				$ex instanceof SyntaxError &&
				!$options['oldtexvc'] &&
				str_starts_with( $ex->getMessage(), 'Deprecation' )
			) {
				$warnings[] = [
					'type' => 'texvc-deprecation',
					'details' => $this->handleTexError( $ex, $options )
				];
				$options['oldtexvc'] = true;
				return $this->check( $input, $options, $warnings );
			}

			if ( $ex instanceof SyntaxError && $options['usemhchem'] && !$options['oldmhchem'] ) {
				$warnings[] = [
					'type' => 'mhchem-deprecation',
					'details' => $this->handleTexError( $ex, $options )
				];
				$options['oldmhchem'] = true;
				return $this->check( $input, $options, $warnings );
			}
			return $this->handleTexError( $ex, $options );
		}
		$output = $input->render();

		$result = [
			'inputN' => $input,
			'status' => '+',
			'output' => $output,
			'warnings' => $warnings,
			'input' => $input,
			'success' => true,
		];

		return $this->postProcess( $options, $input, $result );
	}

	/**
	 * @param string|TexNode|null $inputTree
	 * @return array|true
	 */
	private function checkTreeIntents( $inputTree ) {
		if ( is_string( $inputTree ) || !$inputTree ) {
			return true;
		}
		foreach ( $inputTree->getArgs() as $value ) {
			if ( $value instanceof Fun2 && $value->getFname() === "\\intent" ) {
				$intentStr = MMLutil::squashLitsToUnitIntent( $value->getArg2() );
				$intentContent = MMLParsingUtil::getIntentContent( $intentStr );
				$intentArg = MMLParsingUtil::getIntentArgs( $intentStr );
				$argch = self::checkIntentArg( $intentArg );
				if ( !$argch ) {
					return [
						'success' => false,
						'info' => 'malformatted intent argument',
					];
				}
				// do check on arg1
				$ret = !$intentContent ? true : $this->checkIntent( $intentContent );
				if ( !$ret || ( isset( $ret['success'] ) && $ret['success'] == false ) ) {
					return $ret;
				}
				return $this->checkTreeIntents( $value->getArg1() );
			}

			return self::checkTreeIntents( $value );
		}
		return true;
	}

	public static function checkIntentArg( ?string $input ): bool {
		// arg has roughly the same specs like the NCName in parserintent.pegjs
		return !$input || preg_match( '/^[a-zA-Z0-9._-]+$/', $input );
	}

	/**
	 * @return true|array
	 */
	public function checkIntent( string $input ) {
		// Very early intent syntax checker
		try {
			$parserIntent = new ParserIntent();
			$parserIntent->parse( $input );
			return true;
		} catch ( Exception $exception ) {
			return $this->handleTexError( $exception, null );
		}
	}

	public function handleTexError( Exception $e, ?array $options = null ): array {
		if ( $options && $options['debug'] ) {
			// @phan-suppress-next-line PhanThrowTypeAbsent
			throw $e;
		}
		$report = [ 'success' => false, 'warnings' => [] ];
		if ( $e instanceof SyntaxError ) {
			if ( $e->getMessage() === 'Illegal TeX function' ) {
				$report['status'] = 'F';
				$report['details'] = $e->found;
			} else {
				$report['status'] = 'S';
				$report['details'] = $e->getMessage();
			}

			$report += $this->getLocationInfo( $e );

			$report['error'] = [
				'message' => $e->getMessage(),
				'expected' => $e->expected,
				'found' => $e->found,
				'location' => [
					// This currently only has the start location. The end is not noted in SyntaxError in PHP
					// this issue is tracked in: https://phabricator.wikimedia.org/T321060
					'offset' => $e->grammarOffset,
					'line' => $e->grammarLine,
					'column' => $e->grammarColumn
				],
				'name' => $e->name
			];

		} else {
			$report['status'] = '-';
			$report['details'] = $e->getMessage();
			$report['error'] = $e;
		}
		return $report;
	}

	/**
	 * Gets the location information of an error object, or returns the default error
	 * location if no location information was specified.
	 * @param SyntaxError $e error object
	 * @return array information on the error.
	 */
	private function getLocationInfo( SyntaxError $e ) {
		try {
			return [
				'offset'  => $e->grammarOffset,
				'line' => $e->grammarLine,
				'column' => $e->grammarColumn
			];
		} catch ( Exception ) {
			return [ 'offset' => 0, 'line' => 0, 'column' => 0 ];
		}
	}

	public function preProcessInput( bool $texifyMhchem, array $options, TexArray|string|stdClass $input ): TexArray {
		if ( $texifyMhchem && ( $options['usemhchem'] ?? false ) ) {
			// Parse the chemical equations to TeX with mhChemParser in PHP as preprocessor
			$mhChemParser = new MHChemParser();
			$input = $mhChemParser->toTex( $input, "tex", true );
		}

		if ( is_string( $input ) ) {
			$input = $this->parser->parse( $input, $options );
		}
		if ( !$input instanceof TexArray ) {
			throw new LogicException( 'Tex Array object expected' );
		}
		return $input;
	}

	public function postProcess( array $options, TexArray $input, array $result ): array {
		if ( $options['report_required'] ) {
			foreach ( self::PKGS as $pkg ) {
				$pkg .= '_required';
				$tuRef = $this->tu->getBaseElements()[$pkg];
				$result[$pkg] = $input->containsFunc( $tuRef );
			}
		}

		if (
			!$options['usemhchem'] &&
			( $result['mhchem_required']
				?? $input->containsFunc( $this->tu->getBaseElements()['mhchem_required'] ) )
		) {
			return [
				'status' => 'C',
				'details' => 'mhchem package required.'
			];
		}
		if (
			!$options['usemhchemtexified'] &&
			( $result['mhchemtexified_required']
				?? $input->containsFunc( $this->tu->getBaseElements()['mhchemtexified_required'] ) )
		) {
			return [
				'status' => 'C',
				'details' => 'virtual mhchemtexified package required.'
			];
		}

		if ( !$options['useintent'] ) {
			if ( $result['intent_required'] ??
				$input->containsFunc( $this->tu->getBaseElements()['intent_required'] )
			) {
				return [
					'status' => 'C',
					'details' => 'virtual intent package required.'
				];
			}
		} elseif ( $input->containsFunc( $this->tu->getBaseElements()['intent_required'] ) ) {
			// Preliminary post-checks of correct intent-syntax
			$intentCheck = $this->checkTreeIntents( $input );
			if ( !$intentCheck || ( isset( $intentCheck['success'] ) && !$intentCheck['success'] ) ) {
				return $intentCheck + [ 'status' => 'I', 'details' => 'intent check failed.' ];
			}
		}

		return $result;
	}

}
