<?php

namespace MediaWiki\Extension\Math\HookHandlers;

use MediaWiki\Extension\Math\Hooks\HookRunner;
use MediaWiki\Extension\Math\MathConfig;
use MediaWiki\Extension\Math\MathMathML;
use MediaWiki\Extension\Math\MathMathMLCli;
use MediaWiki\Extension\Math\MathRenderer;
use MediaWiki\Extension\Math\Render\RendererFactory;
use MediaWiki\HookContainer\HookContainer;
use MediaWiki\Logger\LoggerFactory;
use MediaWiki\Parser\Hook\ParserAfterTidyHook;
use MediaWiki\Parser\Hook\ParserFirstCallInitHook;
use MediaWiki\Parser\Hook\ParserOptionsRegisterHook;
use MediaWiki\Parser\Parser;
use MediaWiki\Parser\ParserOptions;
use MediaWiki\User\Options\UserOptionsLookup;

/**
 * Hook handler for Parser hooks
 */
class ParserHooksHandler implements
	ParserFirstCallInitHook,
	ParserAfterTidyHook,
	ParserOptionsRegisterHook
{

	/** @var int */
	private $mathTagCounter = 1;

	/** @var array[] renders delayed to be done as a batch [ MathRenderer, Parser ] */
	private $mathLazyRenderBatch = [];

	/**
	 * @var string[] result of batch rendering.
	 *
	 * We store rendered math to prevent parsing again if we happen to need it later.
	 * Sometimes, parserAfterTidy is called several times.
	 * First time it is called, we batch render all math, but maybe we don't need some math at that point,
	 * so we store the rendered math for later use.
	 * T371972
	 */
	private $mathLazyRenderBatchCompleted = [];

	private readonly HookRunner $hookRunner;

	public function __construct(
		private readonly RendererFactory $rendererFactory,
		private readonly UserOptionsLookup $userOptionsLookup,
		HookContainer $hookContainer,
	) {
		$this->hookRunner = new HookRunner( $hookContainer );
	}

	/**
	 * Register the <math> tag with the Parser.
	 *
	 * @param Parser $parser
	 */
	public function onParserFirstCallInit( $parser ) {
		$parser->setHook( 'math', [ $this, 'mathTagHook' ] );
		// @deprecated the ce tag is deprecated in favour of chem cf. T153606
		$parser->setHook( 'ce', [ $this, 'chemTagHook' ] );
		$parser->setHook( 'chem', [ $this, 'chemTagHook' ] );
	}

	/**
	 * Callback function for the <math> parser hook.
	 *
	 * @param ?string $content (the LaTeX input)
	 * @param array $attributes
	 * @param Parser $parser
	 * @return array|string
	 */
	public function mathTagHook( ?string $content, array $attributes, Parser $parser ) {
		global $wgMathSvgRenderer;
		$mode = $parser->getOptions()->getOption( 'math' );
		if ( $mode === MathConfig::MODE_NATIVE_JAX ) {
			$parser->getOutput()->addModules( [ 'ext.math.mathjax' ] );
			if ( ( $attributes['forcemathmode'] ?? MathConfig::MODE_NATIVE_JAX ) !== MathConfig::MODE_NATIVE_JAX ) {
				$attributes['class'] = 'mathjax_ignore';
			}
			$mode = MathConfig::MODE_NATIVE_MML;
		}
		$renderer = $this->rendererFactory->getRenderer( $content ?? '', $attributes, $mode );

		$parser->getOutput()->addModuleStyles( [ 'ext.math.styles' ] );
		$parser->getOutput()->addModules( [ 'ext.math.polyfills' ] );
		if ( array_key_exists( "qid", $attributes ) ) {
			$parser->getOutput()->addModules( [ 'ext.math.popup' ] );
		}
		if ( ( $wgMathSvgRenderer === 'restbase' && $mode == MathConfig::MODE_MATHML ) ||
			$mode === MathConfig::MODE_NATIVE_JAX ||
			$mode === MathConfig::MODE_NATIVE_MML
		) {
			$marker = Parser::MARKER_PREFIX .
				'-postMath-' . sprintf( '%08X', $this->mathTagCounter++ ) .
				Parser::MARKER_SUFFIX;
			$this->mathLazyRenderBatch[$marker] = [ $renderer, $parser ];
			return $marker;
		}
		return [ $this->mathPostTagHook( $renderer, $parser ), 'markerType' => 'nowiki' ];
	}

	/**
	 * Callback function for the <ce> parser hook.
	 *
	 * @param ?string $content (the LaTeX input)
	 * @param array $attributes
	 * @param Parser $parser
	 * @return array|string
	 */
	public function chemTagHook( ?string $content, array $attributes, Parser $parser ) {
		$attributes['chem'] = true;
		return $this->mathTagHook( '\ce{' . $content . '}', $attributes, $parser );
	}

	/**
	 * Callback function for the <math> parser hook.
	 *
	 * @param MathRenderer $renderer
	 * @param Parser $parser
	 * @return string
	 */
	private function mathPostTagHook( MathRenderer $renderer, Parser $parser ) {
		$checkResult = $renderer->checkTeX();

		if ( $checkResult !== true ) {
			$renderer->addTrackingCategories( $parser );
			return $renderer->getLastError();
		}

		if ( $renderer->render() ) {
			LoggerFactory::getInstance( 'Math' )->debug( "Rendering successful. Writing output" );
			$renderedMath = $renderer->getHtmlOutput();
			$renderer->addTrackingCategories( $parser );
		} else {
			LoggerFactory::getInstance( 'Math' )->warning(
				"Rendering failed. Printing error message." );
			// Set a short parser cache time (10 minutes) after encountering
			// render issues, but not syntax issues.
			$parser->getOutput()->updateCacheExpiry( 600 );
			$renderer->addTrackingCategories( $parser );
			return $renderer->getLastError();
		}

		// Enables indexing of math formula
		$this->hookRunner->onMathFormulaPostRenderRevision(
			$parser->getRevisionRecordObject(),
			$renderer,
			$renderedMath
		);

		// Writes cache if rendering was successful
		$renderer->writeCache();

		return $renderedMath;
	}

	/**
	 * @param Parser $parser
	 * @param string &$text
	 */
	public function onParserAfterTidy( $parser, &$text ) {
		$this->renderBatch( $parser );

		foreach ( $this->mathLazyRenderBatchCompleted as $key => $value ) {
			$count = 0;
			$text = str_replace( $key, $value, $text, $count );
			if ( $count ) {
				// This hook might be called multiple times. However once the tag is replaced the job is done.
				unset( $this->mathLazyRenderBatchCompleted[ $key ] );
			}
		}
	}

	private function renderBatch( Parser $parser ) {
		$mode = $parser->getOptions()->getOption( 'math' );

		if ( $mode === MathConfig::MODE_MATHML ) {
			$renderers = array_column( $this->mathLazyRenderBatch, 0 );
			if ( MathMathMLCli::isMathoidCliConfigured() ) {
				MathMathMLCli::batchEvaluate( $renderers );
			} else {
				MathMathML::batchEvaluate( $renderers );
			}
		}
		foreach ( $this->mathLazyRenderBatch as $key => [ $renderer, $renderParser ] ) {
			$this->mathLazyRenderBatchCompleted[ $key ] = $this->mathPostTagHook( $renderer, $renderParser );
			// Unset directly after (expensive) parse to prevent accidentaly
			// parsing again on subsequent parserAfterTidy calls.
			// Result is stored on mathLazyRenderBatchCompleted array, in case we need it later. (T371972)
			unset( $this->mathLazyRenderBatch[ $key ] );
		}
	}

	/** @inheritDoc */
	public function onParserOptionsRegister( &$defaults, &$inCacheKey, &$lazyLoad ) {
		$defaults['math'] = $this->userOptionsLookup->getDefaultOption( 'math' );
		$inCacheKey['math'] = true;
		$lazyLoad['math'] = function ( ParserOptions $options )  {
			return MathConfig::normalizeRenderingMode(
				$this->userOptionsLookup->getOption( $options->getUserIdentity(), 'math' ) ??
					MathConfig::MODE_NATIVE_MML
			);
		};
	}
}
