<?php

declare(strict_types=1);

/**
 * Hoa
 *
 *
 * @license
 *
 * New BSD License
 *
 * Copyright © 2007-2017, Hoa community. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the name of the Hoa nor the names of its contributors may be
 *       used to endorse or promote products derived from this software without
 *       specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

namespace Kitab\Compiler\Target\DocTest;

use Generator;
use Hoa\File\Directory;
use Hoa\File\Write;
use Kitab\Compiler\IntermediateRepresentation;
use Kitab\Compiler\Target\Target;
use Kitab\Configuration;
use Kitab\Exception;
use League\CommonMark;
use RuntimeException;

class DocTest implements Target
{
    const EXAMPLES_SECTION   = 'Examples';
    const EXCEPTIONS_SECTION = 'Exceptions';

    protected static $_markdownParser = null;
    protected $_generatedTestSuites   = [];
    protected $_codeBlockHandlers     = [];

    public function addCodeBlockHandler(CodeBlockHandler\Definition $codeBlockHandler): self
    {
        $this->_codeBlockHandlers[$codeBlockHandler->getDefinitionName()] = $codeBlockHandler;

        return $this;
    }

    public function removeCodeBlockHandler(CodeBlockHandler\Definition $codeBlockHandler): self
    {
        unset($this->_codeBlockHandlers[$codeBlockHandler->getDefinitionName()]);

        return $this;
    }

    public function compile(IntermediateRepresentation\File $file)
    {
        $testSuites =
            '<?php' . "\n\n" .
            'declare(strict_types=1);';
        $anyTestSuite = false;

        foreach ($file as $representation) {
            if ($representation instanceof IntermediateRepresentation\Entity) {
                $_testSuites = $this->compileEntity($representation);

                if (!empty($_testSuites)) {
                    $testSuites   .= $_testSuites;
                    $anyTestSuite  = true;
                }
            } else {
                throw new Exception\TargetUnknownIntermediateRepresentation(
                    'Intermediate representation `%s` has not been handled.',
                    0,
                    get_class($representation)
                );
            }
        }

        if (false === $anyTestSuite) {
            return;
        }

        $fileName = 'hoa://Kitab/Output/' . realpath($file->name);

        Directory::create(dirname($fileName));

        $output = new Write($fileName, Write::MODE_TRUNCATE_WRITE);
        $output->writeAll($testSuites);
        $output->close();
    }

    protected function compileEntity(IntermediateRepresentation\Entity $entity): string
    {
        $testCases = '';

        // Introduction.
        foreach ($this->getCodeBlocks($entity->documentation) as $i => $codeBlock) {
            foreach (
                $this->compileToTestCases(
                    '0introduction_' . $i, // start with `0` to avoid conflict with existing identifier.
                    $codeBlock
                )
                as $testCase
            ) {
                $testCases .= $testCase;
            }
        }

        // Methods
        if ($entity instanceof IntermediateRepresentation\HasMethods) {
            foreach ($entity->getMethods() as $method) {
                foreach ($this->getCodeBlocks($method->documentation) as $i => $codeBlock) {
                    foreach (
                        $this->compileToTestCases(
                            $method->name . '_' . $i,
                            $codeBlock
                        )
                        as $testCase
                    ) {
                        $testCases .= $testCase;
                    }
                }
            }
        }

        if (null === $testCases) {
            return null;
        }

        return
            sprintf(
                "\n\n" . 'namespace Kitab\Generated\DocTest%s' . "\n" . '{' . "\n\n" .
                'class %s extends \Kitab\DocTest\Suite' . "\n" .
                '{',
                $entity->inNamespace() ? '\\' . $entity->getNamespaceName() : '',
                $this->computeTestSuiteShortName($entity->getShortName(), $entity->name)
            ) .
            $testCases .
            '}' . "\n\n" .
            '}';


        return $testCases;
    }

    public function assemble(array $symbols)
    {
        return;
    }

    protected function getCodeBlocks(IntermediateRepresentation\Documentation $documentation = null): Generator
    {
        if (empty($documentation) ||
            empty($documentation->documentation)) {
            return;
        }

        yield from $this->parseCodeBlocks(
            $this->getMarkdownParser()->parse($documentation->documentation)->walker()
        );
    }

    protected function parseCodeBlocks(CommonMark\Node\NodeWalker $walker): Generator
    {
        $hashes = [];

        while ($event = $walker->next()) {
            $node = $event->getNode();

            if (false === $event->isEntering() ||
                !($node instanceof CommonMark\Block\Element\Heading) ||
                1 !== $node->getLevel() ||
                (self::EXAMPLES_SECTION !== $node->getStringContent() &&
                 self::EXCEPTIONS_SECTION !== $node->getStringContent())) {
                continue;
            }

            while ($childEvent = $walker->next()) {
                $childNode = $childEvent->getNode();

                if ($childNode instanceof CommonMark\Block\Element\Heading &&
                    self::EXAMPLES_SECTION !== $childNode->getStringContent() &&
                    self::EXCEPTIONS_SECTION !== $childNode->getStringContent()) {

                    break;
                }

                if (false === $event->isEntering() ||
                    !($childNode instanceof CommonMark\Block\Element\FencedCode)) {
                    continue;
                }

                $hash = spl_object_hash($childNode);

                if (true === in_array($hash, $hashes)) {
                    continue;
                } else {
                    $hashes[] = $hash;
                }

                yield [
                    'type'    => trim($childNode->getInfo()),
                    'content' => $childNode->getStringContent()
                ];
            }
        }
    }

    protected function compileToTestCases(string $testCaseName, array $codeBlock): Generator
    {
        if (empty($this->_codeBlockHandlers)) {
            throw new RuntimeException(
                'Target `' . __CLASS__ . '` has no code block handlers. ' .
                'Use the `' . __CLASS__ . '::addCodeBlockHandler` method to add ' .
                'at least one code block handler to compile test cases.'
            );
        }

        $suffix = "\n" . '    }' . "\n";

        foreach ($this->_codeBlockHandlers as $codeBlockHandler) {
            $prefix =
                "\n" .
                '    public function case_' . $testCaseName . '_' . $codeBlockHandler->getDefinitionName() . '()' . "\n" .
                '    {' . "\n";

            if (false === $codeBlockHandler->mightHandleCodeBlock($codeBlock['type'])) {
                yield
                    $prefix .
                    '        ' .
                    sprintf(
                        '$this->skip(\'Skipped because there is no handler for the code block of type `%s`.\');',
                        $codeBlock['type']
                    ) .
                    $suffix;

                continue;
            }

            yield
                $prefix .
                '        ' .
                str_replace(
                    "\n",
                    "\n" . '        ',
                    $codeBlockHandler->compileToTestCaseBody(
                        $codeBlock['type'],
                        $codeBlock['content']
                    )
                ) .
                $suffix;
        }
    }

    protected function getMarkdownParser()
    {
        if (null === static::$_markdownParser) {
            static::$_markdownParser = new CommonMark\DocParser(
                CommonMark\Environment::createCommonMarkEnvironment()
            );
        }

        return static::$_markdownParser;
    }

    protected function computeTestSuiteShortName(string $shortName, string $longName): string
    {
        if (false === isset($this->_generatedTestSuites[$longName])) {
            $this->_generatedTestSuites[$longName] = 1;

            return $shortName;
        }

        return $shortName . '__' . $this->_generatedTestSuites[$longName]++;
    }
}