<?php defined("SYSPATH") or die("No direct script access.");
/**
 * FORGE base input library.
 *
 * $Id: Form_Input.php 3326 2008-08-09 21:24:30Z Shadowhand $
 *
 * @package    Forge
 * @author     Kohana Team
 * @copyright  (c) 2007-2008 Kohana Team
 * @license    http://kohanaphp.com/license.html
 */
class Form_Input_Core {

	// Input method
	public $method;

	// Element data
	protected $data = array
	(
		'type'    => 'text',
		'class'   => 'textbox',
		'value'   => ''
	);

	// Protected data keys
	protected $protect = array();

	// Validation rules, matches, and callbacks
	protected $rules = array();
	protected $matches = array();
	protected $callbacks = array();

	// Validation check
	protected $is_valid;

	// Errors
	protected $errors = array();
	protected $error_messages = array();

	/**
	 * Sets the input element name.
	 */
	public function __construct($name)
	{
		$this->data['name'] = $name;
	}

	/**
	 * Sets form attributes, or return rules.
	 */
	public function __call($method, $args)
	{
		if ($method == 'rules')
		{
			if (empty($args))
				return $this->rules;

			// Set rules and action
			$rules  = $args[0];
			$action = substr($rules, 0, 1);

			if (in_array($action, array('-', '+', '=')))
			{
				// Remove the action from the rules
				$rules = substr($rules, 1);
			}
			else
			{
				// Default action is append
				$action = '';
			}

			$this->add_rules(explode('|', $rules), $action);
		}
		elseif ($method == 'name')
		{
			// Do nothing. The name should stay static once it is set.
		}
		else
		{
			$this->data[$method] = $args[0];
		}

		return $this;
	}

	/**
	 * Returns form attributes.
	 *
	 * @param   string  attribute name
	 * @return  string
	 */
	public function __get($key)
	{
		if (isset($this->data[$key]))
		{
			return $this->data[$key];
		}
	}

	/**
	 * Sets a form element that this element must match the value of.
	 *
	 * @chainable
	 * @param   object  another Forge input
	 * @return  object
	 */
	public function matches($input)
	{
		if ( ! in_array($input, $this->matches, TRUE))
		{
			$this->matches[] = $input;
		}

		return $this;
	}

	/**
	 * Sets a callback method as a rule for this input.
	 *
	 * @chainable
	 * @param   callback
	 * @return  object
	 */
	public function callback($callback)
	{
		if ( ! in_array($callback, $this->callbacks, TRUE))
		{
			$this->callbacks[] = $callback;
		}

		return $this;
	}

	/**
	 * Sets or returns the input label.
	 *
	 * @chainable
	 * @param   string   label to set
	 * @return  string|object
	 */
	public function label($val = NULL)
	{
		if ($val === NULL)
		{
			if (isset($this->data['name']) AND isset($this->data['label']))
			{
				return form::label($this->data['name'], $this->data['label']);
			}
			return FALSE;
		}
		else
		{
			$this->data['label'] = ($val === TRUE) ? utf8::ucwords(inflector::humanize($this->name)) : $val;
			return $this;
		}
	}

	/**
	 * Set or return the error message.
	 *
	 * @chainable
	 * @param   string  error message
	 * @return  strong|object
	 */
	public function message($val = NULL)
	{
		if ($val === NULL)
		{
			if (isset($this->data['message']))
				return $this->data['message'];
		}
		else
		{
			$this->data['message'] = $val;
			return $this;
		}
	}

	/**
	 * Runs validation and returns the element HTML.
	 *
	 * @return  string
	 */
	public function render()
	{
		// Make sure validation runs
		$this->validate();

		return $this->html_element();
	}

	/**
	 * Returns the form input HTML.
	 *
	 * @return  string
	 */
	protected function html_element()
	{
		$data = $this->data;

		unset($data['label']);
		unset($data['message']);

		return form::input($data);
	}

	/**
	 * Replace, remove, or append rules.
	 *
	 * @param   array   rules to change
	 * @param   string  action to use: replace, remove, append
	 */
	protected function add_rules( array $rules, $action)
	{
		if ($action === '=')
		{
			// Just replace the rules
			$this->rules = $rules;
			return;
		}

		foreach ($rules as $rule)
		{
			if ($action === '-')
			{
				if (($key = array_search($rule, $this->rules)) !== FALSE)
				{
					// Remove the rule
					unset($this->rules[$key]);
				}
			}
			else
			{
				if ( ! in_array($rule, $this->rules))
				{
					if ($action == '+')
					{
						array_unshift($this->rules, $rule);
					}
					else
					{
						$this->rules[] = $rule;
					}
				}
			}
		}
	}

	/**
	 * Add an error to the input.
	 *
	 * @chainable
	 * @return object
	 */
	public function add_error($key, $val)
	{
		if ( ! isset($this->errors[$key]))
		{
			$this->errors[$key] = $val;
		}

		return $this;
	}

	/**
	 * Set or return the error messages.
	 *
	 * @chainable
	 * @param   string|array  failed validation function, or an array of messages
	 * @param   string        error message
	 * @return  object|array
	 */
	public function error_messages($func = NULL, $message = NULL)
	{
		// Set custom error messages
		if ( ! empty($func))
		{
			if (is_array($func))
			{
				// Replace all
				$this->error_messages = $func;
			}
			else
			{
				if (empty($message))
				{
					// Single error, replaces all others
					$this->error_messages = $func;
				}
				else
				{
					// Add custom error
					$this->error_messages[$func] = $message;
				}
			}
			return $this;
		}

		// Make sure validation runs
		is_null($this->is_valid) and $this->validate();

		// Return single error
		if ( ! is_array($this->error_messages) AND ! empty($this->errors))
			return array($this->error_messages);

		$messages = array();
		foreach ($this->errors as $func => $args)
		{
			if (is_string($args))
			{
				$error = $args;
			}
			else
			{
				// Force args to be an array
				$args = is_array($args) ? $args : array();

				// Add the label or name to the beginning of the args
				array_unshift($args, $this->label ? mb_strtolower($this->label) : $this->name);

				if (isset($this->error_messages[$func]))
				{
					// Use custom error message
					$error = vsprintf($this->error_messages[$func], $args);
				}
				else
				{
					// Get the proper i18n entry, very hacky but it works
					switch ($func)
					{
						case 'valid_url':
						case 'valid_email':
						case 'valid_ip':
							// Fetch an i18n error message
                                                        $error = 'validation.'.$func;
							break;
						case substr($func, 0, 6) === 'valid_':
							// Strip 'valid_' from func name
							$func = (substr($func, 0, 6) === 'valid_') ? substr($func, 6) : $func;
						case 'alpha':
						case 'alpha_dash':
						case 'digit':
						case 'numeric':
							// i18n strings have to be inserted into valid_type
							$args[] = 'validation.'.$func;
							$error = 'validation.valid_type';
							break;
						default:
							$error = 'validation.'.$func;
					}
				}
			}

			// Add error to list
			$messages[] = $error;
		}

		return $messages;
	}

	/**
	 * Get the global input value.
	 *
	 * @return  string|bool
	 */
	protected function input_value($name = array())
	{
		// Get the Input instance
		$input = Input::instance();

		// Fetch the method for this object
		$method = $this->method;

		return $input->$method($name, NULL);
	}

	/**
	 * Load the value of the input, if form data is present.
	 *
	 * @return  void
	 */
	protected function load_value()
	{
		if (is_bool($this->is_valid))
			return;

		if ($name = $this->name)
		{
			// Load POSTed value, but only for named inputs
			$this->data['value'] = $this->input_value($name);
		}

		if (is_string($this->data['value']))
		{
			// Trim string values
			$this->data['value'] = trim($this->data['value']);
		}
	}

	/**
	 * Validate this input based on the set rules.
	 *
	 * @return  bool
	 */
	public function validate()
	{
		// Validation has already run
		if (is_bool($this->is_valid))
			return $this->is_valid;

		// No data to validate
		if ($this->input_value() == FALSE)
			return $this->is_valid = FALSE;

		// Load the submitted value
		$this->load_value();

		// No rules to validate
		if (count($this->rules) == 0 AND count($this->matches) == 0 AND count($this->callbacks) == 0)
			return $this->is_valid = TRUE;

		if ( ! empty($this->rules))
		{
			foreach ($this->rules as $rule)
			{
				if (($offset = strpos($rule, '[')) !== FALSE)
				{
					// Get the args
					$args = preg_split('/, ?/', trim(substr($rule, $offset), '[]'));

					// Remove the args from the rule
					$rule = substr($rule, 0, $offset);
				}

				if (substr($rule, 0, 6) === 'valid_' AND method_exists('valid', substr($rule, 6)))
				{
					$func = substr($rule, 6);

					if ($this->value AND ! valid::$func($this->value))
					{
						$this->errors[$rule] = TRUE;
					}
				}
				elseif (method_exists($this, 'rule_'.$rule))
				{
					// The rule function is always prefixed with rule_
					$rule = 'rule_'.$rule;

					if (isset($args))
					{
						// Manually call up to 2 args for speed
						switch (count($args))
						{
							case 1:
								$this->$rule($args[0]);
							break;
							case 2:
								$this->$rule($args[0], $args[1]);
							break;
							default:
								call_user_func_array(array($this, $rule), $args);
							break;
						}
					}
					else
					{
						// Just call the rule
						$this->$rule();
					}

					// Prevent args from being re-used
					unset($args);
				}
				else
				{
					throw new Kohana_Exception('validation.invalid_rule', $rule);
				}

				// Stop when an error occurs
				if ( ! empty($this->errors))
					break;
			}
		}

		if ( ! empty($this->matches))
		{
			foreach ($this->matches as $input)
			{
				if ($this->value != $input->value)
				{
					// Field does not match
					$this->errors['matches'] = array($input->label ? mb_strtolower($input->label) : $input->name);
					break;
				}
			}
		}

		if ( ! empty($this->callbacks))
		{
			foreach ($this->callbacks as $callback)
			{
				call_user_func($callback, $this);

				// Stop when an error occurs
				if ( ! empty($this->errors))
					break;
			}
		}

		// If there are errors, validation failed
		return $this->is_valid = empty($this->errors);
	}

	/**
	 * Validate required.
	 */
	protected function rule_required()
	{
		if ($this->value === '' OR $this->value === NULL)
		{
			$this->errors['required'] = TRUE;
		}
	}

	/**
	 * Validate length.
	 */
	protected function rule_length($min, $max = NULL)
	{
		// Get the length, return if zero
		if (($length = mb_strlen($this->value)) === 0)
			return;

		if ($max == NULL)
		{
			if ($length != $min)
			{
				$this->errors['exact_length'] = array($min);
			}
		}
		else
		{
			if ($length < $min)
			{
				$this->errors['min_length'] = array($min);
			}
			elseif ($length > $max)
			{
				$this->errors['max_length'] = array($max);
			}
		}
	}

} // End Form Input