<?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                       getGroups();                                        @see __call()
 * @method    Core_Form_Group_Abstract    addGroups(array $groups);                           @see __call()
 * @method    Core_Form_Group_Abstract    addGroup(Core_Form_Group_Interface $group);         @see __call()
 * @method    Core_Form_Group_Abstract    removeGroup($name);                                 @see __call()
 * @method    Core_Form_Group_Interface   createGroup($type, $name, array $options = null);   @see __call()
 * @method    Core_Form_Group_Interface   getGroup($name);                                    @see __call()
 * @method    boolean                     hasGroup($name);                                    @see __call()
 * @method    array                       getElements();                                      @see __call()
 * @method    Core_Form_Group_Abstract    addElements(array $elements);                       @see __call()
 * @method    Core_Form_Group_Abstract    addElement(Core_Form_Element_Interface $element);   @see __call()
 * @method    Core_Form_Group_Abstract    removeElement($name);                               @see __call()
 * @method    Core_Form_Element_Interface createElement($type, $name, array $options = null); @see __call()
 * @method    Core_Form_Element_Interface getElement($name);                                  @see __call()
 * @method    boolean                     hasElement($name);                                  @see __call()
 */
abstract class Core_Form_Group_Abstract implements Core_Form_Group_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 $_context = Core_Form::CONTEXT_DEFAULT;

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

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

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

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

    /**
     * @var Core_Form_Group_Interface
     */
    protected $_parent;
    
    /**
     * @var boolean
     */
    protected $_ignored = false;
    
    /**
     * @var boolean
     */
    protected $_escalateErrors = false;

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

    /**
     * @var mixed
     */
    protected $_template = array(
        Core_Form::CONTEXT_DEFAULT => '{%% import "Core/Macro/Form.tpl" as macro_form %%} {{ macro_form.%1$s(this, context%2$s) }}',
        Core_Form::CONTEXT_ANGULAR => '{%% import "Core/Angular/Macro/Form.tpl" as macro_form %%} {{ macro_form.%1$s(this, context%2$s) }}',
    );

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

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

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

    /**
     * @param Zend_Config|array $options
     *
     * @return void
     */
    public function __construct($options = null)
    {
        $this->getPluginManager()
            ->addType('Core_Form_Group_Interface', 'FormGroup', array(
                'mapping'      => array('Group' => 'Plugin', 'Groups' => 'Plugins'),
                'uniqueAcross' => array('Core_Form_Group_Interface', 'Core_Form_Element_Interface')
            ))
            ->addType('Core_Form_Element_Interface', 'FormElement', array(
                'mapping'      => array('Element' => 'Plugin', 'Elements' => 'Plugins'),
                'uniqueAcross' => array('Core_Form_Group_Interface', 'Core_Form_Element_Interface')
            ));

        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_Group_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_Group_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_Group_Abstract_Element_Abstract
     */
    public function init()
    {
        return $this;
    }

    /**
     * @param Core_Form_Group_Interface $parent
     *
     * @return Core_Form_Group_Interface
     */
    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)
    {
        $method        = 'get' . $name;
        $pluginManager = $this->getPluginManager();
        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));
                }
            } else {
                // fetching relative address
                $tokens = explode('/', $name);
                $name   = array_shift($tokens);
                // first we fetch the child element and then do a relative fetch on it
                return $this->get($name)->get(implode('/', $tokens));
            }
        } elseif (true === method_exists($this, $method)) {
            return call_user_func(array($this, $method));
        } elseif ($pluginManager->isManaged('hasGroup') && $this->hasGroup($name)) {
            return $this->getGroup($name);
        } elseif ($pluginManager->isManaged('hasElement') && $this->hasElement($name)) {
            return $this->getElement($name);
        }

        throw new InvalidArgumentException(sprintf('Failed fetching property, invalid property "%s" passed', $name));
    }

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

    /**
     * @param string $name
     * @param mixed  $value
     *
     * @return \Core_Form_Group_Abstract_Element_Abstract
     * @throws InvalidArgumentException
     */
    public function set($name, $value)
    {
        $method   = 'set' . $name;
        $callable = array($this, $method);
        if (method_exists($this, $method) || $this->getPluginManager()->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_Group_Abstract_Element_Abstract
     * @throws InvalidArgumentException
     */
    public function __set($name, $value)
    {
        return $this->set($name, $value);
    }

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

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

    /**
     * @param string $context
     *
     * @return \Core_Form_Group_Abstract
     */
    public function setContext($context)
    {
        $this->_context = $context;
        return $this;
    }

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

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

    /**
     * @return string
     */
    public function getName()
    {
        if (null === $this->_name) {
            return $this->getModel() . "Form";
        }
        return $this->_name;
    }
    
    /**
     * @param mixed $description
     *
     * @return \Core_Form_Group_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_Group_Abstract
     */
    public function setLabel($label)
    {
        $this->_label = $label;
        return $this;
    }

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

    /**
     * @param string $model
     *
     * @return \Core_Form_Group_Abstract
     */
    public function setModel($model)
    {
        $this->_model = $model;
        return $this;
    }

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

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

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

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

    /**
     * @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_Group_Abstract
     */
    public function setAttribs(array $attribs)
    {
        foreach($attribs as $name => $value) {
            $this->setAttrib($name, $value);
        }
        return $this;
    }

    /**
     * @param string $context
     *
     * @return array
     */
    public function getAttribs($context = null)
    {
        if (null !== $context) {
            $this->setContext($context);
        }
        $context = $this->getContext();
        $attribs = array_merge($this->_getDefaultAttribs($context), $this->_attribs);
        return $this->_mergeMandatoryValues($attribs, $this->_mandatoryAttribs[$context]);
    }

    /**
     * @param string $name
     *
     * @return string
     */
    public function getAttrib($name)
    {
        $name = strtolower($name);
        if (!array_key_exists($name, $this->_attribs)) {
            return null;
        }
        return $this->_attribs[$name];
    }

    /**
     * @param string $name  Attrib name
     * @param string $value Attrib value
     *
     * @return \Core_Form_Group_Abstract
     */
    public function setAttrib($name, $value)
    {
        $this->_attribs[strtolower($name)] = $value;
        return $this;
    }

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

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

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

        return $this;
    }

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

    /**
     * @param string $name   Method name
     * @param array  $params Method params
     *
     * @return mixed
     */
    public function __call($name, array $params)
    {
        $pluginManager = $this->getPluginManager();
        if ($pluginManager->isManaged($name)) {
            $value = $pluginManager->manage($name, $params);
            if ($value === $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)
            ->setPrototype(false);
    }

    /**
     * @return string
     */
    public function __toString()
    {
        try {
            return $this->render();
        } 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 = null)
    {
        if (null !== $context) {
            $this->setContext($context);
        }

        $context  = $this->getContext();
        $template = $this->getTemplate();
        $view     = $this->getView();
        return $view->render($template, array('this' => $this, '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)
    {
        $template = sprintf(
            '{%%%% import "%1$s" as macro_form %%%%} {{ macro_form.%%1$s(this, context%%2$s) }}',
            $template
        );
        $context  = $this->getContext();

        $this->_template[$context] = $template;
        return $this;
    }

    /**
     * @return Twig_TemplateInterface
     */
    public function getTemplate()
    {
        $macro   = $this->getMacro();
        $args    = null;
        $matches = array();
        if (null !== strpos($macro, '(') && preg_match('/^(?P<macro>[a-zA-Z0-9_-]*)\s*\((?P<args>.*)\)\s*$/', trim($macro), $matches)) {
            $macro = trim($matches['macro']);
            $args  = trim(', '. $matches['args']);
        }

        $template = sprintf($this->_template[$this->getContext()], $macro, $args);
        return $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;
    }

    /**
     * @param array $data
     *
     * @return \Core_Form_Group_Abstract
     */
    public function populate(array $data)
    {
        return $this->_setRecursive(array(
            'group'   => 'populate',
            'element' => 'setValue',
        ), $data);
    }

    /**
     * @param array $data
     *
     * @return \Core_Form_Group_Abstract
     */
    public function populateRaw(array $data)
    {
        return $this->_setRecursive(array(
            'group'   => 'populateRaw',
            'element' => 'setRawValue',
        ), $data);
    }

    /**
     * @param bool $skipIgnored
     *
     * @return array
     */
    public function getValues($skipIgnored = true)
    {
        return $this->_getRecursive(array(
            'group'   => 'getValues',
            'element' => 'getValue',
        ), false, $skipIgnored);
    }

    /**
     * @param string $key
     * @param bool $skipIgnored
     *
     * @return null
     */
    public function getValue($key = '', $skipIgnored = true)
    {
        $values = $this->getValues($skipIgnored);

        return isset($values[$key]) ? $values[$key] : null;
    }

    /**
     * @param bool $skipIgnored
     *
     * @return array
     */
    public function getRawValues($skipIgnored = true)
    {
        return $this->_getRecursive(array(
            'group'   => 'getRawValues',
            'element' => 'getRawValue',
        ), false, $skipIgnored);
    }

    /**
     * @return boolean
     */
    public function isValid()
    {
        $errors = array();
        $groups = $this->getGroups();
        foreach($groups as $group) {
            $name        = $group->getName();
            if (true !== $group->isValid()) {
                $groupErrors = $group->getErrors();
                if ($group->isEscalateErrors()) {
                    $errors       += $groupErrors;
                } else {
                    $errors[$name] = $groupErrors; 
                }

            }
        }

        $elements = $this->getElements();
        foreach($elements as $element) {
            $name        = $element->getName();
            if (true !== $element->isValid()) {
                $errors[$name] = $element->getError();
            }
        }
        $this->setErrors($errors);

        return (false === $this->hasErrors());
    }

    /**
     * @return \Core_Form_Group_Abstract
     */
    public function setErrors(array $errors)
    {
        $this->_errors = $errors;
        return $this;
    }

    /**
     * @return array
     */
    public function getErrors()
    {
        return $this->_errors;
    }

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

    /**
     * @return boolean
     */
    public function isIgnored()
    {
        return $this->_ignored;
    }
    
    /**
     * @param boolean $escalateErrors
     *
     * @return \Core_Form_Group_Abstract
     */
    public function setEscalateErrors($escalateErrors)
    {
        $this->_escalateErrors = (bool) $escalateErrors;
        return $this;
    }

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

    /**
     * @param array $config
     *
     * @return mixed
     */
    protected function _getRecursive(array $config, $skipNull = false, $skipIgnored = false)
    {
        $data   = array();
        $groups = $this->getGroups(false);
        foreach($groups as $group) {
            if (false === $group->isIgnored() || false === $skipIgnored) {
                $name        = $group->getName();
                $value       = call_user_func(array($group, $config['group']));
                if (null !== $value || false === $skipNull) {
                    $data[$name] = $value;
                }
            }
        }

        $elements = $this->getElements(false);
        foreach($elements as $element) {
            if (false === $element->isIgnored() || false === $skipIgnored) {
                $name        = $element->getName();
                $value       = call_user_func(array($element, $config['element']));
                if (null !== $value || false === $skipNull) {
                    $data[$name] = $value;
                }
            }
        }
        return $data;
    }

    /**
     * @param array $config
     * @param array $data
     *
     * @return \Core_Form_Group_Abstract
     */
    protected function _setRecursive(array $config, array $data)
    {
        $groups = $this->getGroups(false);
        foreach($groups as $group) {
            $name = $group->getName();
            if (array_key_exists($name, $data)) {
                call_user_func(array($group, $config['group']), $data[$name]);
            }
        }

        $elements = $this->getElements(false);
        foreach($elements as $element) {
            $name = $element->getName();
            if (array_key_exists($name, $data)) {
                call_user_func(array($element, $config['element']), $data[$name]);
            }
        }
        return $this;
    }

    /**
     * @param array $mandatoryValues
     * @param array $target
     *
     * @return void
     */
    protected function _mergeMandatoryValues(array $target, array $mandatoryValues)
    {
        if ($mandatoryValues) {
            foreach ($mandatoryValues 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 string $message
     * 
     * @return string
     */
    protected function _translate($message)
    {
        $translator = $this->getTranslator();
        if (!$translator) {
            return $message;
        }
        return $translator->translate($message);
    }
}
