<?php namespace October\Rain\Exception;

use App;
use Request;
use Throwable;

/**
 * ErrorHandler handles application exception events.
 *
 * @package october\exception
 * @author Alexey Bobkov, Samuel Georges
 */
class ErrorHandler
{
    /**
     * @var \System\Classes\ExceptionBase activeMask used to mask any exception fired.
     */
    protected static $activeMask;

    /**
     * @var array maskLayers is  a collection of masks, so multiples can be applied in order.
     */
    protected static $maskLayers = [];

    /**
     * @var array notFoundExceptions will redirect to the 404 page when captured.
     */
    protected $notFoundExceptions = [
        \October\Rain\Exception\NotFoundException::class,
        \Illuminate\Database\Eloquent\ModelNotFoundException::class,
        \Symfony\Component\HttpKernel\Exception\NotFoundHttpException::class
    ];

    /**
     * handleException handles all exceptions from the framework workflow. This method will mask
     * any foreign exceptions with a "scent" of the native application's exception, so it can
     * render correctly when displayed on the error page.
     * @param Throwable $proposedException The exception candidate that has been thrown.
     * @return mixed Error page contents
     */
    public function handleException(Throwable $proposedException)
    {
        // Disable the error handler for test and CLI environment
        if (App::runningUnitTests() || App::runningInConsole()) {
            return;
        }

        // Detect AJAX request and use error 500
        if (Request::ajax()) {
            return $proposedException instanceof AjaxException
                 ? $proposedException->getContents()
                 : static::getDetailedMessage($proposedException);
        }

        // Clear the output buffer
        while (ob_get_level()) {
            ob_end_clean();
        }

        // Friendly error pages are used
        if (($customError = $this->handleCustomError($proposedException)) !== null) {
            return $customError;
        }

        // If the exception is already our brand, use it
        if ($proposedException instanceof ExceptionBase) {
            $exception = $proposedException;
        }
        // If there is an active mask prepared, use that
        elseif (static::$activeMask !== null) {
            $exception = static::$activeMask;
            $exception->setMask($proposedException);
        }
        // Otherwise we should mask it with our own default scent
        else {
            $exception = new ApplicationException($proposedException->getMessage(), 0);
            $exception->setMask($proposedException);
        }

        return $this->handleDetailedError($exception);
    }

    /**
     * isNotFoundException returns true if the exception is 404-flavored
     */
    protected function isNotFoundException($exception)
    {
        foreach ($this->notFoundExceptions as $type) {
            if ($exception instanceof $type) {
                return true;
            }
        }
        return false;
    }

    /**
     * applyMask prepares a mask exception to be used when any exception fires.
     * @param Exception $exception The mask exception.
     * @return void
     */
    public static function applyMask(Throwable $exception)
    {
        if (static::$activeMask !== null) {
            array_push(static::$maskLayers, static::$activeMask);
        }

        static::$activeMask = $exception;
    }

    /**
     * removeMask destroys the prepared mask by applyMask()
     * @return void
     */
    public static function removeMask()
    {
        if (count(static::$maskLayers) > 0) {
            static::$activeMask = array_pop(static::$maskLayers);
        }
        else {
            static::$activeMask = null;
        }
    }

    /**
     * getDetailedMessage returns a more descriptive error message.
     * @param Exception $exception
     * @return string
     */
    public static function getDetailedMessage($exception)
    {
        return sprintf(
            '"%s" on line %s of %s',
            $exception->getMessage(),
            $exception->getLine(),
            $exception->getFile()
        );
    }

    //
    // Overrides
    //

    /**
     * handleCustomError checks if using a custom error page, if so return the contents.
     * Return NULL if a custom error is not set up.
     * @return mixed
     */
    public function handleCustomError($exception)
    {
    }

    /**
     * handleDetailedError displays the detailed system exception page.
     * @return View Object containing the error page.
     */
    public function handleDetailedError($exception)
    {
        return 'Error: ' . $exception->getMessage();
    }
}
