<?php
/**
* Whoops - php errors for cool kids
* @author Filipe Dobreira <http://github.com/filp>
* Plaintext handler for command line and logs.
* @author Pierre-Yves Landuré <https://howto.biapy.com/>
*/

namespace Whoops\Handler;
use Whoops\Handler\Handler;
use InvalidArgumentException;
use Whoops\Exception\Frame;
use Psr\Log\LoggerInterface;

/**
* Handler outputing plaintext error messages. Can be used
* directly, or will be instantiated automagically by Whoops\Run
* if passed to Run::pushHandler
*/
class PlainTextHandler extends Handler
{
    const VAR_DUMP_PREFIX = '   | ';

    /**
     * @var Psr\Log\LoggerInterface
     */
    protected $logger;

    /**
     * @var bool
     */
    private $addTraceToOutput = true;

    /**
     * @var bool|integer
     */
    private $addTraceFunctionArgsToOutput = false;

    /**
     * @var integer
     */
    private $traceFunctionArgsOutputLimit = 1024;

    /**
     * @var bool
     */
    private $onlyForCommandLine = false;

    /**
     * @var bool
     */
    private $outputOnlyIfCommandLine = true;

    /**
     * @var bool
     */
    private $loggerOnly = false;

    /**
     * Constructor.
     * @throws InvalidArgumentException If argument is not null or a LoggerInterface
     * @param Psr\Log\LoggerInterface|null $logger
     */
    public function __construct($logger = null)
    {
        $this->setLogger($logger);
    }

    /**
     * Set the output logger interface.
     * @throws InvalidArgumentException If argument is not null or a LoggerInterface
     * @param Psr\Log\LoggerInterface|null $logger
     */
    public function setLogger($logger = null)
    {
        if(! (is_null($logger)
            || $logger InstanceOf LoggerInterface)) {
            throw new InvalidArgumentException(
                'Argument to ' . __METHOD__ .
                " must be a valid Logger Interface (aka. Monolog), " .
                get_class($logger) . ' given.'
            );
        }

        $this->logger = $logger;
    }

    /**
     * @return Psr\Log\LoggerInterface|null
     */
    public function getLogger()
    {
        return $this->logger;
    }

    /**
     * Add error trace to output.
     * @param bool|null $addTraceToOutput
     * @return bool|$this
     */
    public function addTraceToOutput($addTraceToOutput = null)
    {
        if(func_num_args() == 0) {
            return $this->addTraceToOutput;
        }

        $this->addTraceToOutput = (bool) $addTraceToOutput;
        return $this;
    }

    /**
     * Add error trace function arguments to output.
     * Set to True for all frame args, or integer for the n first frame args.
     * @param bool|integer|null $addTraceFunctionArgsToOutput
     * @return null|bool|integer
     */
    public function addTraceFunctionArgsToOutput($addTraceFunctionArgsToOutput = null)
    {
        if(func_num_args() == 0) {
            return $this->addTraceFunctionArgsToOutput;
        }

        if(! is_integer($addTraceFunctionArgsToOutput)) {
            $this->addTraceFunctionArgsToOutput = (bool) $addTraceFunctionArgsToOutput;
        }
        else {
            $this->addTraceFunctionArgsToOutput = $addTraceFunctionArgsToOutput;
        }
    }

    /**
     * Set the size limit in bytes of frame arguments var_dump output.
     * If the limit is reached, the var_dump output is discarded.
     * Prevent memory limit errors.
     * @var integer
     */
    public function setTraceFunctionArgsOutputLimit($traceFunctionArgsOutputLimit)
    {
        $this->traceFunctionArgsOutputLimit = (integer) $traceFunctionArgsOutputLimit;
    }

    /**
     * Get the size limit in bytes of frame arguments var_dump output.
     * If the limit is reached, the var_dump output is discarded.
     * Prevent memory limit errors.
     * @return integer
     */
    public function getTraceFunctionArgsOutputLimit()
    {
        return $this->traceFunctionArgsOutputLimit;
    }

    /**
     * Restrict error handling to command line calls.
     * @param bool|null $onlyForCommandLine
     * @return null|bool
     */
    public function onlyForCommandLine($onlyForCommandLine = null)
    {
        if(func_num_args() == 0) {
            return $this->onlyForCommandLine;
        }
        $this->onlyForCommandLine = (bool) $onlyForCommandLine;
    }

    /**
     * Output the error message only if using command line.
     * else, output to logger if available.
     * Allow to safely add this handler to web pages.
     * @param bool|null $outputOnlyIfCommandLine
     * @return null|bool
     */
    public function outputOnlyIfCommandLine($outputOnlyIfCommandLine = null)
    {
        if(func_num_args() == 0) {
            return $this->outputOnlyIfCommandLine;
        }
        $this->outputOnlyIfCommandLine = (bool) $outputOnlyIfCommandLine;
    }

    /**
     * Only output to logger.
     * @param bool|null $loggerOnly
     * @return null|bool
     */
    public function loggerOnly($loggerOnly = null)
    {
        if(func_num_args() == 0) {
            return $this->loggerOnly;
        }

        $this->loggerOnly = (bool) $loggerOnly;
    }

    /**
     * Check, if possible, that this execution was triggered by a command line.
     * @return bool
     */
    private function isCommandLine()
    {
        return PHP_SAPI == 'cli';
    }

    /**
     * Test if handler can process the exception..
     * @return bool
     */
    private function canProcess()
    {
        return $this->isCommandLine() || !$this->onlyForCommandLine();
    }

    /**
     * Test if handler can output to stdout.
     * @return bool
     */
    private function canOutput()
    {
        return ($this->isCommandLine() || ! $this->outputOnlyIfCommandLine())
            && ! $this->loggerOnly();
    }

    /**
     * Get the frame args var_dump.
     * @param  \Whoops\Exception\Frame $frame [description]
     * @param  integer $line  [description]
     * @return string
     */
    private function getFrameArgsOutput(Frame $frame, $line)
    {
        if($this->addTraceFunctionArgsToOutput() === false
            || $this->addTraceFunctionArgsToOutput() < $line) {
            return '';
        }

        // Dump the arguments:
        ob_start();
        var_dump($frame->getArgs());
        if(ob_get_length() > $this->getTraceFunctionArgsOutputLimit()) {
            // The argument var_dump is to big.
            // Discarded to limit memory usage.
            ob_clean();
            return sprintf(
                "\n%sArguments dump length greater than %d Bytes. Discarded.",
                self::VAR_DUMP_PREFIX,
                $this->getTraceFunctionArgsOutputLimit()
            );
        }

        return sprintf("\n%s",
            preg_replace('/^/m', self::VAR_DUMP_PREFIX, ob_get_clean())
        );
    }

    /**
     * Get the exception trace as plain text.
     * @return string
     */
    private function getTraceOutput()
    {
        if(! $this->addTraceToOutput()) {
            return '';
        }
        $inspector = $this->getInspector();
        $frames = $inspector->getFrames();

        $response = "\nStack trace:";

        $line = 1;
        foreach($frames as $frame) {
            /** @var Frame $frame */
            $class = $frame->getClass();

            $template = "\n%3d. %s->%s() %s:%d%s";
            if(! $class) {
                // Remove method arrow (->) from output.
                $template = "\n%3d. %s%s() %s:%d%s";
            }

            $response .= sprintf(
                $template,
                $line,
                $class,
                $frame->getFunction(),
                $frame->getFile(),
                $frame->getLine(),
                $this->getFrameArgsOutput($frame, $line)
            );

            $line++;
        }

        return $response;
    }

    /**
     * @return int
     */
    public function handle()
    {
        if(! $this->canProcess()) {
            return Handler::DONE;
        }

        $exception = $this->getException();

        $response = sprintf("%s: %s in file %s on line %d%s\n",
                get_class($exception),
                $exception->getMessage(),
                $exception->getFile(),
                $exception->getLine(),
                $this->getTraceOutput()
            );

        if($this->getLogger()) {
            $this->getLogger()->error($response);
        }

        if(! $this->canOutput()) {
            return Handler::DONE;
        }

        if(class_exists('\Whoops\Util\Misc')
            && \Whoops\Util\Misc::canSendHeaders()) {
            header('Content-Type: text/plain');
        }

        echo $response;

        return Handler::QUIT;
    }
}
