<?php

/**
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; under version 2
 * of the License (non-upgradable).
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 *
 * Copyright (c) 2018 (original work) Open Assessment Technologies SA;
 *
 *
 */

namespace oat\tao\model\actionQueue\implementation;

use oat\oatbox\event\EventManager;
use oat\oatbox\session\SessionService;
use oat\tao\model\actionQueue\ActionQueue;
use oat\oatbox\service\ConfigurableService;
use oat\tao\model\actionQueue\QueuedAction;
use oat\tao\model\actionQueue\ActionQueueException;
use oat\oatbox\user\User;
use oat\tao\model\actionQueue\restriction\BasicRestriction;
use oat\tao\model\actionQueue\event\InstantActionOnQueueEvent;
use oat\tao\model\actionQueue\event\ActionQueueTrendEvent;

/**
 *
 * Interface InstantActionQueue
 * @author Aleh Hutnikau, <hutnikau@1pt.com>
 * @package oat\tao\model\actionQueue
 */
class InstantActionQueue extends ConfigurableService implements ActionQueue
{

    const QUEUE_TREND = 'queue_trend';

    /**
     * @return EventManager
     */
    protected function getEventManager()
    {
        return $this->getServiceLocator()->get(EventManager::SERVICE_ID);
    }


    /**
     * @param QueuedAction $action
     * @param User $user
     * @return boolean
     * @throws
     */
    public function perform(QueuedAction $action, User $user = null)
    {
        $this->propagate($action);
        if ($user === null) {
            $user = $this->getServiceLocator()->get(SessionService::SERVICE_ID)->getCurrentUser();
        }
        $result = false;
        $actionConfig = $this->getActionConfig($action);
        $restrictions = $this->getRestrictions($actionConfig);
        $allowExecution = $this->checkRestrictions($restrictions);

        if ($allowExecution) {
            $actionResult = $action([]);
            $action->setResult($actionResult);
            $result = true;
            $this->dequeue($action, $user);
        } else {
            $this->queue($action, $user);
        }
        return $result;
    }

    /**
     * Note that this method is not transaction safe so there may be collisions.
     * This implementation supposed to provide approximate position in the queue
     * @param QueuedAction $action
     * @param User $user
     * @return integer
     * @throws
     */
    public function getPosition(QueuedAction $action, User $user = null)
    {
        $action->setServiceLocator($this->getServiceManager());
        $positions = $this->getPositions($action);
        return count($positions);
    }

    /**
     * @inheritdoc
     */
    public function clearAbandonedPositions(QueuedAction $action)
    {
        $action->setServiceLocator($this->getServiceManager());
        $key = $this->getQueueKey($action);
        $positions = $this->getPositions($action);
        $edgeTime = time() - $this->getTtl($action);
        $newPositions = array_filter($positions, function ($val) use ($edgeTime) {
            return $val > $edgeTime;
        });
        $this->getPersistence()->set($key, json_encode($newPositions));
        $this->getPersistence()->set(get_class($action) . self::QUEUE_TREND, 0);
        return count($positions) - count($newPositions);
    }

    /**
     * @return int
     */
    public function getTrend(QueuedAction $action)
    {
        $trend = $this->getPersistence()->get(get_class($action) . self::QUEUE_TREND);
        return (int)$trend;
    }

    /**
     * @param  QueuedAction $action
     * @return bool
     * @throws ActionQueueException
     */
    public function isActionEnabled(QueuedAction $action): bool
    {
        $actionConfig = $this->getActionConfig($action);
        foreach ($this->getRestrictions($actionConfig) as $restriction => $value) {
            if ($value !== 0) {
                return true;
            }
        }

        return false;
    }

    /**
     * @param QueuedAction $action
     * @param User $user
     * @throws \common_Exception
     */
    protected function queue(QueuedAction $action, User $user)
    {
        $key = $this->getQueueKey($action);
        $positions = $this->getPositions($action);
        $positions[$user->getIdentifier()] = time();
        $this->getPersistence()->set($key, json_encode($positions));
        $this->getEventManager()->trigger(new InstantActionOnQueueEvent($key, $user, $positions, 'queue', $action));
        if ($this->getTrend($action) >= 0) {
            $this->getEventManager()->trigger(new ActionQueueTrendEvent($action, true));
            $this->getPersistence()->set(get_class($action) . self::QUEUE_TREND, -1);
        }
    }

    /**
     * @param QueuedAction $action
     * @param User $user
     * @throws \common_Exception
     */
    protected function dequeue(QueuedAction $action, User $user)
    {
        $key = $this->getQueueKey($action);
        $positions = $this->getPositions($action);
        if (array_key_exists($user->getIdentifier(), $positions)) {
            // now we sure that this user has been queued
            unset($positions[$user->getIdentifier()]);
            $this->getEventManager()->trigger(new InstantActionOnQueueEvent($key, $user, $positions, 'dequeue', $action));
            $this->getPersistence()->set($key, json_encode($positions));

            if ($this->getTrend($action) <= 0) {
                $this->getEventManager()->trigger(new ActionQueueTrendEvent($action, false));
                $this->getPersistence()->set(get_class($action) . self::QUEUE_TREND, 1);
            }
        }
    }

    /**
     * @return \common_persistence_KeyValuePersistence
     * @throws \oat\oatbox\service\exception\InvalidServiceManagerException
     */
    protected function getPersistence()
    {
        $persistenceId = $this->getOption(self::OPTION_PERSISTENCE);
        return $this->getServiceManager()->get(\common_persistence_Manager::SERVICE_ID)->getPersistenceById($persistenceId);
    }

    /**
     * @param QueuedAction $action
     * @throws
     * @return integer
     */
    protected function getTtl(QueuedAction $action)
    {
        $actionConfig = $this->getActionConfig($action);
        $ttl = (int) (isset($actionConfig[self::ACTION_PARAM_TTL]) ? $actionConfig[self::ACTION_PARAM_TTL] : 0);
        return $ttl;
    }

    /**
     * @param QueuedAction $action
     * @return string
     */
    protected function getQueueKey(QueuedAction $action)
    {
        return self::class . '_' . $action->getId();
    }

    /**
     * @param QueuedAction $action
     * @throws ActionQueueException in action was not registered in the config
     * @return array
     */
    protected function getActionConfig(QueuedAction $action)
    {
        $actions = $this->getOption(self::OPTION_ACTIONS);
        if (!isset($actions[$action->getId()])) {
            throw new ActionQueueException(__('Action `%s` is not configured in the action queue service', $action->getId()));
        }
        return $actions[$action->getId()];
    }

    /**
     * @param QueuedAction $action
     * @return array
     */
    protected function getPositions(QueuedAction $action)
    {
        $key = $this->getQueueKey($action);
        $positions = json_decode($this->getPersistence()->get($key), true);
        if (!$positions) {
            $positions = [];
        }
        return $positions;
    }

    /**
     * @param array $actionConfig
     * @return array
     */
    private function getRestrictions(array $actionConfig): array
    {
        return $actionConfig['restrictions'] ?? [];
    }

    /**
     * @param array $restrictions
     * @return bool
     */
    private function checkRestrictions(array $restrictions): bool
    {
        $allowExecution = true;

        foreach ($restrictions as $restrictionClass => $value) {
            if (class_exists($restrictionClass) && is_subclass_of($restrictionClass, BasicRestriction::class)) {
                /** @var BasicRestriction $restriction */
                $restriction = new $restrictionClass();
                $this->propagate($restriction);
                $allowExecution = $allowExecution && $restriction->doesComply($value);
            }
        }
        return $allowExecution;
    }
}