<?php

declare(strict_types=1);

namespace Doctrine\RST;

use Doctrine\RST\Meta\MetaEntry;
use Doctrine\RST\Meta\Metas;
use Doctrine\RST\NodeFactory\NodeFactory;
use Doctrine\RST\References\Reference;
use Doctrine\RST\References\ResolvedReference;
use Doctrine\RST\Templates\TemplateRenderer;
use InvalidArgumentException;
use Symfony\Component\String\Slugger\AsciiSlugger;

use function array_shift;
use function dirname;
use function implode;
use function in_array;
use function sprintf;
use function strtolower;
use function trim;

class Environment
{
    /** @var Configuration */
    private $configuration;

    /** @var ErrorManager */
    private $errorManager;

    /** @var UrlGenerator */
    private $urlGenerator;

    /** @var int */
    private $currentTitleLevel = 0;

    /** @var string[] */
    private $titleLetters = [];

    /** @var string */
    private $currentFileName = '';

    /** @var string */
    private $currentDirectory = '.';

    /** @var string */
    private $targetDirectory = '.';

    /** @var string|null */
    private $url = null;

    /** @var Reference[] */
    private $references = [];

    /** @var Metas */
    private $metas;

    /** @var string[] */
    private $dependencies = [];

    /** @var string[] */
    private $unresolvedDependencies = [];

    /** @var string[] */
    private $originalDependencyNames = [];

    /** @var string[] */
    private $variables = [];

    /** @var string[] */
    private $links = [];

    /** @var int[] */
    private $levels = [];

    /** @var int[] */
    private $counters = [];

    /** @var string[] */
    private $anonymous = [];

    /** @var InvalidLink[] */
    private $invalidLinks = [];

    public function __construct(Configuration $configuration)
    {
        $this->configuration = $configuration;
        $this->errorManager  = new ErrorManager($this->configuration);
        $this->urlGenerator  = new UrlGenerator(
            $this->configuration
        );
        $this->metas         = new Metas();

        $this->reset();
    }

    public function reset(): void
    {
        $this->titleLetters      = [];
        $this->currentTitleLevel = 0;
        $this->levels            = [];
        $this->counters          = [];

        for ($level = 0; $level < 16; $level++) {
            $this->levels[$level]   = 1;
            $this->counters[$level] = 0;
        }
    }

    public function getConfiguration(): Configuration
    {
        return $this->configuration;
    }

    public function getErrorManager(): ErrorManager
    {
        return $this->errorManager;
    }

    public function setErrorManager(ErrorManager $errorManager): void
    {
        $this->errorManager = $errorManager;
    }

    public function setMetas(Metas $metas): void
    {
        $this->metas = $metas;
    }

    public function getNodeFactory(): NodeFactory
    {
        return $this->configuration->getNodeFactory($this);
    }

    public function getTemplateRenderer(): TemplateRenderer
    {
        return $this->configuration->getTemplateRenderer();
    }

    public function registerReference(Reference $reference): void
    {
        $this->references[$reference->getName()] = $reference;
    }

    public function resolve(string $section, string $data): ?ResolvedReference
    {
        if (! isset($this->references[$section])) {
            $this->addMissingReferenceSectionError($section);

            return null;
        }

        $reference = $this->references[$section];

        $resolvedReference = $reference->resolve($this, $data);

        if ($resolvedReference === null) {
            $this->addInvalidLink(new InvalidLink($data));

            if ($this->getMetaEntry() !== null) {
                $this->getMetaEntry()->removeDependency(
                    // use the original name
                    $this->originalDependencyNames[$data] ?? $data
                );
            }

            return null;
        }

        if (isset($this->unresolvedDependencies[$data]) && $this->getMetaEntry() !== null) {
            $this->getMetaEntry()->resolveDependency(
                // use the unique, unresolved name
                $this->unresolvedDependencies[$data],
                $resolvedReference->getFile()
            );
        }

        return $resolvedReference;
    }

    public function addInvalidLink(InvalidLink $invalidLink): void
    {
        $this->invalidLinks[] = $invalidLink;
    }

    /** @return InvalidLink[] */
    public function getInvalidLinks(): array
    {
        return $this->invalidLinks;
    }

    /** @return string[]|null */
    public function found(string $section, string $data): ?array
    {
        if (isset($this->references[$section])) {
            $reference = $this->references[$section];

            $reference->found($this, $data);

            return null;
        }

        $this->addMissingReferenceSectionError($section);

        return null;
    }

    /** @param mixed $value */
    public function setVariable(string $variable, $value): void
    {
        $this->variables[$variable] = $value;
    }

    public function createTitle(int $level): string
    {
        for ($currentLevel = 0; $currentLevel < 16; $currentLevel++) {
            if ($currentLevel <= $level) {
                continue;
            }

            $this->levels[$currentLevel]   = 1;
            $this->counters[$currentLevel] = 0;
        }

        $this->levels[$level] = 1;
        $this->counters[$level]++;
        $token = ['title'];

        for ($i = 1; $i <= $level; $i++) {
            $token[] = $this->counters[$i];
        }

        return implode('.', $token);
    }

    public function getNumber(int $level): int
    {
        return $this->levels[$level]++;
    }

    /**
     * @param mixed|null $default
     *
     * @return mixed
     */
    public function getVariable(string $variable, $default = null)
    {
        if (isset($this->variables[$variable])) {
            return $this->variables[$variable];
        }

        return $default;
    }

    public function setLink(string $name, string $url): void
    {
        $name = trim(strtolower($name));

        if ($name === '_') {
            $name = array_shift($this->anonymous);
        }

        $this->links[$name] = trim($url);
    }

    public function resetAnonymousStack(): void
    {
        $this->anonymous = [];
    }

    public function pushAnonymous(string $name): void
    {
        $this->anonymous[] = trim(strtolower($name));
    }

    /** @return string[] */
    public function getLinks(): array
    {
        return $this->links;
    }

    public function getLink(string $name, bool $relative = true): string
    {
        $name = trim(strtolower($name));

        if (isset($this->links[$name])) {
            $link = $this->links[$name];

            if ($relative) {
                return (string) $this->relativeUrl($link);
            }

            return $link;
        }

        return '';
    }

    public function addDependency(string $dependency, bool $requiresResolving = false): void
    {
        if ($requiresResolving) {
            // a hack to avoid collisions between resolved and unresolved dependencies
            $dependencyName                            = 'UNRESOLVED__' . $dependency;
            $this->unresolvedDependencies[$dependency] = $dependencyName;
            // map the original dependency name to the one that will be stored
            $this->originalDependencyNames[$dependency] = $dependencyName;
        } else {
            // the dependency is already a filename, probably a :doc:
            // or from a toc-tree - change it to the canonical URL
            $canonicalDependency = $this->canonicalUrl($dependency);

            if ($canonicalDependency === null) {
                throw new InvalidArgumentException(sprintf(
                    'Could not get canonical url for dependency %s',
                    $dependency
                ));
            }

            $dependencyName = $canonicalDependency;
            // map the original dependency name to the one that will be stored
            $this->originalDependencyNames[$dependency] = $canonicalDependency;
        }

        if (in_array($dependencyName, $this->dependencies, true)) {
            return;
        }

        $this->dependencies[] = $dependencyName;
    }

    /** @return string[] */
    public function getDependencies(): array
    {
        return $this->dependencies;
    }

    public function relativeUrl(?string $url): ?string
    {
        return $this->urlGenerator->relativeUrl($url, $this->currentFileName);
    }

    public function absoluteUrl(string $url): string
    {
        return $this->urlGenerator->absoluteUrl($this->getDirName(), $url);
    }

    public function canonicalUrl(string $url): ?string
    {
        return $this->urlGenerator->canonicalUrl($this->getDirName(), $url);
    }

    public function generateUrl(string $path): string
    {
        return $this->urlGenerator->generateUrl(
            $path,
            $this->currentFileName,
            $this->getDirName()
        );
    }

    public function getDirName(): string
    {
        $dirname = dirname($this->currentFileName);

        if ($dirname === '.') {
            return '';
        }

        return $dirname;
    }

    public function setCurrentFileName(string $filename): void
    {
        $this->currentFileName = $filename;
    }

    /**
     * Returns the currently-parsed filename.
     *
     * This is relative to the root source directory and without
     * the extension (e.g. "index" or "subdir/file")
     */
    public function getCurrentFileName(): string
    {
        return $this->currentFileName;
    }

    public function setCurrentDirectory(string $directory): void
    {
        $this->currentDirectory = $directory;
    }

    public function getCurrentDirectory(): string
    {
        return $this->currentDirectory;
    }

    public function absoluteRelativePath(string $url): string
    {
        return $this->currentDirectory . '/' . $this->getDirName() . '/' . $this->relativeUrl($url);
    }

    public function setTargetDirectory(string $directory): void
    {
        $this->targetDirectory = $directory;
    }

    public function getTargetDirectory(): string
    {
        return $this->targetDirectory;
    }

    public function getUrl(): string
    {
        if ($this->url !== null) {
            return $this->url;
        }

        return $this->currentFileName;
    }

    public function setUrl(string $url): void
    {
        if ($this->getDirName() !== '') {
            $url = $this->getDirName() . '/' . $url;
        }

        $this->url = $url;
    }

    public function getMetas(): Metas
    {
        return $this->metas;
    }

    public function getMetaEntry(): ?MetaEntry
    {
        return $this->metas->get($this->currentFileName);
    }

    public function getLevel(string $letter): int
    {
        foreach ($this->titleLetters as $level => $titleLetter) {
            if ($letter === $titleLetter) {
                return $level;
            }
        }

        $this->currentTitleLevel++;
        $this->titleLetters[$this->currentTitleLevel] = $letter;

        return $this->currentTitleLevel;
    }

    /** @return string[] */
    public function getTitleLetters(): array
    {
        return $this->titleLetters;
    }

    public static function slugify(string $text): string
    {
        return (new AsciiSlugger('en', []))->slug($text)->lower()->toString();
    }

    private function addMissingReferenceSectionError(string $section): void
    {
        $this->errorManager->error(
            sprintf('Unknown reference section "%s"', $section),
            $this->getCurrentFileName()
        );
    }
}