<?php

declare(strict_types=1);

namespace Doctrine\RST\Parser;

use Doctrine\Common\EventManager;
use Doctrine\RST\Event\OnLinkParsedEvent;
use Doctrine\RST\Nodes\ParagraphNode;
use Doctrine\RST\Nodes\SpanNode;
use Doctrine\RST\Parser;

use function array_map;
use function array_shift;
use function count;
use function explode;
use function ltrim;
use function mb_strlen;
use function preg_match;
use function strlen;
use function substr;
use function trim;

final class LineDataParser
{
    /** @var Parser */
    private $parser;

    /** @var EventManager */
    private $eventManager;

    public function __construct(Parser $parser, EventManager $eventManager)
    {
        $this->parser       = $parser;
        $this->eventManager = $eventManager;
    }

    public function parseLink(string $line): ?Link
    {
        // Links
        if (preg_match('/^\.\. _`(.+)`: (.+)$/mUsi', $line, $match) > 0) {
            return $this->createLink($match[1], $match[2], Link::TYPE_LINK);
        }

        // anonymous links
        if (preg_match('/^\.\. _(.+): (.+)$/mUsi', $line, $match) > 0) {
            return $this->createLink($match[1], $match[2], Link::TYPE_LINK);
        }

        // Short anonymous links
        if (preg_match('/^__ (.+)$/mUsi', trim($line), $match) > 0) {
            $url = $match[1];

            return $this->createLink('_', $url, Link::TYPE_LINK);
        }

        // Anchor links - ".. _`anchor-link`:"
        if (preg_match('/^\.\. _`(.+)`:$/mUsi', trim($line), $match) > 0) {
            $anchor = $match[1];

            return new Link($anchor, '#' . $anchor, Link::TYPE_ANCHOR);
        }

        if (preg_match('/^\.\. _(.+):$/mUsi', trim($line), $match) > 0) {
            $anchor = $match[1];

            return $this->createLink($anchor, '#' . $anchor, Link::TYPE_ANCHOR);
        }

        return null;
    }

    private function createLink(string $name, string $url, string $type): Link
    {
        $this->eventManager->dispatchEvent(
            OnLinkParsedEvent::ON_LINK_PARSED,
            new OnLinkParsedEvent($url, $type, $this->parser->getEnvironment()->getCurrentFileName())
        );

        return new Link($name, $url, $type);
    }

    public function parseDirectiveOption(string $line): ?DirectiveOption
    {
        if (preg_match('/^(\s+):(.+): (.*)$/mUsi', $line, $match) > 0) {
            return new DirectiveOption($match[2], trim($match[3]));
        }

        if (preg_match('/^(\s+):(.+):(\s*)$/mUsi', $line, $match) > 0) {
            $value = trim($match[3]);

            return new DirectiveOption($match[2], true);
        }

        return null;
    }

    public function parseDirective(string $line): ?Directive
    {
        if (preg_match('/^\.\. (\|(.+)\| |)([^\s]+)::( (.*)|)$/mUsi', $line, $match) > 0) {
            return new Directive(
                $match[2],
                $match[3],
                trim($match[4])
            );
        }

        return null;
    }

    /**
     * @param string[] $lines
     *
     * @return ListItem[]
     */
    public function parseList(array $lines): array
    {
        $list          = [];
        $currentItem   = null;
        $currentPrefix = null;
        $currentOffset = 0;

        $createListItem = function (string $item, string $prefix): ListItem {
            // parse any markup in the list item (e.g. sublists, directives)
            $nodes = $this->parser->getSubParser()->parseLocal($item)->getNodes();
            if (count($nodes) === 1 && $nodes[0] instanceof ParagraphNode) {
                // if there is only one paragraph node, the value is put directly in the <li> element
                $nodes = [$nodes[0]->getValue()];
            }

            return new ListItem($prefix, mb_strlen($prefix) > 1, $nodes);
        };

        foreach ($lines as $line) {
            if (preg_match(LineChecker::LIST_MARKER, $line, $m) > 0) {
                // a list marker indicates the start of a new list item,
                // complete the previous one and start a new one
                if ($currentItem !== null) {
                    $list[] = $createListItem($currentItem, $currentPrefix);
                }

                $currentOffset = strlen($m[0]);
                $currentPrefix = $m[1];
                $currentItem   = substr($line, $currentOffset) . "\n";

                continue;
            }

            // the list item offset is determined by the offset of the first text
            if (trim($currentItem) === '') {
                $currentOffset = strlen($line) - strlen(ltrim($line));
            }

            $currentItem .= substr($line, $currentOffset) . "\n";
        }

        if ($currentItem !== null) {
            $list[] = $createListItem($currentItem, $currentPrefix);
        }

        return $list;
    }

    /** @param string[] $lines */
    public function parseDefinitionList(array $lines): DefinitionList
    {
        /** @var array{term: SpanNode, classifiers: list<SpanNode>, definition: string}|null $definitionListTerm */
        $definitionListTerm = null;
        $definitionList     = [];

        $createDefinitionTerm = function (array $definitionListTerm): DefinitionListTerm {
            // parse any markup in the definition (e.g. lists, directives)
            $definitionNodes = $this->parser->getSubParser()->parseLocal($definitionListTerm['definition'])->getNodes();
            if (count($definitionNodes) === 1 && $definitionNodes[0] instanceof ParagraphNode) {
                // if there is only one paragraph node, the value is put directly in the <dd> element
                $definitionNodes = [$definitionNodes[0]->getValue()];
            } else {
                // otherwise, .first and .last are added to the first and last nodes of the definition
                $definitionNodes[0]->setClasses($definitionNodes[0]->getClasses() + ['first']);
                $definitionNodes[count($definitionNodes) - 1]->setClasses($definitionNodes[count($definitionNodes) - 1]->getClasses() + ['last']);
            }

            return new DefinitionListTerm(
                $definitionListTerm['term'],
                $definitionListTerm['classifiers'],
                $definitionNodes
            );
        };

        $currentOffset = 0;
        foreach ($lines as $key => $line) {
            // indent or empty line = term definition line
            if ($definitionListTerm !== null && (trim($line) === '') || $line[0] === ' ') {
                if ($currentOffset === 0) {
                    // first line of a definition determines the indentation offset
                    $definition    = ltrim($line);
                    $currentOffset = strlen($line) - strlen($definition);
                } else {
                    $definition = substr($line, $currentOffset);
                }

                $definitionListTerm['definition'] .= $definition . "\n";

            // non empty string at the start of the line = definition term
            } elseif (trim($line) !== '') {
                // we are starting a new term so if we have an existing
                // term with definitions, add it to the definition list
                if ($definitionListTerm !== null) {
                    $definitionList[] = $createDefinitionTerm($definitionListTerm);
                }

                $parts       = explode(' : ', trim($line));
                $term        = array_shift($parts);
                $classifiers = array_map(function (string $classifier): SpanNode {
                    return $this->parser->createSpanNode($classifier);
                }, array_map('trim', $parts));

                $currentOffset      = 0;
                $definitionListTerm = [
                    'term' => $this->parser->createSpanNode($term),
                    'classifiers' => $classifiers,
                    'definition' => '',
                ];
            }
        }

        // append the last definition of the list
        if ($definitionListTerm !== null) {
            $definitionList[] = $createDefinitionTerm($definitionListTerm);
        }

        return new DefinitionList($definitionList);
    }
}
