<?php

namespace App\Model;

use App\Model\Entities\Security\Role;
use App\Model\Entities\Security\Rule;
use App\Model\Interfaces\IResource;
use App\Model\Interfaces\ITemplate;
use App\Model\Services\PermissionService;
use App\Model\Services\UserService;
use Doctrine\DBAL\DBALException;
use Nette\InvalidArgumentException;
use Nette\Security\Permission as NettePermission;

class Permission extends NettePermission
{
    /** @var PermissionService */
    protected $permissionService;
    /** @var UserService */
    protected $userService;

    protected $loggedInUserId;

    /**
     * @param PermissionService $permissionService
     * @param UserService $userService
     */
    public function __construct(PermissionService $permissionService, UserService $userService)
    {
        $this->permissionService = $permissionService;
        $this->userService = $userService;

        try {
            $webmasterRole = $this->permissionService->getWebmasterRole();
            if ($webmasterRole) {
                $this->addRole($webmasterRole->getName());
                $this->allow($webmasterRole->getName());
            }

            $guestRole = $this->permissionService->getGuestRole();
            if ($guestRole) {
                $this->loadRole($guestRole);
            }
        } catch (DBALException $e) {
            // database is not set
        }
    }

    public function setLoggedInUserId($id)
    {
        $this->loggedInUserId = $id;

        try {
            $user = $this->userService->findUserById($id);
            foreach ($user->getRoles() as $roleEntity) {
                $this->loadRole($roleEntity);
            }
        } catch (DBALException $e) {
            // database is not set
        }
    }

    protected function loadRole(Role $roleEntity)
    {
        if ($this->hasRole($roleEntity->getName())) {
            return;
        }

        $parentRoles = $roleEntity->getParentRoles();
        if ($parentRoles->isEmpty()) {
            $this->addRole($roleEntity->getName());
        } else {
            $parents = array();
            foreach ($parentRoles as $parentRoleEntity) {
                $parents = $parentRoleEntity->getName();
                $this->loadRole($parentRoleEntity);
            }
            $this->addRole($roleEntity->getName(), $parents);
        }
    }

    public function isAllowed($role = self::ALL, $resource = self::ALL, $privilege = self::ALL)
    {
        if (!$resource instanceof IResource && $resource !== self::ALL) {
            throw new InvalidArgumentException("Resource must be instance of Resource - " . gettype($resource) . "given.");
        }

        if ($this->loggedInUserId === $resource->getCreatedBy()->getId()) {
            return true;
        }

        $this->autoloadResource($resource->getResourceTemplate(), $resource->getState());

        return parent::isAllowed($role, $this->getResourceName($resource->getResourceTemplate(), $resource->getState()), $privilege);
    }

    /**
     * @param ITemplate $template
     * @param string $state
     * @return mixed
     */
    protected function autoloadResource(ITemplate $template, string $state)
    {
        if ($this->hasResource($this->getResourceName($template, $state))) {
            return;
        }

        if ($template->hasParentTemplate()) {
            $this->autoloadResource($template->getParentTemplate(), $state);
            $this->addResource($this->getResourceName($template, $state), $this->getResourceName($template->getParentTemplate(), $state));
        } else {
            $this->addResource($this->getResourceName($template, $state));
        }

        $rules = $this->permissionService->findRules($template, $state);

        /** @var Rule $rule */
        foreach ($rules as $rule) {
            $this->loadRule($rule);
        }
    }

    protected function loadRule(Rule $rule)
    {
        $role = $rule->getRole()->getName();
        if (!$this->hasRole($role)) {
            return;     // user does not have this role => rule loading is not necessary
        }

        $resource = $this->getResourceName($rule->getTemplate(), $rule->getState());

        if (!$this->hasResource($resource)) {
            $this->addResource($resource);
        }

        if ($rule->getType() === Rule::ALLOW) {
            $this->allow($role, $resource, $rule->getAction());
        } else {
            $this->deny($role, $resource, $rule->getAction());
        }
    }

    protected function getResourceName(ITemplate $template, $state)
    {
        $name = get_class($template);
        $name .= '_';
        $name .= $template->getId();
        $name .= '_';
        $name .= $state;

        return  $name;
    }

}