<?php
/**
 * @link      https://github.com/aercolino/ando-php
 * @copyright Copyright (c) 2015 Andrea Ercolino
 * @license   https://github.com/aercolino/ando-php/blob/master/LICENSE
 */

//@Formatter:off
/**
 * A simple reference copied from the PHP documentation.
 *
 *     1    E_ERROR                Fatal run-time errors. Errors that cannot be recovered from. Execution of the script
 *                                 is halted.
 *     2    E_WARNING              Run-time warnings (non-fatal errors). Execution of the script is not halted.
 *     4    E_PARSE                Compile-time parse errors. Parse errors should only be generated by the parser.
 *     8    E_NOTICE               Run-time notices. The script found something that might be an error, but could also
 *                                 happen when running a script normally.
 *    16    E_CORE_ERROR           Fatal errors at PHP startup. This is like E_ERROR, except it is generated by the
 *                                 core of PHP.
 *    32    E_CORE_WARNING         Non-fatal errors at PHP startup. This is like E_WARNING, except it is generated by
 *                                 the core of PHP.
 *    64    E_COMPILE_ERROR        Fatal compile-time errors. This is like E_ERROR, except it is generated by by the
 *                                 Zend Scripting Engine.
 *   128    E_COMPILE_WARNING      Non-fatal compile-time errors. This is like E_WARNING, except it is generated by by
 *                                 the Zend Scripting Engine.
 *   256    E_USER_ERROR           Fatal user-generated error. This is like E_ERROR, except it is generated in PHP code
 *                                 by using the PHP function trigger_error().
 *   512    E_USER_WARNING         Non-fatal user-generated warning. This is like E_WARNING, except it is generated in
 *                                 PHP code by using the PHP function trigger_error().
 *  1024    E_USER_NOTICE          User-generated notice. This is like E_NOTICE, except it is generated in PHP code by
 *                                 using the PHP function trigger_error().
 *  2048    E_STRICT               Enable to have PHP suggest changes to your code which will ensure the best
 *                                 interoperability and forward compatibility of your code. (Since PHP 5 but not
 *                                 included in E_ALL until PHP 5.4)
 *  4096    E_RECOVERABLE_ERROR    Catchable fatal error. Indicates that a probably dangerous error occurred, but did
 *                                 not leave the Engine in an unstable state. If the error is not caught by a user
 *                                 defined handle, the application aborts as it was an E_ERROR. (Since PHP 5.2)
 *  8192    E_DEPRECATED           Run-time notices. Enable this to receive warnings about code that will not work in
 *                                 future versions. (Since PHP 5.3)
 * 16384    E_USER_DEPRECATED      User-generated warning message. This is like E_DEPRECATED, except it is generated in
 *                                 PHP code by using the PHP function trigger_error(). (Since PHP 5.3)
 */
//@Formatter:on

/**
 * Class Ando_ErrorFactory
 */
class Ando_ErrorFactory
{
    // -----------------------------------------------------------------------------------------------INTERNAL SINGLETON

    /**
     * Map error names to error values.
     *
     * @var int[]
     */
    protected $map_to_int = array(
            'E_ERROR'             => E_ERROR,
            'E_WARNING'           => E_WARNING,
            'E_PARSE'             => E_PARSE,
            'E_NOTICE'            => E_NOTICE,
            'E_CORE_ERROR'        => E_CORE_ERROR,
            'E_CORE_WARNING'      => E_CORE_WARNING,
            'E_COMPILE_ERROR'     => E_COMPILE_ERROR,
            'E_COMPILE_WARNING'   => E_COMPILE_WARNING,
            'E_USER_ERROR'        => E_USER_ERROR,
            'E_USER_WARNING'      => E_USER_WARNING,
            'E_USER_NOTICE'       => E_USER_NOTICE,
            'E_STRICT'            => E_STRICT,
            'E_RECOVERABLE_ERROR' => E_RECOVERABLE_ERROR,
            'E_DEPRECATED'        => E_DEPRECATED,
            'E_USER_DEPRECATED'   => E_USER_DEPRECATED,
    );

    /**
     * Map error values to error names.
     *
     * @var string[]
     */
    protected $map_to_str = array();

    protected
    function __construct()
    {
        $this->map_to_str = array_flip($this->map_to_int);
    }

    /**
     * Internal singleton.
     *
     * @var Ando_ErrorFactory
     */
    protected static $instance;

    /**
     * Get the singleton.
     *
     * @return Ando_ErrorFactory
     */
    protected static
    function instance()
    {
        if ( is_null(self::$instance) ) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    // ---------------------------------------------------------------------------------------------CATEGORIES OF ERRORS

    /**
     * All errors, as an array of integers.
     *
     * @return int[]
     */
    public static
    function all_errors()
    {
        $result = array_values(self::instance()->map_to_int);
        return $result;
    }

    /**
     * All errors, as an array of strings.
     *
     * @return string[]
     */
    public static
    function all_errors_to_str()
    {
        $result = array_keys(self::instance()->map_to_int);
        return $result;
    }

    /**
     * Non catchable errors cannot be handled from an error handler.
     *
     * @link https://github.com/php/php-src/blob/fc33f52d8c25997dd0711de3e07d0dc260a18c11/Zend/zend.c#L1078
     *
     * @return int[]
     */
    public static
    function non_catchable_errors()
    {
        return array(
                E_ERROR, E_PARSE, E_CORE_ERROR, E_CORE_WARNING, E_COMPILE_ERROR, E_COMPILE_WARNING
        );
    }

    /**
     * Catchable errors can be handled from an error handler.
     *
     * @return int[]
     */
    public static
    function catchable_errors()
    {
        return array_diff(self::all_errors(), self::non_catchable_errors());
    }

    /**
     * Non catchable shutdown errors cannot be handled from an error handler and jump to script shutdown.
     *
     * @return int[]
     */
    public static
    function non_catchable_shutdown_errors()
    {
        return array(
                E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR
        );
    }

    /**
     * Catchable shutdown errors jump to script shutdown unless they are handled from an error handler.
     *
     * @return int[]
     */
    public static
    function catchable_shutdown_errors()
    {
        return array(
                E_RECOVERABLE_ERROR, E_USER_ERROR
        );
    }

    /**
     * Shutdown errors jump to script shutdown.
     *
     * @return int[]
     */
    public static
    function shutdown_errors()
    {
        return array_merge(self::non_catchable_shutdown_errors(), self::catchable_shutdown_errors());
    }

    /**
     * Non shutdown errors do not jump to script shutdown.
     *
     * @return int[]
     */
    public static
    function non_shutdown_errors()
    {
        return array_diff(self::all_errors(), self::shutdown_errors());
    }

    /**
     * Catchable non shutdown errors can be handled from an error handler and do not jump to script shutdown.
     *
     * @return int[]
     */
    public static
    function catchable_non_shutdown_errors()
    {
        return array_intersect(self::catchable_errors(), self::non_shutdown_errors());
    }

    /**
     * Non catchable non shutdown errors cannot be handled from an error handler but do not jump to script shutdown.
     *
     * @return int[]
     */
    public static
    function non_catchable_non_shutdown_errors()
    {
        return array_intersect(self::non_catchable_errors(), self::non_shutdown_errors());
    }

    /**
     * The type of the error according to PHP. It's kind of legacy...
     *
     * @link https://github.com/php/php-src/blob/fc33f52d8c25997dd0711de3e07d0dc260a18c11/main/main.c#L1083
     *
     * @param $error
     *
     * @return string
     */
    public static
    function php_error_type( $error )
    {
        switch ($error) {
            case E_ERROR:
            case E_CORE_ERROR:
            case E_COMPILE_ERROR:
            case E_USER_ERROR:
                $result = "Fatal error";
                break;
            case E_RECOVERABLE_ERROR:
                $result = "Catchable fatal error";
                break;
            case E_WARNING:
            case E_CORE_WARNING:
            case E_COMPILE_WARNING:
            case E_USER_WARNING:
                $result = "Warning";
                break;
            case E_PARSE:
                $result = "Parse error";
                break;
            case E_NOTICE:
            case E_USER_NOTICE:
                $result = "Notice";
                break;
            case E_STRICT:
                $result = "Strict Standards";
                break;
            case E_DEPRECATED:
            case E_USER_DEPRECATED:
                $result = "Deprecated";
                break;
            default:
                $result = "Unknown error";
                break;
        }
        return $result;
    }

    // ---------------------------------------------------------------------------------------------CONVERSION OF ERRORS

    /**
     * Not an error.
     */
    const NO_ERROR = 0;
    const NO_ERROR_STR = 'NO_ERROR';

    /**
     * Unknown error.
     */
    const UNKNOWN_ERROR = -1;
    const UNKNOWN_ERROR_STR = 'UNKNOWN_ERROR';

    /**
     * Convert errors from string (or int) to int (or string), using unknown for unmapped values.
     *
     * @param int|int[]|string|string[] $errors
     * @param string[]|int[]            $map
     * @param string|int                $unknown
     *
     * @return string[]|int[]
     */
    protected static
    function convert( $errors, $map, $unknown )
    {
        $was_array = true;
        if ( ! is_array($errors) ) {
            $was_array = false;
            $errors    = array($errors);
        }
        $result = array();
        foreach ($errors as $error) {
            $result[] = isset($map[$error])
                    ? $map[$error]
                    : $unknown;
        }
        if ( ! $was_array ) {
            list($result) = $result;
        }
        return $result;
    }

    /**
     * Convert errors from int to string.
     *
     * @param int|int[] $errors
     *
     * @return string|string[]
     */
    public static
    function to_str( $errors )
    {
        $result = self::convert($errors, self::instance()->map_to_str, self::UNKNOWN_ERROR_STR);
        return $result;
    }

    /**
     * Convert errors from string to int.
     *
     * @param string|string[] $errors
     *
     * @return int[]
     */
    public static
    function to_int( $errors )
    {
        $result = self::convert($errors, self::instance()->map_to_int, self::UNKNOWN_ERROR);
        return $result;
    }

    /**
     * Convert a set of errors to a mask of bits.
     *
     * @param int|int[] $errors
     *
     * @return int
     */
    public static
    function to_mask( $errors )
    {
        if ( ! is_array($errors) ) {
            return (int) $errors;
        }
        $result = 0;
        foreach ($errors as $error) {
            $result = $result | (int) $error;
        }
        return $result;
    }

    /**
     * Convert a mask of bits to a set of errors.
     *
     * @param int $mask
     *
     * @return int[]
     */
    public static
    function to_array( $mask )
    {
        $mask   = (int) $mask;
        $result = array();
        foreach (self::all_errors() as $error) {
            if ( $error & $mask ) {
                $result[] = $error;
            }
        }
        return $result;
    }

    // ----------------------------------------------------------------------------QUICK AND DIRTY VISUALIZERS OF ERRORS

    /**
     * Show info about the error.
     *
     * @param $handler
     * @param $level
     * @param $error
     */
    protected static
    function see_error( $handler, $level, $error )
    {
        $name = $level == self::NO_ERROR
                ? '(none)'
                : self::to_str($level);
        $type = self::php_error_type($level);
        echo "\n$handler: $name ($type)\n";
        if ( '(none)' != $name ) {
            echo "\nerror = ", print_r($error, true), "\n";
        }
    }

    /**
     * Visualizer for non catchable errors. To be appended to the shutdown handlers queue.
     */
    public static
    function non_catchable_error_visualizer()
    {
        static $count = 0;
        if ( ++$count > 1 ) {
            return;
        }

        $error = error_get_last();
        $level = is_array($error) && isset($error['type']) && is_numeric($error['type'])
                ? (int) $error['type']
                : 0;
        self::see_error('Non catchable error', $level, $error);
        // no backtrace available here...
    }

    protected $catchable_error_handled = true;  // True means no default error processing is needed.

    /**
     * Visualizer for catchable errors. To be pushed onto the error handlers stack (conceptually).
     *
     * @param int    $type
     * @param string $message
     * @param string $file
     * @param int    $line
     * @param array  $context
     *
     * @return bool
     */
    public static
    function catchable_error_visualizer( $type, $message, $file, $line, $context )
    {
        $error = compact('type', 'message', 'file', 'line', 'context');
        self::see_error('Catchable error', $type, $error);
        debug_print_backtrace();
        return self::instance()->catchable_error_handled;
    }

    /**
     * Append the non catchable error visualizer to the shutdown handlers queue.
     */
    public static
    function register_non_catchable_error_visualizer()
    {
        /**
         * Note that
         *   - (1) register_shutdown_function appends the given function to a shutdown callbacks queue, so it must play
         *     nicely with unknown code executed before and after, during the same shutdown;
         *   - (2) it does not check whether the given function is already in the queue or not;
         *   - (3) it's not possible to un-register a registered shutdown function.
         * Therefore, a registered shutdown function, not only must play nicely with other unknown code, but also with
         * possible additional executions of its own code. Tip: Use something like:
         * <code>static $count = 0; if (++$count > 1) return; ...</code>
         */
        register_shutdown_function(array('Ando_ErrorFactory', 'non_catchable_error_visualizer'));
    }

    /**
     * Push the catchable error visualizer onto the error handlers stack (conceptually).
     *
     * @param bool $handled
     *
     * @return mixed Previous handler.
     */
    public static
    function register_catchable_error_visualizer( $handled = true )
    {
        self::instance()->catchable_error_handled = (bool) $handled;

        $result = set_error_handler(array(
                'Ando_ErrorFactory', 'catchable_error_visualizer'
        ));
        return $result;
    }

    // -----------------------------------------------------------------------------------------------TRIGGERS OF ERRORS

    /**
     * Trigger E_ERROR.
     */
    public static
    function E_ERROR()
    {
        eval(self::E_ERROR_code());
    }

    /**
     * Trigger E_WARNING.
     */
    public static
    function E_WARNING()
    {
        eval(self::E_WARNING_code());
    }

    /**
     * Trigger E_PARSE.
     */
    public static
    function E_PARSE()
    {
        eval(self::E_PARSE_code());
    }

    /**
     * Trigger E_NOTICE.
     */
    public static
    function E_NOTICE()
    {
        eval(self::E_NOTICE_code());
    }

    /**
     * Trigger E_CORE_ERROR.
     */
    public static
    function E_CORE_ERROR()
    {
        eval(self::E_CORE_ERROR_code());
    }

    /**
     * By looking at the PHP source files, I discovered that, unlike E_CORE_ERROR, E_CORE_WARNING is triggered only
     * during low level operations, like loading modules during startup, or handling deactivation of signal handlers
     * during shutdown (5.4+). Given that there is no way to interfere with startup from userland, and given that,
     * as much as I can currently understand from the sources, there is neither a way to do it during shutdown because
     * userland shutdown handlers get executed before zend_signal_deactivate, I'm giving up on this, for now.
     *
     * @link http://lxr.php.net/s?refs=zend_signal_deactivate&project=PHP_5_4
     *
     * For the above reasons this method is not public.
     */
    protected static
    function E_CORE_WARNING()
    {
        eval(self::no_way_to_trigger_E_CORE_WARNING_from_userland_yet());
    }

    /**
     * Trigger E_COMPILE_ERROR.
     */
    public static
    function E_COMPILE_ERROR()
    {
        eval(self::E_COMPILE_ERROR_code());
    }

    /**
     * Trigger E_COMPILE_WARNING.
     */
    public static
    function E_COMPILE_WARNING()
    {
        eval(self::E_COMPILE_WARNING_code());
    }

    /**
     * Trigger E_USER_ERROR.
     */
    public static
    function E_USER_ERROR()
    {
        eval(self::E_USER_ERROR_code());
    }

    /**
     * Trigger E_USER_WARNING.
     */
    public static
    function E_USER_WARNING()
    {
        eval(self::E_USER_WARNING_code());
    }

    /**
     * Trigger E_USER_NOTICE.
     */
    public static
    function E_USER_NOTICE()
    {
        eval(self::E_USER_NOTICE_code());
    }

    /**
     * Trigger E_STRICT.
     */
    public static
    function E_STRICT()
    {
        eval(self::E_STRICT_code());
    }

    /**
     * Trigger E_RECOVERABLE_ERROR.
     */
    public static
    function E_RECOVERABLE_ERROR()
    {
        eval(self::E_RECOVERABLE_ERROR_code());
    }

    /**
     * Trigger E_DEPRECATED.
     */
    public static
    function E_DEPRECATED()
    {
        eval(self::E_DEPRECATED_code());
    }

    /**
     * Trigger E_USER_DEPRECATED.
     */
    public static
    function E_USER_DEPRECATED()
    {
        eval(self::E_USER_DEPRECATED_code());
    }

    //--------------------------------------------------------------------------------------------------CODES FOR ERRORS

    /**
     * Code for E_ERROR error by calling an undefined function.
     * 
     * @return string
     */
    public static
    function E_ERROR_code()
    {
        $name = uniqid('oops_');
        $result = "$name();";
        return $result;
    }

    /**
     * Code for E_WARNING error by calling a PHP function with less arguments.
     *
     * @return string
     */
    public static
    function E_WARNING_code()
    {
        $result = "strpos();";
        return $result;
    }

    /**
     * Code for E_NOTICE error by using an undefined constant.
     *
     * @return string
     */
    public static
    function E_NOTICE_code()
    {
        $name = uniqid('oops_');
        $result = "$name;";
        return $result;
    }

    /**
     * Code for E_PARSE error by not using a semicolon.
     *
     * @return string
     */
    public static
    function E_PARSE_code()
    {
        $result = "echo ''";
        return $result;
    }

    /**
     * Code for E_CORE_ERROR error by declaring a class directly implementing Traversable.
     *
     * @return string
     */
    public static
    function E_CORE_ERROR_code()
    {
        $name = uniqid('oops_');
        $result = "class $name implements Traversable {}";
        return $result;
    }

    /**
     * Missing E_CORE_WARNING code.
     *
     * @return string
     */
    protected static
    function no_way_to_trigger_E_CORE_WARNING_from_userland_yet()
    {
        // TODO Find a way to trigger E_CORE_WARNING from userland.

        // You can play with this code if you like, but it doesn't really work.
        // In fact, it only works inside the CLI created by exec "php...".

        $name     = uniqid('oops_');
        $filename = sys_get_temp_dir() . "/$name.ini";
        file_put_contents($filename, "extension=$name");
        $result = "php -c $filename oops.php";
        exec($result, $output, $return);
        unlink($filename);
        
        $result = '';
        return $result;
    }

    /**
     * Code for E_COMPILE_ERROR error by declaring a class called self.
     *
     * @return string
     */
    public static
    function E_COMPILE_ERROR_code()
    {
        $result = 'class self {}';
        return $result;
    }

    /**
     * Code for E_COMPILE_WARNING error by not closing a multiline comment.
     *
     * @return string
     */
    public static
    function E_COMPILE_WARNING_code()
    {
        $name = uniqid('oops_');
        $result = "/* $name";
        return $result;
    }

    /**
     * Code for E_USER_ERROR error by calling trigger_error with type E_USER_ERROR.
     *
     * @return string
     */
    public static
    function E_USER_ERROR_code()
    {
        $name = uniqid('oops_');
        $result = "trigger_error('$name', E_USER_ERROR);";
        return $result;
    }

    /**
     * Code for E_USER_WARNING error by calling trigger_error with type E_USER_WARNING.
     *
     * @return string
     */
    public static
    function E_USER_WARNING_code()
    {
        $name = uniqid('oops_');
        $result = "trigger_error('$name', E_USER_WARNING);";
        return $result;
    }

    /**
     * Code for E_USER_NOTICE error by calling trigger_error with type E_USER_NOTICE.
     *
     * @return string
     */
    public static
    function E_USER_NOTICE_code()
    {
        $name = uniqid('oops_');
        $result = "trigger_error('$name', E_USER_NOTICE);";
        return $result;
    }

    /**
     * Code for E_RECOVERABLE_ERROR error by converting to string an object of a class without __toString.
     *
     * @return string
     */
    public static
    function E_RECOVERABLE_ERROR_code()
    {
        $name = uniqid('oops_');
        $result = "
            class $name{}
            echo new $name();
        ";
        return $result;
    }

    /**
     * Code for E_DEPRECATED error by calling call_user_method.
     *
     * @return string
     */
    public static
    function E_DEPRECATED_code()
    {
        $data = uniqid('oops_');
        $codes = [
            // see http://php.net/manual/en/migration56.deprecated.php
            '5.6' => "
                class A {
                    function f() { echo '$data'; }
                }
                
                class B {
                    function f() { A::f(); }
                }
                
                (new B)->f();
            ",
            // see http://php.net/manual/en/migration70.deprecated.php
            '7.0' => "
                class foo {
                    function bar() {
                        echo '$data';
                    }
                }
                
                foo::bar();
            ",
            // see http://php.net/manual/en/migration71.deprecated.php
            '7.1' => "
                mb_ereg_replace('(.)',\"strtoupper('\\1')\",'$data','e');
            "
        ];
        uksort($codes, function ($a, $b) { return -version_compare($a, $b); });
        foreach ($codes as $version => $code) {
            if (version_compare(PHP_VERSION, $version, '>=')) {
                return $code;
            }
        }
        return null;
    }

    /**
     * Code for E_USER_DEPRECATED error by calling trigger_error with type E_USER_DEPRECATED.
     *
     * @return string
     */
    public static
    function E_USER_DEPRECATED_code()
    {
        $name = uniqid('oops_');
        $result = "trigger_error('$name', E_USER_DEPRECATED);";
        return $result;
    }
}
