<?php

/**
 * This file is part of Collision.
 *
 * (c) Nuno Maduro <enunomaduro@gmail.com>
 *
 *  For the full copyright and license information, please view the LICENSE
 *  file that was distributed with this source code.
 */

namespace NunoMaduro\Collision;

use Whoops\Exception\Frame;
use Whoops\Exception\Inspector;
use Symfony\Component\Console\Output\ConsoleOutput;
use Symfony\Component\Console\Output\OutputInterface;
use NunoMaduro\Collision\Contracts\Writer as WriterContract;
use NunoMaduro\Collision\Contracts\Highlighter as HighlighterContract;
use NunoMaduro\Collision\Contracts\ArgumentFormatter as ArgumentFormatterContract;

/**
 * This is an Collision Writer implementation.
 *
 * @author Nuno Maduro <enunomaduro@gmail.com>
 */
class Writer implements WriterContract
{
    /**
     * The number of frames if no verbosity is specified.
     */
    const VERBOSITY_NORMAL_FRAMES = 1;

    /**
     * Holds an instance of the Output.
     *
     * @var \Symfony\Component\Console\Output\OutputInterface
     */
    protected $output;

    /**
     * Holds an instance of the Argument Formatter.
     *
     * @var \NunoMaduro\Collision\Contracts\ArgumentFormatter
     */
    protected $argumentFormatter;

    /**
     * Holds an instance of the Highlighter.
     *
     * @var \NunoMaduro\Collision\Contracts\Highlighter
     */
    protected $highlighter;

    /**
     * Ignores traces where the file string matches one
     * of the provided regex expressions.
     *
     * @var string[]
     */
    protected $ignore = [];

    /**
     * Declares whether or not the trace should appear.
     *
     * @var bool
     */
    protected $showTrace = true;

    /**
     * Declares whether or not the editor should appear.
     *
     * @var bool
     */
    protected $showEditor = true;

    /**
     * Creates an instance of the writer.
     *
     * @param \Symfony\Component\Console\Output\OutputInterface|null $output
     * @param \NunoMaduro\Collision\Contracts\ArgumentFormatter|null $argumentFormatter
     * @param \NunoMaduro\Collision\Contracts\Highlighter|null $highlighter
     */
    public function __construct(
        OutputInterface $output = null,
        ArgumentFormatterContract $argumentFormatter = null,
        HighlighterContract $highlighter = null
    ) {
        $this->output = $output ?: new ConsoleOutput;
        $this->argumentFormatter = $argumentFormatter ?: new ArgumentFormatter;
        $this->highlighter = $highlighter ?: new Highlighter;
    }

    /**
     * {@inheritdoc}
     */
    public function write(Inspector $inspector): void
    {
        $this->renderTitle($inspector);

        $frames = $this->getFrames($inspector);

        $editorFrame = array_shift($frames);
        if ($this->showEditor && $editorFrame !== null) {
            $this->renderEditor($editorFrame);
        }

        if ($this->showTrace && ! empty($frames)) {
            $this->renderTrace($frames);
        } else {
            $this->output->writeln('');
        }
    }

    /**
     * {@inheritdoc}
     */
    public function ignoreFilesIn(array $ignore): WriterContract
    {
        $this->ignore = $ignore;

        return $this;
    }

    /**
     * {@inheritdoc}
     */
    public function showTrace(bool $show): WriterContract
    {
        $this->showTrace = $show;

        return $this;
    }

    /**
     * {@inheritdoc}
     */
    public function showEditor(bool $show): WriterContract
    {
        $this->showEditor = $show;

        return $this;
    }

    /**
     * {@inheritdoc}
     */
    public function setOutput(OutputInterface $output): WriterContract
    {
        $this->output = $output;

        return $this;
    }

    /**
     * {@inheritdoc}
     */
    public function getOutput(): OutputInterface
    {
        return $this->output;
    }

    /**
     * Returns pertinent frames.
     *
     * @param  \Whoops\Exception\Inspector $inspector
     *
     * @return array
     */
    protected function getFrames(Inspector $inspector): array
    {
        return $inspector->getFrames()
            ->filter(
                function ($frame) {
                    foreach ($this->ignore as $ignore) {
                        if (preg_match($ignore, $frame->getFile())) {
                            return false;
                        }
                    }

                    return true;
                }
            )
            ->getArray();
    }

    /**
     * Renders the title of the exception.
     *
     * @param \Whoops\Exception\Inspector $inspector
     *
     * @return \NunoMaduro\Collision\Contracts\Writer
     */
    protected function renderTitle(Inspector $inspector): WriterContract
    {
        $exception = $inspector->getException();
        $message = $exception->getMessage();
        $class = $inspector->getExceptionName();

        $this->render("<bg=red;options=bold> $class </> : <comment>$message</>");

        return $this;
    }

    /**
     * Renders the editor containing the code that was the
     * origin of the exception.
     *
     * @param \Whoops\Exception\Frame $frame
     *
     * @return \NunoMaduro\Collision\Contracts\Writer
     */
    protected function renderEditor(Frame $frame): WriterContract
    {
        $this->render('at <fg=green>'.$frame->getFile().'</>'.':<fg=green>'.$frame->getLine().'</>');

        $content = $this->highlighter->highlight((string) $frame->getFileContents(), (int) $frame->getLine());

        $this->output->writeln($content);

        return $this;
    }

    /**
     * Renders the trace of the exception.
     *
     * @param  array $frames
     *
     * @return \NunoMaduro\Collision\Contracts\Writer
     */
    protected function renderTrace(array $frames): WriterContract
    {
        $this->render('<comment>Exception trace:</comment>');
        foreach ($frames as $i => $frame) {
            if ($i > static::VERBOSITY_NORMAL_FRAMES && $this->output->getVerbosity(
                ) < OutputInterface::VERBOSITY_VERBOSE) {
                $this->render('<info>Please use the argument <fg=red>-v</> to see more details.</info>');
                break;
            }

            $file = $frame->getFile();
            $line = $frame->getLine();
            $class = empty($frame->getClass()) ? '' : $frame->getClass().'::';
            $function = $frame->getFunction();
            $args = $this->argumentFormatter->format($frame->getArgs());
            $pos = str_pad((int) $i + 1, 4, ' ');

            $this->render("<comment><fg=cyan>$pos</>$class$function($args)</comment>");
            $this->render("    <fg=green>$file</>:<fg=green>$line</>", false);
        }

        return $this;
    }

    /**
     * Renders an message into the console.
     *
     * @param  string $message
     * @param  bool $break
     *
     * @return $this
     */
    protected function render(string $message, bool $break = true): WriterContract
    {
        if ($break) {
            $this->output->writeln('');
        }

        $this->output->writeln("  $message");

        return $this;
    }
}
