<?php
/**
 * core.framework
 *
 * @category   Core
 * @package    Core_Form
 * @copyright  Copyright (c) 2011. Burza d.o.o. (http://web.burza.hr/en/)
 * @license    proprietary
 */

/**
 * @category  Core
 * @package   Core_Form
 * @copyright Copyright (c) 2011. Burza d.o.o. (http://web.burza.hr/en/)
 * @license   proprietary
 * @method    array                      getFilters();                                         @see __call()
 * @method    Core_Form_Element_Abstract addFilters(array $filters);                           @see __call()
 * @method    Core_Form_Element_Abstract addFilter(Core_Filter_Interface $filter);             @see __call()
 * @method    Core_Form_Element_Abstract removeFilter($name);                                  @see __call()
 * @method    Core_Filter_Interface      createFilter($type, $name, array $options = null);    @see __call()
 * @method    Core_Filter_Interface      getFilter($name);                                     @see __call()
 * @method    boolean                    hasFilter($name);                                     @see __call()
 * @ method    array                      getValidators();                                      @see __call()
 * @method    Core_Form_Element_Abstract addValidators(array $validators);                     @see __call()
 * @method    Core_Form_Element_Abstract addValidator(Core_Validate_Interface $validator);     @see __call()
 * @method    Core_Form_Element_Abstract removeValidator($name);                               @see __call()
 * @method    Core_Validate_Interface    createValidator($type, $name, array $options = null); @see __call()
 * @method    Core_Validate_Interface    getValidator($name);                                  @see __call()
 * @method    boolean                    hasValidator($name);                                  @see __call()
 */
abstract class Core_Form_Element_Abstract implements Core_Form_Element_Interface, Core_Angular_Directive_Interface
{
    /**
     * @var Zend_Translator
     */
    protected $_translator;
    
    /**
     * @var Core_Plugin_Manager
     */
    protected $_pluginManager;

    /**
     * @var array
     */
    protected $_attribs = array();

    /**
     * @var array
     */
    protected $_mandatoryAttribs = array(
        Core_Form::CONTEXT_DEFAULT => array(),
        Core_Form::CONTEXT_ANGULAR => array(),
    );

    /**
     * @var array
     */
    protected $_defaultAttribs = array(
        Core_Form::CONTEXT_DEFAULT => array(),
        Core_Form::CONTEXT_ANGULAR => array(),
    );

    /**
     * @var string
     */
    protected $_name;

    /**
     * @var string
     */
    protected $_label;

    /**
     * @var string
     */
    protected $_description;

    /**
     * @var string
     */
    protected $_placeholder;

    /**
     * @var mixed
     */
    protected $_rawValue;

    /**
     * @var string
     */
    protected $_errorMessage;

    /**
     * @var string
     */
    protected $_error;

    /**
     * @var boolean
     */
    protected $_required = false;

    /**
     * @var boolean
     */
    protected $_ignored = false;

    /**
     * @var boolean
     */
    protected $_allowEmpty;

    /**
     * @var Core_Form_Group_Interface
     */
    protected $_parent;

    /**
     * @var Twig_Environment
     */
    protected $_view;

    /**
     * @var mixed
     */
    protected $_template;

    /**
     * @var string
     */
    protected $_macro;

    /**
     * @var boolean
     */
    protected $_prototype = false;

    /**
     * @return void
     */
    public function __construct($options = null)
    {
        $this->getPluginManager()
            ->addType('Core_Filter_Interface', 'Filter', array(
                'mapping' => array('Filter'    => 'Plugin', 'Filters'    => 'Plugins'),
            ))
            ->addType('Core_Validate_Interface', 'Validate', array(
                'mapping' => array('Validator' => 'Plugin', 'Validators' => 'Plugins')
            ));

        if (is_array($options)) {
            $this->setOptions($options);
        } elseif ($options instanceof Zend_Config) {
            $this->setConfig($options);
        }
    }
    
    /**
     * @param Zend_Translate $translator
     * 
     * @return \Core_Form_Group_Abstract
     */
    public function setTranslator(Zend_Translate $translator)
    {
        $this->_translator = $translator;
        return $this;
    }
    
    /**
     * @return Zend_Translate
     */
    public function getTranslator()
    {
        if (null === $this->_translator) {
            $parent = $this->getParent();
            if (null !== $parent) {
                $this->_translator = $parent->getTranslator();
            }
        }
        return $this->_translator;
    }

    /**
     * @param boolean $prototype
     *
     * @return \Core_Form_Element_Abstract
     */
    public function setPrototype($prototype)
    {
        $this->_prototype = (bool) $prototype;
        return $this;
    }

    /**
     * @return boolean
     */
    public function isPrototype()
    {
        return $this->_prototype;
    }

    /**
     * @param Core_Plugin_Manager $pluginManager
     *
     * @return \Core_Form_Element_Abstract
     */
    public function setPluginManager(Core_Plugin_Manager $pluginManager)
    {
        $this->_pluginManager = $pluginManager;
        return $this;
    }

    /**
     * @return Core_Plugin_Manager
     */
    public function getPluginManager()
    {
        if (null === $this->_pluginManager) {
            $this->_pluginManager = new Core_Plugin_Manager($this);
        }
        return $this->_pluginManager;
    }

    /**
     * @param Core_Plugin_Interface $parent
     *
     * @return \Core_Form_Element_Abstract
     */
    public function init()
    {
        return $this;
    }

    /**
     * @param Core_Plugin_Interface $parent
     *
     * @return \Core_Form_Element_Abstract
     */
    public function setOwner(Core_Plugin_Interface $parent)
    {
        if (!$parent instanceof Core_Form_Group_Interface) {
            $message = sprintf(
                'Failed setting group "%s" parent, expected Core_Form_Group_Interface, got %s',
                $this->getName(),
                get_class($parent)
            );
            throw new InvalidArgumentException($message);
        }
        $this->_parent = $parent;
        return $this;
    }

    /**
     * @return Core_Form_Group_Interface
     */
    public function getParent()
    {
        return $this->_parent;
    }

    /**
     * @return Core_Form_Group_Abstract
     */
    public function getRoot()
    {
        $parent = $this->getParent();
        if (null === $parent) {
            $root = $this;
        } else {
            $root = $parent->getRoot();
        }
        return $root;
    }

    /**
     * @param string $name
     * @return mixed
     *
     * @throws InvalidArgumentException
     */
    public function get($name)
    {
        if (false !== ($idx = strpos($name, '/'))) {
            $name = trim($name);
            if (0 === $idx) {
                // absolute request, fetch relative on root
                $root   = $this->getRoot();
                if ('/' === $name) {
                    // requested just root
                    return $root;
                } else {
                    // requested from root
                    return $root->get(substr($name, 1));
                }
            }
        }
        
        $method   = 'get' . $name;
        $callable = array($this, $method);
        if (!method_exists($this, $method) && !$this->_pluginManager->isManaged($name)) {
            throw new InvalidArgumentException(sprintf('Failed fetching property, invalid property "%s" passed', $name));
        }
        return call_user_func($callable);
    }

    /**
     * @param string $name
     * @return mixed
     *
     * @throws InvalidArgumentException
     */
    public function __get($name)
    {
        return $this->get($name);
    }

    /**
     * @param string $name
     * @param mixed  $value
     *
     * @return \Core_Form_Element_Abstract
     * @throws InvalidArgumentException
     */
    public function set($name, $value)
    {
        $method   = 'set' . $name;
        $callable = array($this, $method);
        if (method_exists($this, $method) || $this->_pluginManager->isManaged($name)) {
            call_user_func($callable, $value);
        } else {
            throw new InvalidArgumentException(sprintf('Failed setting property, invalid property "%s" passed', $name));
        }
        return $this;
    }

    /**
     * @param string $name
     * @param mixed  $value
     *
     * @return \Core_Form_Element_Abstract
     * @throws InvalidArgumentException
     */
    public function __set($name, $value)
    {
        return $this->set($name, $value);
    }

    /**
     * @return array
     */
    public function getAngularRepresentation($attribs = null)
    {
        $representation = array();
        $validators     = $this->getValidators();
        $errorMessage   = $this->getErrorMessage();
        foreach($validators as $validator) {
            $validatorRepresentation = $validator->getAngularRepresentation($errorMessage);
            foreach ($validatorRepresentation as $type => $data) {
                if (null === $attribs) {
                    // validators
                    $representation[$type]       = $data['errorMessage'];
                } else {
                    // attributes
                    $attribName                  = isset($data['attribName']) ? $data['attribName'] : $type;
                    $representation[$attribName] = $data['config'];
                }
            }
        }
        return $representation;
    }

    /**
     * @return boolean
     */
    public function isInCollection()
    {
        $parent = $this->getParent();
        if (null === $parent) {
            return false;
        } else if (null === ($parent = $parent->getParent())) {
            return false;
        }
        return ($parent instanceof Core_Form_Group_Collection);
    }

    /**
     * @return string
     */
    public function getFullyQualifiedAngularModel()
    {
        return 'data.'. $this->getName();
    }

    /**
     * @return string
     */
    public function getFullyQualifiedAngularId()
    {
        $parent = $this->getParent();
        if (null === $parent) {
            $id = $this->getFullyQualifiedId();
        } elseif ($parent->isPrototype()) {
            $id = $parent->getFullyQualifiedAngularId();
        } else {
            $id = $this->getFullyQualifiedId();
        }
        return $id;
    }

    /**
     * @param string $name
     *
     * @return \Core_Form_Element_Abstract
     */
    public function setName($name)
    {
        $this->_name = $name;
        return $this;
    }

    /**
     * @return string
     */
    public function getRawValue()
    {
        return $this->_rawValue;
    }

    /**
     * @param mixed $rawValue
     *
     * @return \Core_Form_Element_Abstract
     */
    public function setRawValue($rawValue)
    {
        $this->_rawValue = $rawValue;
        return $this;
    }

    /**
     * @return string
     */
    public function getValue()
    {
        $value   = $this->getRawValue();
        $filters = $this->getFilters();
        foreach ($filters as $filter) {
            $value = $filter->filter($value);
        }
        return $value;
    }

    /**
     * @param mixed $value
     *
     * @return \Core_Form_Element_Abstract
     */
    public function setValue($value)
    {
        return $this->setRawValue($value);
    }

    /**
     * @return string
     */
    public function getName()
    {
        return $this->_name;
    }

    /**
     * @param mixed $description
     *
     * @return \Core_Form_Element_Abstract
     */
    public function setDescription($description)
    {
        $this->_description = $description;
        return $this;
    }

    /**
     * @return string
     */
    public function getDescription()
    {
        return $this->_translate($this->_description);
    }

    /**
     * @param mixed $label
     *
     * @return \Core_Form_Element_Abstract
     */
    public function setLabel($label)
    {
        $this->_label = $label;
        return $this;
    }

    /**
     * @return string
     */
    public function getLabel()
    {
        if (null === $this->_label) {
            return $this->getName();
        }
        return $this->_translate($this->_label);
    }

    /**
     * @param mixed $placeholder
     *
     * @return \Core_Form_Element_Abstract
     */
    public function setPlaceholder($placeholder)
    {
        $this->_placeholder = $placeholder;
        return $this;
    }

    /**
     * @return string
     */
    public function getPlaceholder()
    {
        return $this->_translate($this->_placeholder);
    }

    /**
     * @return string
     */
    public function getFullyQualifiedName()
    {
        $parent = $this->getParent();
        if (null === $parent || null === ($parentName = $parent->getFullyQualifiedName())) {
            $name = $this->getName();
        } else {
            $name = $parentName .'['. $this->getName() .']';
        }
        return $name;
    }

    /**
     * @param string $id
     *
     * @return \Core_Form_Element_Abstract
     */
    public function setId($id)
    {
        return $this->setAttrib('id', $id);
    }

    /**
     * @return string
     */
    public function getId()
    {
        return $this->getAttrib('id', $this->getName());
    }

    /**
     * @return string
     */
    public function getFullyQualifiedId()
    {
        $parent = $this->getParent();
        if (null === $parent || null === ($parentId = $parent->getFullyQualifiedId())) {
            $id = $this->getId();
        } else {
            $id = $parentId .'_'. $this->getId();
        }
        return $id;
    }

    /**
     * @param array $attribs
     *
     * @return \Core_Form_Element_Abstract
     */
    public function setAttribs(array $attribs)
    {
        foreach($attribs as $name => $value) {
            $this->setAttrib($name, $value);
        }
        return $this;
    }

    /**
     * @return array
     */
    public function getAttribs($context = Core_Form::CONTEXT_DEFAULT)
    {
        $attribs = $this->_getDefaultAttribs($context);
        foreach(array_keys($this->_attribs) as $name) {
            $attribs[$name] = $this->getAttrib($name);
        }
        $attribs = $this->_mergeValues($attribs, $this->_mandatoryAttribs[$context]);
        if (Core_Form::CONTEXT_ANGULAR === $context) {
            $attribs += $this->getAngularRepresentation(true);
        }
        return $attribs;
    }

    /**
     * @param string $name
     *
     * @return string
     */
    public function getAttrib($name, $default = null)
    {
        $method = '_getAttrib'. $name;
        if (method_exists($this, $method)) {
            $value = array_key_exists($name, $this->_attribs) ? $this->_attribs[$name] : null;
            return call_user_func(array($this, $method), $value);
        }

        $name = strtolower($name);
        if (!array_key_exists($name, $this->_attribs)) {
            return $default;
        }
        return $this->_attribs[$name];
    }

    /**
     * @param string $name  Attrib name
     * @param string $value Attrib value
     *
     * @return \Core_Form_Element_Abstract
     */
    public function setAttrib($name, $value)
    {
       $method = '_setAttrib'. str_replace(array('-'), '', $name);
       if (method_exists($this, $method)) {
           $value = call_user_func(array($this, $method), $value);
       }

        $this->_attribs[strtolower($name)] = $value;
        return $this;
    }

    /**
     * @param array $options
     *
     * @return \Core_Form_Element_Abstract
     */
    public function setOptions(array $options)
    {
        if (isset($options['owner'])) {
            $this->setOwner($options['owner']);
            unset($options['owner']);
        }
        
        if (isset($options['filters'])) {
            $this->addFilters($options['filters']);
            unset($options['filters']);
        }

        if (isset($options['validators'])) {
            $this->addValidators($options['validators']);
            unset($options['validators']);
        }

        foreach($options as $name => $value) {
            $this->set($name, $value);
        }

        return $this;
    }

    /**
     * @param Zend_Config $config
     *
     * @return \Core_Form_Element_Abstract
     */
    public function setConfig(Zend_Config $config)
    {
        return $this->setOptions($config->toArray());
    }

    /**
     * @param string $errorMessage
     *
     * @return \Core_Form_Element_Abstract
     */
    public function setErrorMessage($errorMessage)
    {
        $this->_errorMessage = $errorMessage;
        return $this;
    }

    /**
     * @return string
     */
    public function getErrorMessage()
    {
        return $this->_translate($this->_errorMessage);
    }

    /**
     * @param string $error
     *
     * @return \Core_Form_Element_Abstract
     */
    public function setError($error)
    {
        $this->_error = $error;
        return $this;
    }

    /**
     * @return string
     */
    public function getError()
    {
        return $this->_error;
    }

    /**
     * @return boolean
     */
    public function hasError()
    {
        return (null !== $this->_error);
    }

    /**
     * @return \Core_Form_Element_Abstract
     */
    public function resetError()
    {
        $this->_error = null;
        return $this;
    }

    /**
     * @param boolean $required
     *
     * @return \Core_Form_Element_Abstract
     */
    public function setRequired($required)
    {
        $this->_required = (bool) $required;
        return $this;
    }

    /**
     * @return boolean
     */
    public function isRequired()
    {
        return $this->_required;
    }

    /**
     * @param boolean $ignored
     *
     * @return \Core_Form_Element_Abstract
     */
    public function setIgnored($ignored)
    {
        $this->_ignored = (bool) $ignored;
        return $this;
    }

    /**
     * @return boolean
     */
    public function isIgnored()
    {
        return $this->_ignored;
    }

    /**
     * @param boolean $allowEmpty
     *
     * @return \Core_Form_Element_Abstract
     */
    public function setAllowEmpty($allowEmpty)
    {
        switch (true) {
            case is_bool($allowEmpty) || is_numeric($allowEmpty):
            case in_array(strtolower($allowEmpty), array('true', 'false')):
                $this->_allowEmpty = (bool) $allowEmpty;
                break;
            default:
                $this->_allowEmpty = trim($allowEmpty);
        }
        return $this;
    }

    /**
     * @return boolean
     */
    public function isAllowEmpty()
    {
        if (null !== $this->_allowEmpty) {
            if (is_string($this->_allowEmpty)) {
                $matches     = array();
                preg_match_all('/(\/[a-zA-Z0.9_-]+)+/us', $this->_allowEmpty, $matches);
                $tokens      = array_unique($matches[0]);
                $tokenValues = array();

                foreach ($tokens as $token) {
                    $tokenValues[$token] = var_export($this->get($token), true);
                }                
                $expression = 'return (bool) ('. str_replace($tokens, $tokenValues, $this->_allowEmpty) .');';

                // evaluate it and return as boolean
                $allowEmpty = eval($expression);
            } else {
                $allowEmpty = $this->_allowEmpty;                
            }
        } else {
            $allowEmpty = (false === $this->isRequired());
        }
        return $allowEmpty;
    }

    /**
     * @return Core_Validate_Abstract[]
     */
    public function getValidators()
    {
        if ($this->isRequired()) {
            $this->_prependRequiredValidator();
        }
        return $this->_pluginManager->manage('getValidators', array());
    }

    /**
     * @param string $name   Method name
     * @param array  $params Method params
     *
     * @return mixed
     */
    public function __call($name, array $params)
    {
        if ($this->_pluginManager->isManaged($name)) {
            $value = $this->_pluginManager->manage($name, $params);
            if ($value === $this->_pluginManager) {
                $value = $this;
            }
            return $value;
        }
        throw new BadMethodCallException(sprintf('Call to undefined method %s::%s()', get_class($this), $name));
    }

    /**
     * @return void
     */
    public function __clone()
    {
        $pluginManager = clone $this->getPluginManager();
        $pluginManager->setOwner($this);

        $this->setPluginManager($pluginManager);
    }

    /**
     * @return string
     */
    public function __toString()
    {
        try {
            return $this->render(Core_Form::CONTEXT_DEFAULT);
        } catch (Exception $exception) {
            $format  = 'Exception "%s: %s" while rendering form group %s: %s';
            $message = sprintf(
                $format,
                get_class($exception),
                $exception->getMessage(),
                $this->getName(),
                $exception->getTraceAsString()
            );
            trigger_error($message, E_USER_ERROR);
            return '';
        }
    }

    /**
     * @return string
     */
    public function render($context)
    {
        $template = $this->getTemplate();
        $macro    = $this->getMacro();
        return $template->render(array('this' => $this, 'macro' => $macro, 'context' => $context));
    }

    /**
     * @param Twig_Environment $view
     *
     * @return \Core_Form_Group_Abstract
     */
    public function setView(Twig_Environment $view)
    {
        $this->_view = $view;
        return $this;
    }

    /**
     * @return Twig_Environment
     */
    public function getView()
    {
        if (null === $this->_view) {
            $this->setView(Core_Application::get('View'));
        }
        return $this->_view;
    }

    /**
     * @param string $template
     *
     * @return \Core_Form_Group_Abstract
     */
    public function setTemplate($template)
    {
        $this->_template = $template;
        return $this;
    }

    /**
     * @return Twig_TemplateInterface
     */
    public function getTemplate()
    {
        if (!($this->_template instanceof Twig_TemplateInterface)) {
            $this->_template = $this->getView()->loadTemplate($this->_template);
        }
        return $this->_template;
    }

    /**
     * @param string $macro
     *
     * @return \Core_Form_Group_Abstract
     */
    public function setMacro($macro)
    {
        $this->_macro = $macro;
        return $this;
    }

    /**
     * @return string
     */
    public function getMacro()
    {
        return $this->_macro;
    }

    /**
     * @return boolean
     */
    public function isValid()
    {
        $this->resetError();

        $value      = $this->getValue();
        if ($this->isAllowEmpty() && empty($value)) {
            return true;
        }

        $validators = $this->getValidators();
        foreach($validators as $validator) {
            $validator->setContext($this->getRoot()->getValues());
            if (!$validator->isValid($value)) {
                $error   = $validator->getErrorMessage($this->getErrorMessage());
                $this->setError($error);
                break;
            }
        }
        return (false === $this->hasError());
    }

    /**
     * @return Core_Form_Element_Abstract
     */
    protected function _prependRequiredValidator()
    {
        // TODO: set sort to prepend when PluginManager gains support for it
        $this->_configureValidator('Notempty');
        return $this;
    }

    /**
     * @param string $name
     * @param array  $options
     *
     * @return \Core_Form_Element_Abstract
     */
    protected function _configureValidator($name, array $options = null)
    {
        if ($this->hasValidator($name)) {
            if (null !== $options) {
                $validator = $this->getValidator($name);
                $validator->setOptions($options);
            }
        } else {
            $this->createValidator($name, $name, $options);
        }
        return $this;
    }

    /**
     * @param array $target
     * @param array $values
     *
     * @return void
     */
    protected function _mergeValues(array $target, array $values)
    {
        if ($values) {
            foreach ($values as $key => $value) {
                $source = $value;
                if (is_int($key)) {
                    $key = $value;
                }

                if (!isset($target[$key]) && null !== ($value = $this->get($source))) {
                    $target[$key] = $value;
                }
            }
        }
        return $target;
    }

    /**
     * @param string $context
     *
     * @return array
     */
    protected function _getDefaultAttribs($context)
    {
        return $this->_defaultAttribs[$context];
    }
    
   /**
     * @param int $minLength
     *
     * @return int
     */
    protected function _setAttribNgMinLength($minLength)
    {
        $minLength = intval($minLength);
        $this->_configureValidator('Stringlength', array('min' => $minLength));
        return $minLength;
    }
    
    /**
     * @param int $maxLength
     *
     * @return int
     */
    protected function _setAttribNgMaxLength($maxLength)
    {
        $maxLength = intval($maxLength);
        $this->_configureValidator('Stringlength', array('max' => $maxLength));
        return $maxLength;
    }
    
    /**
     * @param int $maxLength
     *
     * @return int
     */
    protected function _setAttribMaxLength($maxLength)
    {
        $maxLength = intval($maxLength);
        $this->_configureValidator('Stringlength', array('max' => $maxLength));
        return $maxLength;
    }

    /**
     * @param mixed $pattern
     *
     * @return mixed
     */
    protected function _setAttribPattern($pattern)
    {
        $this->_configureValidator('Regex', array('regex' => $pattern));

        return $pattern;
    }

    /**
     * @param float $min
     *
     * @return float
     */
    protected function _setAttribMin($min)
    {
        $min = floatval($min);
        $this->_configureValidator('Min', array('min' => $min));
        return $min;
    }

    /**
     * @param float $max
     *
     * @return float
     */
    protected function _setAttribMax($max)
    {
        $max = floatval($max);
        $this->_configureValidator('Max', array('max' => $max));
        return $max;
    }

    /**
     * @param float $step
     *
     * @return float
     */
    protected function _setAttribStep($step)
    {
        $step = floatval($step);
        $this->_configureValidator('Divisible', array('step' => $step));
        return $step;
    }
    
    /**
     * @param string $value
     * 
     * @return string
     */
    protected function _getAttribPlaceholder($value)
    {
        return $this->_translate($value);
    }
    
    /**
     * @param string $message
     * 
     * @return string
     */
    protected function _translate($message)
    {
        $translator = $this->getTranslator();
        if (!$translator) {
            return $message;
        }
        return $translator->translate($message);
    }
}
