<?php

declare(strict_types=1);

namespace Doctrine\RST\Renderers;

use Doctrine\RST\Environment;
use Doctrine\RST\InvalidLink;
use Doctrine\RST\Nodes\Node;
use Doctrine\RST\Nodes\SpanNode;
use Doctrine\RST\Span\SpanToken;
use InvalidArgumentException;

use function is_string;
use function preg_replace;
use function preg_replace_callback;
use function sprintf;
use function str_replace;

abstract class SpanNodeRenderer implements NodeRenderer, SpanRenderer
{
    /** @var Environment */
    protected $environment;

    /** @var SpanNode */
    protected $span;

    public function __construct(
        Environment $environment,
        SpanNode $span
    ) {
        $this->environment = $environment;
        $this->span        = $span;
    }

    public function render(): string
    {
        $value = $this->span->getValue();

        $span = $this->renderSyntaxes($value);

        $span = $this->renderTokens($span);

        return $span;
    }

    private function renderSyntaxes(string $span): string
    {
        $span = $this->escape($span);

        $span = $this->renderStrongEmphasis($span);

        $span = $this->renderEmphasis($span);

        $span = $this->renderNbsp($span);

        $span = $this->renderVariables($span);

        $span = $this->renderBrs($span);

        return $span;
    }

    private function renderStrongEmphasis(string $span): string
    {
        return (string) preg_replace_callback('/\*\*(.+)\*\*/mUsi', function (array $matches): string {
            return $this->strongEmphasis($matches[1]);
        }, $span);
    }

    private function renderEmphasis(string $span): string
    {
        return (string) preg_replace_callback('/\*(.+)\*/mUsi', function (array $matches): string {
            return $this->emphasis($matches[1]);
        }, $span);
    }

    private function renderNbsp(string $span): string
    {
        return (string) preg_replace('/~/', $this->nbsp(), $span);
    }

    private function renderVariables(string $span): string
    {
        return (string) preg_replace_callback('/\|(.+)\|/mUsi', function (array $match): string {
            $variable = $this->environment->getVariable($match[1]);

            if ($variable === null) {
                return '';
            }

            if ($variable instanceof Node) {
                return $variable->render();
            }

            if (is_string($variable)) {
                return $variable;
            }

            return (string) $variable;
        }, $span);
    }

    private function renderBrs(string $span): string
    {
        // Adding brs when a space is at the end of a line
        return (string) preg_replace('/ \n/', $this->br(), $span);
    }

    private function renderTokens(string $span): string
    {
        foreach ($this->span->getTokens() as $token) {
            $span = $this->renderToken($token, $span);
        }

        return $span;
    }

    private function renderToken(SpanToken $spanToken, string $span): string
    {
        switch ($spanToken->getType()) {
            case SpanToken::TYPE_LITERAL:
                return $this->renderLiteral($spanToken, $span);

            case SpanToken::TYPE_REFERENCE:
                return $this->renderReference($spanToken, $span);

            case SpanToken::TYPE_LINK:
                return $this->renderLink($spanToken, $span);
        }

        throw new InvalidArgumentException(sprintf('Unknown token type %s', $spanToken->getType()));
    }

    private function renderLiteral(SpanToken $spanToken, string $span): string
    {
        return str_replace(
            $spanToken->getId(),
            $this->literal($spanToken->get('text')),
            $span
        );
    }

    private function renderReference(SpanToken $spanToken, string $span): string
    {
        $reference = $this->environment->resolve($spanToken->get('section'), $spanToken->get('url'));

        if ($reference === null) {
            $this->environment->addInvalidLink(new InvalidLink($spanToken->get('url')));

            return str_replace($spanToken->getId(), $spanToken->get('text'), $span);
        }

        $link = $this->reference($reference, $spanToken->getTokenData());

        return str_replace($spanToken->getId(), $link, $span);
    }

    private function renderLink(SpanToken $spanToken, string $span): string
    {
        $url  = $spanToken->get('url');
        $link = $spanToken->get('link');

        if ($url === '') {
            $url = $this->environment->getLink($link);

            if ($url === '') {
                $metaEntry = $this->environment->getMetaEntry();

                if ($metaEntry !== null && $metaEntry->hasTitle($link)) {
                    // A strangely-complex way to simply get the current relative URL
                    // For example, if the current page is "reference/page", then
                    // this would return "page" so the final URL is href="page#some-anchor".
                    $currentRelativeUrl = $this->environment->relativeUrl('/' . $metaEntry->getUrl());
                    $url                = $currentRelativeUrl . '#' . Environment::slugify($link);
                }
            }

            if ($url === '') {
                $this->environment->addInvalidLink(new InvalidLink($link));

                return str_replace($spanToken->getId(), $link, $span);
            }
        }

        $link = $this->link($url, $this->renderSyntaxes($link));

        return str_replace($spanToken->getId(), $link, $span);
    }
}
