<?php
namespace IntellexApps\PHP\UploadFile\Domain\Service;

use IntellexApps\PHP\UploadFile\Domain\Exception\FileSystem\InvalidPathException;
use IntellexApps\PHP\UploadFile\Domain\Exception\Permission\InvalidPermissionException;
use IntellexApps\PHP\UploadFile\Domain\Exception\Permission\InvalidRoleException;
use IntellexApps\PHP\UploadFile\Domain\Exception\Permission\PermissionDeniedException;
use IntellexApps\PHP\UploadFile\Domain\Model\FolderItem;
use IntellexApps\PHP\UploadFile\Domain\Request\File\UpdatePermissionsRequest;
use IntellexApps\PHP\UploadFile\Util\Configuration;
use IntellexApps\PHP\UploadFile\Util\Inflector;

class PermissionService extends BaseService
{

    const PERMISSION_NONE = 0;
    const PERMISSION_WRITE = 2;
    const PERMISSION_READ = 4;
    const PERMISSION_READ_WRITE = 6;
    const LOCAL_PERMISSIONS_DIR = 'permissions.data';
    const ROOT_FOLDER_KEY = '.';

    /**
     * Get permission .
     *
     * @param $int
     * @return string
     */
    public static function getPermissionStringRepresentation($int)
    {
        $permission = '';
        switch($int) {
            case 1:
                $permission = 'x';
                break;
            case 2:
                $permission = 'w';
                break;
            case 4:
                $permission = 'r';
                break;
            case 5:
                $permission = 'rx';
                break;
            case 6:
                $permission = 'rw';
                break;
            case 7:
                $permission = 'rwx';
                break;
        }

        return str_replace('x', '', $permission);
    }

    /**
     * Get permission code for string
     *
     * @param $string
     * @param bool $isDir
     * @return int
     */
    public static function getPermissionCodeForString($string, $isDir = false)
    {
        switch($string) {
            case 'r':
                if($isDir) {
                    return 7;
                }
                return 4;
            case 'rw':
                if($isDir) {
                    return 7;
                }
                return 6;
            case 'rwx':
                return 7;
            default:
                return 0;
        }
    }

    /**
     * Get current role
     *
     * @return int
     */
    public function getCurrentRole()
    {
        // TODO: Load from current user
        return 1;
    }

    /**
     * Build roles
     *
     * TODO: These are just default ones - pull from config if provided
     *
     * @return array
     */
    public function buildRoles()
    {
        return [
            '1' => [
                'id' => 1,
                'name' => 'Admin',
                'is_active' => true,
                'permissions' => [
                    'directory' => self::PERMISSION_READ_WRITE,
                    'file' => self::PERMISSION_READ_WRITE
                ]
            ],
            '2' => [
                'id' => 2,
                'name' => 'Manager',
                'is_active' => true,
                'permissions' => [
                    'directory' => self::PERMISSION_READ,
                    'file' => self::PERMISSION_READ_WRITE
                ]
            ],
            '3' => [
                'id' => 3,
                'name' => 'Viewer',
                'is_active' => true,
                'permissions' => [
                    'directory' => self::PERMISSION_READ,
                    'file' => self::PERMISSION_READ
                ]
            ]
        ];
    }

    /**
     * Change role
     *
     * @param $newRole
     */
    public function changeRole($newRole) {}

    /**
     * Change permission
     *
     * @param $path
     * @param $roleId
     * @param $permission
     * @return mixed
     * @throws InvalidPathException
     * @throws InvalidPermissionException
     * @throws InvalidRoleException
     */
    public function changePermission($path, $roleId, $permission)
    {

        // Validate persmission
        if(!$this->validatePermission($permission)) {
            throw new InvalidPermissionException($permission);
        }

        // Validate role
        if(!$this->validateRole($roleId)) {
            throw new InvalidRoleException($roleId);
        }

        $fullPath = $this->get('upload')->getFullPath($path);
        if(!$this->get('file_system')->itemExist($fullPath)) {
            throw new InvalidPathException($path);
        }

        return $this->writePermissionForPath($path, $roleId, $permission);
    }


    /**
     * Check if file can be read
     *
     * @param $path
     * @return bool
     */
    public function canRead($path)
    {
        // TODO: Disabled for now
        return true;

        $permissions = $this->getPermissionsForPath($path);

        if($permissions > 0) {
            return true;
        }

        return false;
    }

    /**
     * Check if file is writable
     *
     * @param $path
     * @return bool
     */
    public function canWrite($path)
    {
        // TODO: Disabled for now
        return true;

        $permissions = $this->getPermissionsForPath($path);

        if($permissions == self::PERMISSION_READ_WRITE || $permissions == self::PERMISSION_WRITE) {
            return true;
        }

        return false;
    }

    /**
     * Do something if readable
     *
     * @param $path
     * @param $callable
     * @return
     * @throws PermissionDeniedException
     */
    public function ifReadable($path, $callable) {
        if($this->canRead($path)) {
            return $callable();
        }

        throw new PermissionDeniedException($path);
    }

    /**
     * Do something if writable
     *
     * @param $path
     * @param $callable
     * @return
     * @throws PermissionDeniedException
     */
    public function ifWritable($path, $callable) {
        if($this->canWrite($path)) {
            return $callable();
        }

        throw new PermissionDeniedException($path);

    }

    /**
     * Get permissions for path
     * z
     * @param $path
     * @param bool $all
     * @return mixed
     */
    public function getPermissionsForPath($path, $all = false)
    {
        $permissionFile = $this->getConfigurationDirForPath($path, self::LOCAL_PERMISSIONS_DIR);

        $itemName = $this->getKeyForPermissions($path);

        $dir = false;

        if(is_dir($path)) {
            $dir = true;
        }

        if(!file_exists($permissionFile)) {
            $this->get('file_system')->writeFile($permissionFile, serialize([]));
        }

        $permissions = unserialize(file_get_contents($permissionFile));

        if(isset($permissions[$itemName])) {
            if($all) {
                return $permissions[$itemName];
            }

            if(isset($permissions[$itemName][$this->getCurrentRole()])) {
                return $permissions[$itemName][$this->getCurrentRole()];
            }
        }

        if($all) {
            return null;
        }

        // Return default
        $roles = $this->get('configuration')->getRoles();

        if(isset($roles[$this->getCurrentRole()])) {
            return $roles[$this->getCurrentRole()]['permissions'][$dir ? 'directory' : 'file'];
        }
    }

    /**
     * Get configuration dir for path
     *
     * @param $path
     * @param null $file
     * @return mixed
     */
    private function getConfigurationDirForPath($path, $file = null)
    {
        $relativePath = FileSystemService::relativePath($path);

        $dir = $relativePath == '/' ? $path : dirname($path);
        $directory = $this->get('configuration')->getConfigDirectory($dir);

        if($file) {
            return Inflector::joinPaths([$directory, $file]);
        }

        return $directory;
    }

    /**
     * Validate permission
     *
     * @param $permission
     * @return bool
     * @throws InvalidPermissionException
     */
    private function validatePermission($permission)
    {
        $allowedPermissionValues = array(
            self::PERMISSION_NONE,
            self::PERMISSION_READ,
            self::PERMISSION_WRITE,
            self::PERMISSION_READ_WRITE
        );

        $allowedPermissionValues = array_combine($allowedPermissionValues, $allowedPermissionValues);
        if(!isset($allowedPermissionValues[$permission])) {
            return false;
        }

        return true;
    }

    /**
     * Check if role is valid
     *
     * @param $roleId
     * @return bool
     */
    private function validateRole($roleId)
    {
        $roles = $this->get('configuration')->getRoles();

        foreach($roles as $role) {
            if($role['id'] == $roleId) {
                return true;
            }
        }

        return false;
    }

    /**
     * Write permission for path
     *
     * @param $path
     * @param $roleId
     * @param $permission
     * @return mixed
     */
    private function writePermissionForPath($path, $roleId, $permission)
    {
        $permissionFile = $this->getConfigurationDirForPath($path, self::LOCAL_PERMISSIONS_DIR);

        if(!$this->get('file_system')->fileExists($permissionFile)) {
            $this->get('file_system')->writeFile($permissionFile, serialize([]), true);
        }

        $existingPermissions = unserialize($this->get('file_system')->readFile($permissionFile));

        if(empty($existingPermissions)) {
            $existingPermissions = [];
        }

        $key = basename($path);

        if(empty($key)) {
            $key = self::ROOT_FOLDER_KEY;
        }

        $existingPermissions[$key][$roleId] = $permission;

        $this->get('file_system')->writeFile($permissionFile, serialize($existingPermissions), true);

        return true;
    }


    /**
     * Item moved
     *
     * @param $source
     * @param $destination
     * @return bool
     */
    public function itemMoved($source, $destination)
    {

        // Get source permissions
        $sourcePermissions = $this->getPermissionsForPath($source, true);

        // No source permissions for item - nothing to do
        if(empty($sourcePermissions)) {
            return true;
        }

        // Get destination permissions
        $destinationPermissions = $this->getPermissionsForPath($destination, true);

        // Get item name
        $itemName = $this->getKeyForPermissions($destination);

        // Add new item permissions
        $destinationPermissions[$itemName] = $sourcePermissions;

        // Update permissions in destination
        $this->updateAllPermissionsForPath($destination, $destinationPermissions);

        // Delete permissions in source
        $this->deletePermissionsForPath($source);

        return true;
    }

    /**
     * Item copied
     *
     * @param $source
     * @param $destination
     * @return bool
     */
    public function itemCopied($source, $destination)
    {
        // Get source permissions
        $sourcePermissions = $this->getPermissionsForPath($source, true);

        // No source permissions for item - nothing to do
        if(empty($sourcePermissions)) {
            return true;
        }

        // Get destination permissions
        $destinationPermissions = $this->getPermissionsForPath($destination, true);

        // Get item name
        $itemName = $this->getKeyForPermissions($destination);

        // Add new item permissions
        $destinationPermissions[$itemName] = $sourcePermissions;

        // Update permissions in destination
        $this->updateAllPermissionsForPath($destination, $destinationPermissions);

        return true;
    }

    /**
     * Item renamed
     *
     * @param $source
     * @param $destination
     * @return bool
     */
    public function itemRenamed($source, $destination)
    {
        // Get source permissions
        $sourcePermissions = $this->getPermissionsForPath($source, true);

        // No source permissions for item - nothing to do
        if(empty($sourcePermissions)) {
            return true;
        }

        // Get destination permissions
        $destinationPermissions = $this->getPermissionsForPath($destination, true);

        // Get item name
        $itemName = $this->getKeyForPermissions($destination);

        // Add new item permissions
        $destinationPermissions[$itemName] = $sourcePermissions;

        // Update permissions in destination
        $this->updateAllPermissionsForPath($destination, $destinationPermissions);

        return true;
    }

    /**
     * Handle permissions on item deleted
     *
     * @param $path
     * @return bool
     */
    public function itemDeleted($path)
    {
        $this->deletePermissionsForPath($path);
        return true;
    }

    /**
     * Delete permissions for path
     *
     * @param $path
     * @return bool
     */
    private function deletePermissionsForPath($path)
    {
        $permissionFile = $this->getConfigurationDirForPath($path, self::LOCAL_PERMISSIONS_DIR);

        $permissions = unserialize($this->get('file_system')->readFile($permissionFile));

        $itemName = $this->getKeyForPermissions($path);

        if(isset($permissions[$itemName])) {
            unset($permissions[$itemName]);
            $this->get('file_system')->writeFile($permissionFile, serialize($permissions), true);
        }

        return true;
    }

    /**
     * Update all permissions for path
     *
     * @param $path
     * @param $permissions
     */
    private function updateAllPermissionsForPath($path, $permissions)
    {
        $permissionFile = $this->getConfigurationDirForPath($path, self::LOCAL_PERMISSIONS_DIR);
        $this->get('file_system')->writeFile($permissionFile, serialize($permissions), true);
    }

    /**
     * Get key for permissions
     *
     * @param $path
     * @return string
     */
    private function getKeyForPermissions($path)
    {
        $relativePath = FileSystemService::relativePath($path);

        if($relativePath == '/') {
            $itemName = self::ROOT_FOLDER_KEY;
        } else {
            $itemName = basename($path);
        }

        return $itemName;
    }

    /**
     * Update file permissions
     *
     * @param UpdatePermissionsRequest $request
     * @return FolderItem
     * @throws \Exception
     * @internal param $param
     */
    public function updateFilePermissions(UpdatePermissionsRequest $request)
    {
        $permission = $request->getPermissions();
        $path = $this->container->get('upload')->getFullPath($request->getPath());

        $newPermission = $this->convertToFileSystemPermission($permission, $this->container->get('file_system')->directoryExists($path));

        if($this->container->get('file_system')->setPermissions($path, $newPermission)) {
            return new FolderItem($path);
        }

        throw new \Exception(__('Permission update failed'));
    }

    /**
     * Convert to file system permission
     *
     * @param $permissions
     * @return string
     */
    private function convertToFileSystemPermission($permissions, $isDir = false)
    {
        $permission = '0700';

        if($permissions['group']) {
            $permission[2] = self::getPermissionCodeForString($permissions['group'], $isDir);
        }

        if($permissions['world']) {
            $permission[3] = self::getPermissionCodeForString($permissions['world'], $isDir);
        }

        return $permission;
    }
}