<?php

namespace dokuwiki\Parsing;

use dokuwiki\Debug\DebugHelper;
use dokuwiki\Parsing\Lexer\Lexer;
use dokuwiki\Parsing\ParserMode\AbstractMode;
use dokuwiki\Parsing\ParserMode\Base;

/**
 * Sets up the Lexer with modes and points it to the Handler
 * For an intro to the Lexer see: wiki:parser
 */
class Parser
{
    /** @var Handler */
    protected $handler;

    /** @var ModeRegistry the registry of the parse this parser belongs to */
    protected ModeRegistry $registry;

    /** @var Lexer $lexer */
    protected $lexer;

    /** @var AbstractMode[] $modes */
    protected $modes = [];

    /** @var bool mode connections may only be set up once */
    protected $connected = false;

    /**
     * @param Handler $handler
     * @param ModeRegistry|null $registry the registry of the parse, injected
     *     into every mode (all descend from AbstractMode) as it is added. Null
     *     is a backward-compatibility path for the legacy new Parser($handler)
     *     signature: the registry is taken from the handler in that case.
     */
    public function __construct(Handler $handler, ?ModeRegistry $registry = null)
    {
        $this->handler = $handler;
        $this->registry = $registry ?? $handler->getModeRegistry();
    }

    /**
     * Accessor for the Handler instance.
     *
     * @return Handler
     */
    public function getHandler()
    {
        return $this->handler;
    }

    /**
     * Adds the base mode and initialized the lexer
     *
     * @param Base $BaseMode
     */
    protected function addBaseMode($BaseMode)
    {
        $this->modes['base'] = $BaseMode;
        if (!$this->lexer) {
            $this->lexer = new Lexer($this->handler, 'base', true);
        }
        $this->injectDependencies($BaseMode);
        $this->handler->registerModeObject('base', $BaseMode);
    }

    /**
     * Give a mode the shared objects of this parse: its registry and lexer.
     *
     * Called as the mode joins the parser, before any connect/handle callback
     * runs. Every mode descends from AbstractMode, which carries the setters.
     *
     * @param AbstractMode $Mode
     */
    protected function injectDependencies(AbstractMode $Mode)
    {
        $Mode->setModeRegistry($this->registry);
        $Mode->setLexer($this->lexer);
    }

    /**
     * Add a new syntax element (mode) to the parser
     *
     * PHP preserves order of associative elements
     * Mode sequence is important
     *
     * @param string $name
     * @param AbstractMode $Mode
     */
    public function addMode($name, AbstractMode $Mode)
    {
        if (!isset($this->modes['base'])) {
            $this->addBaseMode(new Base());
        }
        $this->injectDependencies($Mode);
        $this->modes[$name] = $Mode;
        $this->handler->registerModeObject($name, $Mode);
    }

    /**
     * Connect all modes with each other
     *
     * This is the last step before actually parsing.
     */
    protected function connectModes()
    {

        if ($this->connected) {
            return;
        }

        // Run all preConnect() first so modes can register shared
        // metadata (e.g. line start markers) before any patterns are built
        foreach (array_keys($this->modes) as $mode) {
            if ($mode == 'base') continue;
            $this->modes[$mode]->preConnect();
        }

        foreach (array_keys($this->modes) as $mode) {
            if ($mode == 'base') continue;

            foreach (array_keys($this->modes) as $cm) {
                if ($this->modes[$cm]->accepts($mode)) {
                    $this->modes[$mode]->connectTo($cm);
                }
            }

            $this->modes[$mode]->postConnect();
        }

        $this->connected = true;
    }

    /**
     * Parses wiki syntax to instructions
     *
     * @param string $doc the wiki syntax text
     * @return array instructions
     */
    public function parse($doc)
    {
        $this->connectModes();
        // Normalize CRs and pad doc
        $doc = "\n" . str_replace("\r\n", "\n", $doc) . "\n";
        $this->lexer->parse($doc);

        if (!method_exists($this->handler, 'finalize')) {
            /** @deprecated 2019-10 we have a legacy handler from a plugin, assume legacy _finalize exists */

            DebugHelper::dbgCustomDeprecationEvent(
                'finalize()',
                $this->handler::class . '::_finalize()',
                __METHOD__,
                __FILE__,
                __LINE__
            );
            $this->handler->_finalize();
        } else {
            $this->handler->finalize();
        }
        return $this->handler->calls;
    }
}
