<?php
/**
 * Web Application Component Toolkit
 *
 * @link http://www.phpwact.org/
 *
 * @author Wact Development Team
 * @link http://www.phpwact.org/team
 *
 * @copyright Copyright 2006, Jeff Moore
 * @license http://opensource.org/licenses/mit-license.php MIT
 */

/**
 * Provides basic exception generation features.
 *
 * This class can be used to throw exceptions with simple messages.
 * <code>
 * throw new Wact_Exception('An error occured');
 * </code>
 *
 * The first parameter defines a message format with substitution variables
 * that can be specified in subsequent parameters.
 * <code>
 * throw new Wact_Exception('File not found: {Filename}', 'index.html');
 * </code>
 *
 * Accessor methods are automatically defined for each substitution variable.
 * <code>
 * echo $e->getFilename();
 * </code>
 * 
 * Variables that don't appear in the format may be passed in an array
 * parameter after all format parameters.
 * <code>
 * throw new Wact_Exception('File not found: {Filename}', 'index.html',
 *      array('extra' => $object));
 * </code>
 * 
 * Exceptions can be repacked and re-thrown to cross layer boundaries by passing
 * the triggering exception to the contructor as the last parameter.
 *
 * <code>
 * catch (Exception $e) {
 *     throw new Wact_Exception('Wrapping Message', $e);
 * }
 * </code>
 * 
 */
class Wact_Exception_Base extends Exception {

    /**
     * A list of reserved variable names based on this class and parent
     *
     * @var array
     */
    protected $RESERVED = array(
        'cause', 'code', 'file', 'format', 'line', 'message', 'trace', 'traceasstring',
        );
    
    /**
     * An exception that caused this exception
     * @var Exception
     */
    protected $cause;

    /**
     * Uninterpolated exception message
     * @var string
     */
    protected $format;

    /**
     * Variable data
     * @var array
     */
    protected $variables = array();

    /**
     * Class constructor.
     * Note that in order to do variable interpolation on the error message,
     * subclass must assign additional fields before the parent constructor is
     * called.
     * @param Exception
     */
    function __construct() {

        $args = func_get_args();
        if (count($args) > 0) {
            $this->format = array_shift($args);
        } else {
            $this->format = '';
        }
            
        if (preg_match_all('/(?<={)\w+(?=})/', $this->format, $matches)) {
            $expectedVariables = $matches[0];
        } else {
            $expectedVariables = array();
        }

        for ($i = 0; $i < count($expectedVariables); $i++) {
            $variable = $expectedVariables[$i];
            if (in_array(strtolower($variable), $this->RESERVED)) {
                trigger_error("Substitution variable '{$variable}' is reserved", E_USER_WARNING);
                array_shift($args);
            } else {
                $name = $this->_getCanonicalName($variable);
                
                // only consume a parameter on the first mention
                if (!isset($this->variables[$name])) {
                    if (count($args) > 0) {
                        $value = array_shift($args);
                    } else {
                        trigger_error("Substitution variable '{$variable}' not specified in parameter list", E_USER_WARNING);
                        $value = NULL;
                    }
                    $this->variables[$name] = $value;
                }
            }
        }

        if (count($args) > 0) {
            $extras = array_shift($args);
            if (is_array($extras)) {
                foreach($extras as $variable => $value) {
                    if (in_array(strtolower($variable), $this->RESERVED)) {
                        trigger_error("Substitution variable '{$variable}' is reserved", E_USER_WARNING);
                    } else {
                        $name = $this->_getCanonicalName($variable);
                        $this->variables[$name] = $value;
                    }
                }
            } else {
                array_push($args, $extras);
            }
        }

        if (count($args) > 0) {
            $cause = array_shift($args);
            if ($cause instanceof Exception) {
                $this->cause = $cause;
            } else {
                array_push($args, $cause);
            }
        }

        $count = count($args);
        if ($count > 0) {
            trigger_error("{$count} unconsumed constructor parameters to " . get_class($this), E_USER_WARNING);
        }
        
        // We have to set the interpolated message here because there is no lazy way to do it.
        parent::__construct(self::interpolate($this->format, $this->variables));
    }

/**
    * returns the case-preserving, case-insensitive name of a variable
    * @param string name of variable
    * @return string canonical name of variable
    */
    protected function _getCanonicalName($name) {
        foreach($this->variables as $key => $value) {
            if (strcasecmp($name, $key) == 0) {
                return $key;
            }
        }
        return $name;
    }
    
    /**
     * Returns the exception (if any) that caused this exception, allowing
     * exceptions to be chained
     * @return Exception cause
     */
    function getCause() {
        return $this->cause;
    }

    /**
     * Returns the non-interpolated error message for this exception.
     * @return string message
     */
    function getFormat() {
        return $this->format;
    }

    /**
     * Implement general accessor methods for assigned variables.
     */
    function __call($name, $params) {
        if (substr($name, 0, 3) == 'get') {
            $var = substr($name, 3);
            $name = $this->_getCanonicalName($var);
            if (isset($this->variables[$name])) {
                return $this->variables[$name];
            } else {
                trigger_error("Accessing undefined variable '{$var}'", E_USER_NOTICE);
                return NULL;
            }
        }
    }

    /**
     * returns the string representation of this exception, including any wrapped
     * exceptions.
     * @return string representation of this exception
     */
    function __toString() {
        $message = parent::__toString();
        if (isset($this->cause)) {
            $causeMessage = $this->cause->__toString();
            $message .="\nCause:\n    " . str_replace("\n", "\n    ", $causeMessage);
        }
        return $message;
    }
    
    /**
     * An HTML representation of this exception.
     * Compatible with PEAR error Html error handling naming convention
     * @return string HTML representation of this exception
     */
    function toHtml($formatter = NULL) {
        if (empty($formatter)) {
            $formatter = new Wact_Exception_Formatter_Html();
        }

        $formatter->beginException();

        $formatter->writeExceptionMessage($this->getMessage(),
        $this->getFile(), $this->getLine(), get_class($this));

        $formatter->writeStackTrace($this->getTrace());

        if (isset($this->cause)) {
            $formatter->beginChildSection("Triggering Exception");
            $formatter->writeException($this->cause);
            $formatter->endChildSection();
        }

        $formatter->endException();

        return $formatter->getMessage();
    }

    /**
     * Subsitute variables in the format string demarcated by {variable}
     * name tokens with values specified in the array of variables.
     * Subsitution is case-insensitive. 
     *
     * @param string format
     * @param array $variables
     * @return string message
     */
    static function interpolate($format, $variables) {
        $message = $format;
        foreach($variables as $var => $value) {
            $message = str_ireplace('{' . $var . '}', $value, $message);
        }
        return $message;
    }
}

?>