<?php namespace Todaymade\Daux\Format\HTML\ContentTypes\Markdown\TOC;

use DeepCopy\DeepCopy;
use League\CommonMark\Block\Element\Document;
use League\CommonMark\Block\Element\Heading;
use League\CommonMark\Block\Element\ListBlock;
use League\CommonMark\Block\Element\ListData;
use League\CommonMark\Block\Element\ListItem;
use League\CommonMark\Block\Element\Paragraph;
use League\CommonMark\DocumentProcessorInterface;
use League\CommonMark\Inline\Element\Link;
use League\CommonMark\Inline\Element\Text;
use League\CommonMark\Node\Node;
use ReflectionMethod;
use Todaymade\Daux\Config;
use Todaymade\Daux\ContentTypes\Markdown\TableOfContents;

class Processor implements DocumentProcessorInterface
{
    protected $config;

    public function __construct(Config $config)
    {
        $this->config = $config;
    }

    public function hasAutoTOC()
    {
        return array_key_exists('html', $this->config) && array_key_exists('auto_toc', $this->config['html']) && $this->config['html']['auto_toc'];
    }

    /**
     * @param Document $document
     *
     * @return void
     */
    public function processDocument(Document $document)
    {
        /** @var TableOfContents[] $tocs */
        $tocs = [];

        $headings = [];

        $document->heading_ids = [];
        $walker = $document->walker();
        while ($event = $walker->next()) {
            $node = $event->getNode();

            if ($node instanceof TableOfContents && !$event->isEntering()) {
                $tocs[] = $node;
                continue;
            }

            if (!($node instanceof Heading) || !$event->isEntering()) {
                continue;
            }

            $this->ensureHeadingHasId($document, $node);
            $headings[] = new Entry($node);
        }

        if (count($headings) && (count($tocs) || $this->hasAutoTOC())) {
            $generated = $this->generate($headings);

            if (count($tocs)) {
                foreach ($tocs as $toc) {
                    $toc->appendChild($this->render($generated->getChildren()));
                }
            } else {
                $document->prependChild($this->render($generated->getChildren()));
            }
        }
    }

    /**
     * Get an escaped version of the link
     * @param string $url
     * @return string
     */
    protected function escaped($url) {
        $url = trim($url);
        $url = preg_replace('~[^\\pL0-9_]+~u', '-', $url);
        $url = trim($url, "-");
        $url = iconv("utf-8", "ASCII//TRANSLIT//IGNORE", $url);
        $url = preg_replace('~[^-a-zA-Z0-9_]+~', '', $url);

        return $url;
    }

    protected function getUniqueId(Document $document, $proposed) {
        if ($proposed == "page_") {
            $proposed = "page_section_" . (count($document->heading_ids) + 1);
        }

        // Quick path, it's a unique ID
        if (!in_array($proposed, $document->heading_ids)) {
            $document->heading_ids[] = $proposed;
            return $proposed;
        }

        $extension = 1; // Initialize the variable at one, so on the first iteration we have 2
        do {
            $extension++;
        } while (in_array("$proposed-$extension", $document->heading_ids));

        return "$proposed-$extension";
    }

    /**
     * @param Heading $node
     */
    protected function ensureHeadingHasId(Document $document, Heading $node)
    {
        // If the node has an ID, no need to generate it, just check it's unique
        $attributes = $node->getData('attributes', []);
        if (array_key_exists('id', $attributes) && !empty($attributes['id'])) {
            $node->data['attributes']['id'] = $this->getUniqueId($document, $attributes['id']);

            return;
        }

        // Well, seems we have to generate an ID
        $walker = $node->walker();
        $inside = [];
        while ($event = $walker->next()) {
            $insideNode = $event->getNode();

            if ($insideNode instanceof Heading) {
                continue;
            }

            $inside[] = $insideNode;
        }

        $text = '';
        foreach ($inside as $other) {
            if ($other instanceof Text) {
                $text .= ' ' . $other->getContent();
            }
        }

        $node->data['attributes']['id'] = $this->getUniqueId($document,'page_'. $this->escaped($text));
    }

    /**
     * Make a tree of the list of headings
     *
     * @param Entry[] $headings
     * @return RootEntry
     */
    public function generate($headings)
    {
        /** @var Entry $previous */
        $root = $previous = new RootEntry();
        foreach ($headings as $heading) {
            if ($heading->getLevel() < $previous->getLevel()) {
                $parent = $previous;
                do {
                    $parent = $parent->getParent();
                } while ($heading->getLevel() <= $parent->getLevel() && $parent->getLevel() != 0);

                $parent->addChild($heading);
                $previous = $heading;
                continue;
            }


            if ($heading->getLevel() > $previous->getLevel()) {
                $previous->addChild($heading);
                $previous = $heading;
                continue;
            }

            //if ($heading->getLevel() == $previous->getLevel()) {
            $previous->getParent()->addChild($heading);
            $previous = $heading;
            continue;
            //}
        }

        return $root;
    }

    /**
     * @param Entry[] $entries
     * @return ListBlock
     */
    protected function render(array $entries)
    {
        $data = new ListData();
        $data->type = ListBlock::TYPE_UNORDERED;

        $list = new ListBlock($data);
        $list->data['attributes']['class'] = 'TableOfContents';

        foreach ($entries as $entry) {
            $item = new ListItem($data);

            $a = new Link('#' . $entry->getId());

            $content = $entry->getContent();
            if ($content != null) {
                foreach ($this->cloneChildren($content) as $node) {
                    $a->appendChild($node);
                }
            }

            $p = new Paragraph();
            $p->appendChild($a);

            $item->appendChild($p);

            if (!empty($entry->getChildren())) {
                $item->appendChild($this->render($entry->getChildren()));
            }

            $list->appendChild($item);
        }

        return $list;
    }

    /**
     * Set the specified property to null on the object.
     *
     * @param Heading $object The object to modify
     * @param string $property The property to nullify
     */
    protected function setNull(Heading $object, $property)
    {
        $prop = new \ReflectionProperty(get_class($object), $property);
        $prop->setAccessible(true);
        $prop->setValue($object, null);
    }

    /**
     * @param Heading $node
     * @return Node[]
     */
    protected function cloneChildren(Heading $node)
    {
        $firstClone = clone $node;

        // We have no choice but to hack into the
        // system to reset the parent, previous and next
        $this->setNull($firstClone, 'parent');
        $this->setNull($firstClone, 'previous');
        $this->setNull($firstClone, 'next');

        // Also, the child elements need to know the next parents
        foreach ($firstClone->children() as $subnode) {
            $method = new ReflectionMethod(get_class($subnode), 'setParent');
            $method->setAccessible(true);
            $method->invoke($subnode, $firstClone);
        }

        return (new DeepCopy())->copy($firstClone)->children();
    }
}
