<?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\oatbox\filesystem;

use oat\oatbox\service\ConfigurableService;
use League\Flysystem\AdapterInterface;
use common_exception_Error;
use Zend\ServiceManager\ServiceLocatorAwareInterface;
use \League\Flysystem\Filesystem as FlyFileSystem;
use League\Flysystem\FilesystemInterface;

 /**
 * A service to reference and retrieve filesystems
 */
class FileSystemService extends ConfigurableService
{
    const SERVICE_ID = 'generis/filesystem';

    const OPTION_FILE_PATH = 'filesPath';

    const OPTION_ADAPTERS = 'adapters';

    const OPTION_DIRECTORIES = 'dirs';

    const FLYSYSTEM_ADAPTER_NS = '\\League\\Flysystem\\Adapter\\';

    const FLYSYSTEM_LOCAL_ADAPTER = 'Local';

    private $filesystems = [];

    /**
     *
     * @param $id
     * @return \oat\oatbox\filesystem\Directory
     */
    public function getDirectory($id)
    {
        return $this->propagate(new Directory($id, ''));
    }

    /**
     * Returns the directory config
     * @return array
     */
    protected function getDirectories()
    {
        return $this->hasOption(self::OPTION_DIRECTORIES)
            ? $this->getOption(self::OPTION_DIRECTORIES)
            : [];
    }

    /**
     * Add a directory reference
     * @param string $id
     * @param string $adapterId
     */
    protected function addDir($id, $adapterId)
    {
        $dirs = $this->getDirectories();
        $dirs[$id] = $adapterId;
        $this->setOption(self::OPTION_DIRECTORIES, $dirs);
    }
    
    /**
     * Returns whenever or not a FS exists
     * @param string $id
     * @return boolean
     */
    public function hasDirectory($id)
    {
        $adapterConfig = $this->getOption(self::OPTION_ADAPTERS);
        $dirConfig = $this->getOption(self::OPTION_DIRECTORIES);
        return isset($adapterConfig[$id]) || isset($dirConfig[$id]);
    }

    /**
     * Get FileSystem by ID
     *
     * Retrieve an existing FileSystem by ID.
     *
     * @param string $id
     * @return FilesystemInterface
     * @throws \common_exception_Error
     * @throws \common_exception_NotFound
     */
    public function getFileSystem($id)
    {
        if (!isset($this->filesystems[$id])) {
            $config = $this->getAdapterConfig($id);
            $adapter = $this->getFlysystemAdapter($config['adapter']);
            $this->filesystems[$id] = new FileSystem($id, new FlyFileSystem($adapter), $config['path']);
        }
        return $this->filesystems[$id];
    }
    
    /**
     * Creates a filesystem using the default implementation (Local)
     * Override this function to create your files elsewhere by default
     *
     * @param string $id
     * @param string $subPath
     * @return FilesystemInterface
     */
    public function createFileSystem($id, $subPath = null)
    {
        $this->addDir($id, 'default');
        return $this->getFileSystem($id);
    }

    /**
     * Create a new local file system
     *
     * @deprecated never rely on a directory being local, use addDir instead
     * @param string $id
     * @return FilesystemInterface
     */
    public function createLocalFileSystem($id)
    {
        $path = $this->getOption(self::OPTION_FILE_PATH) . \helpers_File::sanitizeInjectively($id);
        $this->registerLocalFileSystem($id, $path);
        return $this->getFileSystem($id);
    }
    
    /**
     * Registers a local file system, used for transition
     *
     * @deprecated never rely on a directory being local, use addDir instead
     * @param string $id
     * @param string $path
     * @return boolean
     */
    public function registerLocalFileSystem($id, $path)
    {
        $adapters = $this->hasOption(self::OPTION_ADAPTERS) ? $this->getOption(self::OPTION_ADAPTERS) : [];
        $adapters[$id] = [
            'class' => self::FLYSYSTEM_LOCAL_ADAPTER,
            'options' => ['root' => $path]
        ];
        $this->setOption(self::OPTION_ADAPTERS, $adapters);
        return true;
    }

    /**
     * Remove a filesystem adapter
     *
     * @param string $id
     * @return boolean
     */
    public function unregisterFileSystem($id)
    {
        if (isset($this->filesystems[$id])) {
            unset($this->filesystems[$id]);
        }
        $adapters = $this->getOption(self::OPTION_ADAPTERS);
        if (isset($adapters[$id])) {
            unset($adapters[$id]);
            $this->setOption(self::OPTION_ADAPTERS, $adapters);
            return true;
        } elseif ($this->hasDirectory($id)) {
            $directories = $this->getOption(self::OPTION_DIRECTORIES);
            unset($directories[$id]);
            $this->setOption(self::OPTION_DIRECTORIES, $directories);
            return true;
        } else {
            return false;
        }
    }

    /**
     * Get file adapter by file
     *
     * @param File $file
     * @return AdapterInterface
     * @throws \common_exception_NotFound
     * @throws common_exception_Error
     */
    public function getFileAdapterByFile(File $file)
    {
        $config = $this->getAdapterConfig($file->getFileSystemId());
        return $this->getFlysystemAdapter($config['adapter']);
    }

    /**
     * Returns the configuration for an adapter
     * @param string $id
     * @return string[]
     */
    protected function getAdapterConfig($id)
    {
        $dirs = $this->getDirectories();
        if (!isset($dirs[$id])) {
            $config = [
                'adapter' => $id,
                'path' => ''
            ];
        } elseif (is_array($dirs[$id])) {
            $config = $dirs[$id];
        } else {
            $config = [
                'adapter' => $dirs[$id],
                'path' => $id
            ];
        }
        return $config;
    }

    /**
     * inspired by burzum/storage-factory
     *
     * @param string $id
     * @throws \common_exception_NotFound if adapter doesn't exist
     * @throws \common_exception_Error if adapter is not valid
     * @return AdapterInterface
     */
    protected function getFlysystemAdapter($id)
    {
        $fsConfig = $this->getOption(self::OPTION_ADAPTERS);
        if (!isset($fsConfig[$id])) {
            throw new \common_exception_NotFound('Undefined filesystem "' . $id . '"');
        }
        $adapterConfig = $fsConfig[$id];
        // alias?
        while (is_string($adapterConfig)) {
            $adapterConfig = $fsConfig[$adapterConfig];
        }
        $class = $adapterConfig['class'];
        $options = isset($adapterConfig['options']) ? $adapterConfig['options'] : [];

        if (!class_exists($class)) {
            if (class_exists(self::FLYSYSTEM_ADAPTER_NS . $class)) {
                $class = self::FLYSYSTEM_ADAPTER_NS . $class;
            } elseif (class_exists(self::FLYSYSTEM_ADAPTER_NS . $class . '\\' . $class . 'Adapter')) {
                $class = self::FLYSYSTEM_ADAPTER_NS . $class . '\\' . $class . 'Adapter';
            } else {
                throw new common_exception_Error('Unknown Flysystem adapter "' . $class . '"');
            }
        }

        if (!is_subclass_of($class, 'League\Flysystem\AdapterInterface')) {
            throw new common_exception_Error('"' . $class . '" is not a flysystem adapter');
        }
        $adapter = (new \ReflectionClass($class))->newInstanceArgs($options);
        if ($adapter instanceof ServiceLocatorAwareInterface) {
            $adapter->setServiceLocator($this->getServiceLocator());
        }
        return $adapter;
    }
}