<?php
/**
 * A class for filtering and validating arrays of data.
 *
 * This class provides a way to run a sequence of filters and/or validators on
 * an array of data and retrieve the filtered and/or validated version.
 */
class input
{
    /**
     * @var array The filter and/or validator rules.
     */
    protected $rule;

    /**
     * @var array The unfiltered/unvalidated data.
     */
    protected $data;
    
    /**
     * @var array The error messages.
     */
    protected $error;

    ////////////////////////////////////////////////////////////////////////////
    // Construct and destruct functions.

    /**
     * @param array $rule The filter and/or validator rules.
     */
    public function __construct(array $rule)
    {
        $this->rule = $this->create_rule($rule);
        $this->clear();
    }
    
    ////////////////////////////////////////////////////////////////////////////
    // Overload functions.

    /**
     * A shortcut for get().
     * 
     * @param string $name The variable name.
     * @return string The value.
     */
    public function __get($name)
    {
        return $this->get($name);
    }

    /**
     * A shortcut for set().
     * 
     * @param string $name The variable name.
     * @param mixed $value The value.
     */
    public function __set($name, $value)
    {
        $this->set($name, $value);
    }
    
    /**
     * A shortcut for is_set().
     * 
     * @param string $name The variable name.
     * @return boolean True if the variable exists, else false.
     */
    public function __isset($name)
    {
        return $this->is_set($name);
    }

    /**
     * A shortcut for clear().
     * 
     * @param string $name The variable name.
     */
    public function __unset($name)
    {
        $this->clear($name);
    }
    
    ////////////////////////////////////////////////////////////////////////////
    // Interface functions.

    /**
     * Checks if the data is valid or not.
     * 
     * @param array (optional) $data The additional data.
     * @return boolean True if valid, else false.
     */
    public function is_valid(array $data = null)
    {
        if ($data)
        {
            $this->set($data);
        }
        
        $valid = $error = array();
        
        foreach ($this->rule as $variable => $rule)
        {
            $value = $this->get($variable);
            
            foreach ($rule as $r)
            {
                if ($r instanceof filter)
                {
                    $value = $r->filter($value);
                }
                elseif ($r instanceof validator)
                {
                    if (!$r->is_valid($value))
                    {
                        $message = $r->get_error();
                        $error[$variable] = ($message !== null) ? $message : 'Invalid';
                        break;
                    }
                }
                elseif (substr($r, 0, 6) === 'match:')
                {
                    $match = substr($r, 6);
                    
                    if (!array_key_exists($match, $this->rule))
                    {
                        throw new outofrangeexception('Variable ' . $variable . ' does not exist.');
                    }
                    
                    if ($this->get($match) != $value)
                    {
                        $error[$variable] = 'Mismatch';
                        break;
                    }
                }
                else
                {
                    $value = call_user_func($r, $value);
                }
            }
            
            $valid[$variable] = $value;
        }
        
        if ($error)
        {
            $this->set_error($error);
        }
        else
        {
            $this->set($valid);
        }
        
        return !$error;
    }

    /**
     * Gets one or all variables.
     * 
     * @param string (optional) $name The variable name.
     * @param mixed (optional) $alternative The alternative value.
     * @return string The value if it exists, else the alternative value.
     */
    public function get($name = null, $alternative = null)
    {
        if ($name === null)
        {
            return $this->data;
        }
        
        if (array_key_exists($name, $this->data))
        {
            return $this->data[$name];
        }
        
        return $alternative;
    }
    
    /**
     * Sets one or more variable.
     * 
     * @param string $name The variable name.
     * @param mixed $value The value.
     * -
     * @param array $name The variables.
     * @return input A fluent interface.
     */
    public function set($name, $value = null)
    {
        if (!is_array($name))
        {
            $name = array($name => $value);
        }
        
        foreach ($name as $k => $v)
        {
            if (array_key_exists($k, $this->rule))
            {
                $this->data[$k] = $v;
            }
        }
        
        return $this;
    }

    /**
     * Checks if a variable is set or not.
     * 
     * @param string $name The variable name.
     * @return boolean True if the variable exists, else false.
     */
    public function is_set($name)
    {
        return array_key_exists($name, $this->data);
    }
    
    /**
     * Clears one or all variables.
     * 
     * @param string (optional) $name The variable name.
     * @return input A fluent interface.
     */
    public function clear($name = null)
    {
        if ($name === null)
        {
            $this->data = array();
        }
        else
        {
            unset($this->data[$name]);
        }
        
        $this->clear_error($name);
        return $this;
    }
    
    /**
     * Gets one or all error message.
     * 
     * @param string (optional) $name The variable name.
     * @throws outofrangeexception If the error for the variable does not exist.
     * @return string The message.
     */
    public function get_error($name = null)
    {
        if ($name === null)
        {
            return $this->error;
        }
        
        if (!array_key_exists($name, $this->error))
        {
            throw new outofrangeexception('Error message for variable ' . $name . ' does not exist.');
        }
        
        return $this->error[$name];
    }
    
    /**
     * Sets an error message.
     * 
     * @param string $name The variable name.
     * @param string $value The error message.
     * -
     * @param array $name The error messages.
     * @throws outofrangeexception If the variable does not exist.
     * @return input A fluent interface.
     */
    public function set_error($name, $value = 'Invalid')
    {
        if (!is_array($name))
        {
            $name = array($name => $value);
        }
        
        foreach ($name as $k => $v)
        {
            if (!array_key_exists($k, $this->rule))
            {
                throw new outofrangeexception('Variable ' . $k . ' does not exist.');
            }
            
            $this->error[$k] = $v;
        }
        
        return $this;
    }
    
    /**
     * Checks if one or all variables are erroneous or not.
     * 
     * @param string (optional) $name The variable name.
     * @throws outofrangeexception If the variable does not exist.
     * @return boolean True if the variable is erroneous, else false.
     */
    public function is_error($name = null)
    {
        if ($name === null)
        {
            return (boolean) $this->error;
        }
        
        if (!array_key_exists($name, $this->rule))
        {
            throw new outofrangeexception('Variable ' . $name . ' does not exist.');
        }
        
        return array_key_exists($name, $this->error);
    }
    
    /**
     * Clears one or all error messages.
     * 
     * @param string (optional) $name The variable name.
     * @return input A fluent interface.
     */
    public function clear_error($name = null)
    {
        if ($name === null)
        {
            $this->error = array();
        }
        else
        {
            unset($this->error[$name]);
        }
        
        return $this;
    }
    
    ////////////////////////////////////////////////////////////////////////////
    // Internal functions.
    
    /**
    * Creates a list of organized rules.
    *
    * @param array $rule The rules.
    * @return array The organized rules.
    */
    protected function create_rule(array $rule)
    {
        $new = array();
        $base = array();
        
        if (array_key_exists('*', $rule))
        {
            $base = $rule['*'];
            
            if (!is_array($base))
            {
                $base = array($base);
            }
            
            unset($rule['*']);
        }
        
        foreach ($rule as $k => $v)
        {
            if (is_numeric($k))
            {
                $k = $v;
                $v = null;
            }
            
            $new[$k] = $base;
            
            if ($v !== null)
            {
                if (!is_array($v))
                {
                    $v = array($v);
                }
                
                $new[$k] = array_merge($new[$k], $v);
            }
        }
        
        return $new;
    }
}
