<?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;

use Hoa\File;
use Kitab\Exception;
use PhpParser\Error;
use PhpParser\NodeTraverser;
use PhpParser\NodeVisitor;
use PhpParser\ParserFactory;
use PhpParser\Parser\Multiple as ParserMultiple;
use PhpParser\PrettyPrinter;

/**
 * A parser producing an Intermediate Representation.
 *
 * This parser takes one file, parses it, generates an Abstract Syntax Tree,
 * and transforms it into an Intermediate Representation.
 *
 * The PHP 7 form is prefered over PHP 5 and lower forms, it means it will try
 * to parse with PHP 7 strategy first. They are small but subtle [differences
 * with previous PHP
 * versions](http://php.net/manual/en/migration70.incompatible.php#migration70.incompatible.variable-handling).
 *
 * All the work done by the parser is delegated to
 * [PHP-Parser](https://github.com/nikic/PHP-Parser).
 */
class Parser
{
    /**
     * The PHP parser, aka [PHP-Parser](https://github.com/nikic/PHP-Parser),
     * that is used to parse PHP files.
     *
     * The PHP parser is allocated once, hence the static declaration.
     */
    protected static $_phpParser        = null;

    /**
     * A traverser, for PHP-Parser, is a set of visitors visiting the Abstract
     * Syntax Tree produced by the parser. This traverser will be used to
     * apply visitors on the AST. It always contain a name resolver visitor,
     * to get fully qualified names everywhere in the code.
     *
     * The traverser is allocated once, hence the static declaration.
     */
    protected static $_phpTraverser     = null;

    /**
     * Pretty print visitor to transform a PHP AST into its PHP representation.
     *
     * The pretty printer is allocated once, hence the static declaration.
     */
    protected static $_phpPrettyPrinter = null;

    /**
     * The `parse` methods parses a file aiming at containing PHP code, and
     * produces the Intermediate Representation of it if valid. [Get more
     * information about the general workflow](kitab/compiler/index.html).
     *
     * # Examples
     *
     * ```php,ignore
     * $file   = new Hoa\File\SplFileInfo('path/to/a/file.php');
     * $parser = new Kitab\Compiler\Parser();
     *
     * $intermediateRepresentation = $parser->parse($file);
     * ```
     *
     * # Exceptions
     *
     * The `Kitab\Exception\PhpParserError` can be thrown if the given file
     * does not contain valid PHP code. The exception contains the `Error`
     * exception from PHP-Parser, which holds more information, as a previous
     * exception.
     */
    public function parse(File\SplFileInfo $file): IntermediateRepresentation\File
    {
        $phpParser = self::getPhpParser();
        $fileName  = $file->getPathName();

        try {
            $statements = $phpParser->parse(file_get_contents($fileName));
        } catch (Error $e) {
            throw new Exception\PhpParserError(
                'A syntax error has been found in the file `%s`:' . "\n" .
                '> %s',
                0,
                [$fileName, $e->getMessage()],
                $e
            );
        }

        return $this->intoIntermediateRepresentation($statements, $fileName);
    }

    /**
     * Transform the Abstract Syntax Tree into its Intermediate Representation.
     *
     * In PHP-Parser, the AST is a hashmap of n-dimensions of statements. The
     * produced IR is a tree of structures. The file name of the original file
     * is kept to provide more context.
     *
     * The transformation is applied by a visitor. It is added on the
     * traverser (see `self::$_phpTraverser`), run, and remove. The visitor
     * contains the resulting IR.
     */
    protected function intoIntermediateRepresentation(
        array $statements,
        string $fileName
    ): IntermediateRepresentation\File {
        $intoIR = new IntermediateRepresentation\Into($fileName);

        $traverser = self::getTraverser();
        $traverser->addVisitor($intoIR);
        $traverser->traverse($statements);
        $traverser->removeVisitor($intoIR);

        return $intoIR->collect();
    }

    /**
     * Get the statically allocated PHP-Parser instance.
     */
    public static function getPhpParser(): ParserMultiple
    {
        if (null === self::$_phpParser) {
            self::$_phpParser = (new ParserFactory())->create(ParserFactory::PREFER_PHP7);
        }

        return self::$_phpParser;
    }

    /**
     * Get the statically allocated traverser instance.
     */
    protected static function getTraverser(): NodeTraverser
    {
        if (null === self::$_phpTraverser) {
            self::$_phpTraverser = new NodeTraverser();
            self::$_phpTraverser->addVisitor(new NodeVisitor\NameResolver());
        }

        return self::$_phpTraverser;
    }

    /**
     * Get the statically allocated pretty printer
     */
    public static function getPhpPrettyPrinter(): PrettyPrinter\Standard
    {
        if (null === self::$_phpPrettyPrinter) {
            self::$_phpPrettyPrinter = new PrettyPrinter\Standard(['shortArraySyntax' => true]);
        }

        return self::$_phpPrettyPrinter;
    }

    /**
     * Extract content from a comment of kind block (`/**`).
     *
     * This is a small utility used to extract documentation from the code.
     *
     * # Examples
     *
     * ```php
     * $content = 'foobar';
     * $input   = '/**' . $content . '*' . '/';
     *
     * assert(Kitab\Compiler\Parser::extractFromComment($input) === $content);
     * ```
     */
    public static function extractFromComment($comment)
    {
        return preg_replace(
            ',(^(/\*\*|\h*\*/?\h?)|\*/$),m',
            '',
            $comment
        );
    }
}