<?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) 2015 (original work) Open Assessment Technologies SA (under the project TAO-PRODUCT);
 *
 */

namespace oat\taoDeliveryRdf\model;

use oat\taoGroups\models\GroupsService;
use oat\oatbox\user\User;
use oat\oatbox\service\ConfigurableService;
use core_kernel_classes_Resource;
use core_kernel_classes_Class;
use \core_kernel_classes_Property;
use oat\taoDelivery\model\AssignmentService;
use oat\taoDeliveryRdf\model\guest\GuestTestUser;
use oat\taoDelivery\model\RuntimeService;
use oat\taoDelivery\model\AttemptServiceInterface;

/**
 * Service to manage the assignment of users to deliveries
 *
 * @access public
 * @author Joel Bout, <joel@taotesting.com>
 * @package taoDelivery
 */
class GroupAssignment extends ConfigurableService implements AssignmentService
{
    /**
     * Interface part
     */
    const PROPERTY_GROUP_DELIVERY = 'http://www.tao.lu/Ontologies/TAOGroup.rdf#Deliveries';

    const DISPLAY_ATTEMPTS_OPTION = 'display_attempts';

    const DISPLAY_DATES_OPTION = 'display_dates';

    /**
     * (non-PHPdoc)
     * @see \oat\taoDelivery\model\AssignmentService::getAssignments()
     */
    public function getAssignments(User $user)
    {
        $assignments = [];
        foreach ($this->getAssignmentFactories($user) as $factory) {
            $assignments[] = $factory->toAssignment();
        }
        
        return $this->orderAssignments($assignments);
    }

    /**
     * @param User $user
     * @return array
     */
    public function getAssignmentFactories(User $user)
    {
        $assignments = [];

        $displayAttempts = ($this->hasOption(self::DISPLAY_ATTEMPTS_OPTION)) ? $this->getOption(self::DISPLAY_ATTEMPTS_OPTION) : true;

        $displayDates = ($this->hasOption(self::DISPLAY_DATES_OPTION)) ? $this->getOption(self::DISPLAY_DATES_OPTION) : true;

        if ($this->isDeliveryGuestUser($user)) {
            foreach ($this->getGuestAccessDeliveries() as $id) {
                $delivery = new \core_kernel_classes_Resource($id);
                $startable = $this->verifyTime($delivery) && $this->verifyToken($delivery, $user);
                $assignments[] = $this->getAssignmentFactory($delivery, $user, $startable, $displayAttempts, $displayDates);
            }
        } else {
            foreach ($this->getDeliveryIdsByUser($user) as $id) {
                $delivery = new \core_kernel_classes_Resource($id);
                $startable = $this->verifyTime($delivery) && $this->verifyToken($delivery, $user);
                $assignments[] = $this->getAssignmentFactory($delivery, $user, $startable, $displayAttempts, $displayDates);
            }
        }
        return $assignments;
    }

    /**
     * @deprecated
     */
    public function getRuntime($deliveryId)
    {
        return $this->getServiceLocator()->get(RuntimeService::SERVICE_ID)->getRuntime($deliveryId);
    }
    
    
    /**
     *
     * @param string $deliveryId
     * @return array identifiers of the users
     */
    public function getAssignedUsers($deliveryId)
    {
        $groupClass = GroupsService::singleton()->getRootClass();
        $groups = $groupClass->searchInstances([
            self::PROPERTY_GROUP_DELIVERY => $deliveryId
        ], ['recursive' => true, 'like' => false]);
        
        $users = [];
        foreach ($groups as $group) {
            foreach (GroupsService::singleton()->getUsers($group) as $user) {
                $users[] = $user->getUri();
            }
        }
        return array_unique($users);
    }
    
    /**
     * Helpers
     */
    /**
     * @param core_kernel_classes_Resource $delivery
     */
    public function onDelete(core_kernel_classes_Resource $delivery)
    {
        $groupClass = GroupsService::singleton()->getRootClass();
        $assigned = $groupClass->searchInstances([
            self::PROPERTY_GROUP_DELIVERY => $delivery
        ], ['like' => false, 'recursive' => true]);
        
        $assignationProperty = new core_kernel_classes_Property(self::PROPERTY_GROUP_DELIVERY);
        foreach ($assigned as $groupInstance) {
            $groupInstance->removePropertyValue($assignationProperty, $delivery);
        }
    }

    /**
     * @param User $user
     * @return array
     */
    public function getDeliveryIdsByUser(User $user)
    {
        $deliveryUris = [];
        // check if really available
        foreach (GroupsService::singleton()->getGroups($user) as $group) {
            foreach ($group->getPropertyValues(new \core_kernel_classes_Property(self::PROPERTY_GROUP_DELIVERY)) as $deliveryUri) {
                $candidate = new core_kernel_classes_Resource($deliveryUri);
                if (!$this->isUserExcluded($candidate, $user) && $candidate->exists()) {
                    $deliveryUris[$candidate->getUri()] = $candidate->getUri();
                }
            }
        }

        ksort($deliveryUris);
        return $deliveryUris;
    }

    /**
     * Check if a user is excluded from a delivery
     * @param core_kernel_classes_Resource $delivery
     * @param User $user the URI of the user to check
     * @return boolean true if excluded
     */
    protected function isUserExcluded(\core_kernel_classes_Resource $delivery, User $user)
    {
        $excludedUsers = $delivery->getPropertyValues(new \core_kernel_classes_Property(DeliveryContainerService::PROPERTY_EXCLUDED_SUBJECTS));
        return in_array($user->getIdentifier(), $excludedUsers);
    }

    /**
     * Search for deliveries configured for guest access
     *
     * @return array
     */
    public function getGuestAccessDeliveries()
    {
        $class = new core_kernel_classes_Class(DeliveryAssemblyService::CLASS_URI);

        return $class->searchInstances(
            [
                DeliveryContainerService::PROPERTY_ACCESS_SETTINGS => DeliveryAssemblyService::PROPERTY_DELIVERY_GUEST_ACCESS
            ],
            ['recursive' => true]
        );
    }

    /**
     * Check if current user is guest
     *
     * @param User $user
     * @return bool
     */
    public function isDeliveryGuestUser(User $user)
    {
        return ($user instanceof GuestTestUser);
    }

    /**
     * @param string $deliveryIdentifier
     * @param User $user
     * @return bool
     */
    public function isDeliveryExecutionAllowed($deliveryIdentifier, User $user)
    {
        $delivery = new \core_kernel_classes_Resource($deliveryIdentifier);
        return $this->verifyUserAssigned($delivery, $user)
            && $this->verifyTime($delivery)
            && $this->verifyToken($delivery, $user);
    }

    /**
     * @param core_kernel_classes_Resource $delivery
     * @param User $user
     * @return bool
     */
    protected function verifyUserAssigned(core_kernel_classes_Resource $delivery, User $user)
    {
        $returnValue = false;
    
        //check for guest access mode
        if ($this->isDeliveryGuestUser($user) && $this->hasDeliveryGuestAccess($delivery)) {
            $returnValue = true;
        } else {
            $userGroups = GroupsService::singleton()->getGroups($user);
            $deliveryGroups = GroupsService::singleton()->getRootClass()->searchInstances([
                self::PROPERTY_GROUP_DELIVERY => $delivery->getUri()
            ], [
                'like' => false, 'recursive' => true
            ]);
            $returnValue = count(array_intersect($userGroups, $deliveryGroups)) > 0 && !$this->isUserExcluded($delivery, $user);
        }
    
        return $returnValue;
    }
    
    /**
     * Check if delivery configured for guest access
     *
     * @param core_kernel_classes_Resource $delivery
     * @return bool
     * @throws \common_exception_InvalidArgumentType
     */
    protected function hasDeliveryGuestAccess(core_kernel_classes_Resource $delivery)
    {
        $returnValue = false;
    
        $properties = $delivery->getPropertiesValues([
            new core_kernel_classes_Property(DeliveryContainerService::PROPERTY_ACCESS_SETTINGS),
        ]);
        $propAccessSettings = current($properties[DeliveryContainerService::PROPERTY_ACCESS_SETTINGS ]);
        $accessSetting = (!(is_object($propAccessSettings)) or ($propAccessSettings == "")) ? null : $propAccessSettings->getUri();
    
        if (!is_null($accessSetting)) {
            $returnValue = ($accessSetting === DeliveryAssemblyService::PROPERTY_DELIVERY_GUEST_ACCESS);
        }
    
        return $returnValue;
    }

    /**
     * @param core_kernel_classes_Resource $delivery
     * @param User $user
     * @return bool
     */
    protected function verifyToken(core_kernel_classes_Resource $delivery, User $user)
    {
        $propMaxExec = $delivery->getOnePropertyValue(new \core_kernel_classes_Property(DeliveryContainerService::PROPERTY_MAX_EXEC));
        $maxExec = is_null($propMaxExec) ? 0 : $propMaxExec->literal;
        
        //check Tokens
        $usedTokens = count($this->getServiceLocator()->get(AttemptServiceInterface::SERVICE_ID)
            ->getAttempts($delivery->getUri(), $user));
    
        if (($maxExec != 0) && ($usedTokens >= $maxExec)) {
            \common_Logger::d("Attempt to start the compiled delivery " . $delivery->getUri() . "without tokens");
            return false;
        }
        return true;
    }

    /**
     * @param core_kernel_classes_Resource $delivery
     * @return bool
     */
    protected function verifyTime(core_kernel_classes_Resource $delivery)
    {
        $deliveryProps = $delivery->getPropertiesValues([
            DeliveryContainerService::PROPERTY_START,
            DeliveryContainerService::PROPERTY_END,
        ]);
        
        $startExec = empty($deliveryProps[DeliveryContainerService::PROPERTY_START])
            ? null
            : (string)current($deliveryProps[DeliveryContainerService::PROPERTY_START]);
        $stopExec = empty($deliveryProps[DeliveryContainerService::PROPERTY_END])
            ? null
            : (string)current($deliveryProps[DeliveryContainerService::PROPERTY_END]);
        
        $startDate  =    date_create('@' . $startExec);
        $endDate    =    date_create('@' . $stopExec);
        if (!$this->areWeInRange($startDate, $endDate)) {
            \common_Logger::d("Attempt to start the compiled delivery " . $delivery->getUri() . " at the wrong date");
            return false;
        }
        return true;
    }
    
    /**
     * Check if the date are in range
     * @param type $startDate
     * @param type $endDate
     * @return boolean true if in range
     */
    protected function areWeInRange($startDate, $endDate)
    {
        return (empty($startDate) || date_create() >= $startDate)
        && (empty($endDate) || date_create() <= $endDate);
    }
    
    /**
     * Order Assignments of a given user.
     *
     * By default, this method relies on the taoDelivery:DisplayOrder property
     * to order the assignments (Ascending order). However, implementers extending
     * the GroupAssignment class are encouraged to override this method if they need
     * another behaviour.
     *
     * @param array $assignments An array of assignments.
     * @return array The $assignments array ordered.
     */
    protected function orderAssignments(array $assignments)
    {
        usort($assignments, function ($a, $b) {
            return $a->getDisplayOrder() - $b->getDisplayOrder();
        });
        
        return $assignments;
    }

    /**
     * @param core_kernel_classes_Resource $delivery
     * @param User $user
     * @param $startable
     * @param bool $displayAttempts
     * @return AssignmentFactory
     */
    protected function getAssignmentFactory(\core_kernel_classes_Resource $delivery, User $user, $startable, $displayAttempts = true, $displayDates = true)
    {
        $factory = new AssignmentFactory($delivery, $user, $startable, $displayAttempts, $displayDates);
        $factory->setServiceLocator($this->getServiceLocator());
        return $factory;
    }
}