<?php
namespace DynCom\mysyde\common\classes;
use DynCom\mysyde\common\interfaces\GenericDBModelInterface;
use DynCom\mysyde\common\interfaces\GenericDBQueryWrapperInterface;
use DynCom\mysyde\common\interfaces\Repository;
use DynCom\mysyde\common\interfaces\RepositoryUnitOfWorkInterface;

/**
 * Created by PhpStorm.
 * User: Bauer
 * Date: 6/2/2015
 * Time: 10:04 AM
 */
class UnitOfWork implements RepositoryUnitOfWorkInterface
{

    const ENTITY_STATE_NEW = 'ENTITY_STATE_NEW';
    const ENTITY_STATE_CLEAN = 'ENTITY_STATE_CLEAN';
    const ENTITY_STATE_DIRTY = 'ENTITY_STATE_DIRTY';
    const ENTITY_STATE_REMOVED = 'ENTITY_STATE_REMOVED';

    protected $possibleStates = array(self::ENTITY_STATE_NEW, self::ENTITY_STATE_CLEAN, self::ENTITY_STATE_DIRTY, self::ENTITY_STATE_REMOVED);
    protected $repositories;
    protected $entityCollection;
    protected $db;

    /**
     * UnitOfWork constructor.
     * @param GenericDBQueryWrapperInterface $queryWrapper
     */
    public function __construct(GenericDBQueryWrapperInterface $queryWrapper)
    {
        $this->entityCollection = new GenericObjectStorage();
        $this->db = $queryWrapper;
    }

    /**
     * @param Repository $repository
     */
    public function registerRepository(Repository $repository)
    {
        $objectClass = $repository->getObjectClass();
        $this->repositories[$objectClass] = $repository;
    }

    /**
     * @param GenericDBModelInterface $entity
     */
    public function registerNew(GenericDBModelInterface $entity)
    {
        $this->registerEntity($entity, self::ENTITY_STATE_NEW);
    }

    /**
     * @param GenericDBModelInterface $entity
     */
    public function registerClean(GenericDBModelInterface $entity)
    {
        $this->registerEntity($entity, self::ENTITY_STATE_CLEAN);
    }

    /**
     * @param GenericDBModelInterface $entity
     */
    public function registerDirty(GenericDBModelInterface $entity)
    {
        $this->registerEntity($entity, self::ENTITY_STATE_DIRTY);
    }

    /**
     * @param GenericDBModelInterface $entity
     */
    public function registerRemoved(GenericDBModelInterface $entity)
    {
        $this->registerEntity($entity, self::ENTITY_STATE_REMOVED);
    }

    /**
     * @param GenericDBModelInterface $entity
     * @param $state
     */
    public function registerEntity(GenericDBModelInterface $entity, $state)
    {
        if (!in_array($state, $this->possibleStates, true)) {
            throw new \InvalidArgumentException('State \'' . $state . '\' to register is not defined');
        }
        $className = get_class($entity);
        if (!array_key_exists($className, $this->repositories)) {
            throw new \InvalidArgumentException('UoW has no repository for entity of class ' . $className);
        }
        $data = array('className' => get_class($entity),'state' => $state);

        if (isset($entity->id) && ((int)$entity->id > 0)) {
            $this->repositories[$className]->lockRecordByID((int)$entity->id);
        }
        if($this->entityCollection->contains($entity)) {
            $data = $this->entityCollection->offsetGet($entity);
            $existingObjState = $data['state'];
            if($existingObjState === $state) {
                $this->entityCollection->detach($entity);
            } else {
                throw new \LogicException('Cannot register the same entity with different states');
            }
        }
        $this->entityCollection->attach($entity, $data);
    }

    /**
     * @param GenericDBModelInterface $entity
     */
    public function unregisterEntity(GenericDBModelInterface $entity)
    {
        $className = get_class($entity);
        if (!array_key_exists($className, $this->repositories)) {
            throw new \InvalidArgumentException('UoW has no repository of entity of class ' . $className);
        }
        if ($this->entityCollection->contains($entity)) {
            $this->entityCollection->detach($entity);
        }
        if (isset($entity->id) && ((int)$entity->id > 0)) {
            $this->repositories[$className]->releaseRecordLock((int)$entity->id);
        }
    }

    /**
     * @param Repository $repository
     */
    public function unregisterRepository(Repository $repository)
    {
        $className = $repository->getObjectClass();
        if (array_key_exists($className, $this->repositories)) {
            unset($this->repositories[$className]);
        }
    }

    /**
     * @return $this
     */
    public function clear()
    {
        $this->entityCollection->clear();
        return $this;
    }

    public function commit()
    {

        if (!$this->db->startTransaction()) {
            throw new \BadMethodCallException('Could not start transaction. ' . $this->db->getErrorMessage());
        }

        $c = $this->entityCollection;
        $c->rewind();
        while ($c->valid()) {

            $object = $c->current();
            $data = $c->getInfo();
            $className = $data['className'];
            $state = $data['state'];
            $repository = $this->repositories[$className];
            if($repository instanceof Repository && $object instanceof GenericDBModelInterface) {
                switch ($state) {
                    case self::ENTITY_STATE_CLEAN:
                        break;
                    case self::ENTITY_STATE_NEW:
                        $currInsertQuery = $repository->getCreateInDBQuery($object);
                        $this->db->setQuery($currInsertQuery);
                        if (!$this->db->doQuery()) {
                            throw new \BadMethodCallException('Could not execute Query in transaction. ' . $this->db->getErrorMessage());
                        }
                        break;
                    case self::ENTITY_STATE_DIRTY:
                        $currUpdateQuery = $repository->getUpdateSingleQuery($object);
                        $this->db->setQuery($currUpdateQuery);
                        if (!$this->db->doQuery()) {
                            throw new \BadMethodCallException('Could not execute Query in transaction. ' . $this->db->getErrorMessage());
                        }
                        break;
                    case self::ENTITY_STATE_REMOVED:
                        $currDeleteQuery = $repository->getDeleteByIDQuery($object);
                        $this->db->setQuery($currDeleteQuery);
                        if (!$this->db->doQuery()) {
                            throw new \BadMethodCallException('Could not execute Query in transaction. ' . $this->db->getErrorMessage());
                        }
                        break;
                }
            }
            $c->next();
        }
        if (!$this->db->commitTransaction()) {
            throw new \BadMethodCallException('Could not commit transaction. ' . $this->db->getErrorMessage());
        }

        $c->rewind();
        while ($c->valid()) {

            $index = $c->key();
            $object = $c->current();
            $data = $c->getInfo();
            $className = $data['className'];
            $state = $data['state'];
            $repository = $this->repositories[$className];
            if ($repository instanceof Repository && $object instanceof GenericDBModelInterface)
            {
	    	switch ($state) {
                    case self::ENTITY_STATE_CLEAN: break;
                    case self::ENTITY_STATE_NEW:
                        $repository->add($object,false);
                        break;
                    case self::ENTITY_STATE_DIRTY:
                        $repository->releaseRecordLock($object->id);
                        $repository->recacheEntityFromDBByID($object->id);
                        break;
                    case self::ENTITY_STATE_REMOVED:
                        $repository->releaseRecordLock($object->id);
                        $repository->removeEntityFromCache($object);
                        break;
                }
	    }
            $c->next();
        }

        $this->clear();

    }

    public function rollback()
    {
        //Only release record locks - transaction actually happens only on commit(),
        //but entities with ID are locked from registration in their repositories
        $c = $this->entityCollection;
        $c->rewind();
        while ($c->valid()) {
            $index = $c->key();
            $object = $c->current();
            $data = $c->getInfo();
            $className = $data['className'];
            $state = $data['state'];
            $repository = $this->repositories[$className];
            if (isset($object->id) && ((int)$object->id > 0) && $repository instanceof Repository) {
                $repository->releaseRecordLock($object->id);
            }
            $c->next();
        }
        $c->rewind();
    }
}