<?php
/**
 * Base entity
 *
 * LICENSE: This Software is the property of Lifestyle Webconsulting GmbH (Aschaffenburg, Germany)
 * and is protected by copyright law - it is NOT Freeware.
 *
 * Any unauthorized use of this software without a valid license
 * is a violation of the license agreement and will be prosecuted by
 * civil and criminal law.
 *
 * @copyright  2014 Lifestyle Webconsulting GmbH
 * @version    $Id:$
 * @link       http://www.life-style.de
 */

namespace AppBundle\Entity;

use Sso\WebserviceBundle\Error\Storage as ErrorStorage;
use Sso\WebserviceBundle\Error\Type\External as ExternalError;
use Symfony\Component\Validator\Validator\LegacyValidator as Validator;
use Sso\WebserviceBundle\Exception\HiddenException;
use Symfony\Component\Validator\Validator\ValidatorInterface;

/**
 * Base entity
 */
abstract class Base
{
    /**
     * Errors
     *
     * @var ErrorStorage
     */
    private $errors;

    /**
     * Validator
     *
     * @var ValidatorInterface
     */
    private $validator;

    /**
     * List of fields, that has been changed via set
     *
     * @var array
     */
    private $changedFields = array();

    /**
     * Constructor
     *
     * @param ValidatorInterface $validator
     */
    public function __construct(ValidatorInterface $validator)
    {
        $this->setValidator($validator);
    }

    /**
     * Set validator
     *
     * @param ValidatorInterface $validator
     */
    public function setValidator(ValidatorInterface $validator)
    {
        $this->validator = $validator;
    }

    /**
     * Set values
     *
     * @param array|object $values
     * @param array|null $validationGroups Null for full validation, array of group-name(s) for group specific validation
     */
    public function init($values, $validationGroups, ErrorStorage $errors)
    {
        // Throw exception, if input format is not valid
        if (!is_array($values) && !($values instanceof \Traversable)) {
            $errors->addError(new ExternalError('b1', 'b1', 'Invalid data format', 'Data should be an array or an traversable object, but is ' . get_class($values) . '.'));
            throw new \RuntimeException();
        }

        // Fill up object with values
        $this->_setValues($values);

        // Validate
        $this->validate($validationGroups);

        // Throw exception, if any error has occured
        if ($this->errors()->hasErrors()) {
            $errors->addErrors($this->errors->getErrors());
            throw new HiddenException();
        }
    }

    /**
     * Set properties
     *
     * @param array|object $values
     */
    private function _setValues($values)
    {
        $castValues = !is_array($values);

        // Get methods
        $methods = $this->_getSetters();

        // Set values
        foreach ($values as $key => $value) {
            if (!isset($methods[strtolower($key)])) {
                continue;
            }
            if ($castValues) {
                $value = (string) $value;
            }
            $method = $methods[strtolower($key)];
            $this->$method($value);
        }
    }

    /**
     * Validate object
     *
     * @param array|null $validationGroups Null for full validation, array of group-name(s) for group specific validation
     * @return bool True on successful validation
     */
    public function validate($validationGroups = null)
    {
        $errors = $this->validator->validate($this, null, $validationGroups);
        foreach ($errors as $error) {
            list($errorCode, $shortMessage, $longMessage) = explode('##', $error->getMessage());
            $this->errors()->addError(new ExternalError('v001', $errorCode, $shortMessage, $longMessage));
        }
        return 0 === count($errors);
    }

    /**
     * Returns an assoc array of setter methods
     *
     * @return array
     */
    private function _getSetters()
    {
        $methods = array();
        foreach (array_filter(get_class_methods($this), array($this, 'filterSetters')) as $method) {
            $methods[strtolower(substr($method, 3))] = $method;
        }
        return $methods;
    }

    /**
     * Filter list of methods
     *
     * @param string $method
     * @return bool
     */
    public function filterSetters($method)
    {
        return preg_match('/^set[A-Z]/', $method);
    }

    /**
     * Returns true, if data is valid, no error found
     *
     * @return bool
     */
    public function isValid()
    {
        return $this->validate() && !$this->errors()->hasErrors();
    }

    /**
     * Mark value as changed
     *
     * @param string $field
     */
    protected function _setChanged($field)
    {
        $this->changedFields[$field] = true;
    }

    /**
     * Returns true, if value has been changed
     *
     * @param string $field
     * @return bool
     */
    protected function _hasChanged($field)
    {
        return isset($this->changedFields[$field]);
    }

    /**
     * Get list of changed fields
     *
     * @return array
     */
    public function getChangedFields()
    {
        return array_keys($this->changedFields);
    }

    /**
     * Get error storage
     *
     * @return ErrorStorage
     */
    public function errors()
    {
        return $this->errors ? $this->errors : ($this->errors = new ErrorStorage());
    }
}
