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

use IntellexApps\PHP\UploadFile\Domain\Exception\FileSystem\InvalidPathException;
use IntellexApps\PHP\UploadFile\Domain\Exception\NotImplementedException;
use IntellexApps\PHP\UploadFile\Domain\Exception\UploadFileException;
use IntellexApps\PHP\UploadFile\Domain\Model\FolderItem;
use IntellexApps\PHP\UploadFile\Domain\Request\File\CopyRequest;
use IntellexApps\PHP\UploadFile\Domain\Request\File\CreateRequest;
use IntellexApps\PHP\UploadFile\Domain\Request\File\DeleteRequest;
use IntellexApps\PHP\UploadFile\Domain\Request\File\EditRequest;
use IntellexApps\PHP\UploadFile\Domain\Request\File\InitRequest;
use IntellexApps\PHP\UploadFile\Domain\Request\File\ListFilesRequest;
use IntellexApps\PHP\UploadFile\Domain\Request\File\MoveRequest;
use IntellexApps\PHP\UploadFile\Domain\Request\File\MultipartUploadRequest;
use IntellexApps\PHP\UploadFile\Domain\Request\File\RenameRequest;
use IntellexApps\PHP\UploadFile\Domain\Request\File\UploadRequest;
use IntellexApps\PHP\UploadFile\Domain\Request\File\ViewItemRequest;
use IntellexApps\PHP\UploadFile\Domain\Response\DeleteResponse;
use IntellexApps\PHP\UploadFile\Dossier;
use IntellexApps\PHP\UploadFile\Util\Configuration;
use IntellexApps\PHP\UploadFile\Util\Inflector;

/** Main upload file logic resides here */
class UploadFileService extends BaseService
{

    /** @var FileSystemService */
    var $fileSystemService;

    /** @var ConfigurationService */
    var $configurationService;

    /**
     * Initialize service
     */
    public function init()
    {
        $this->fileSystemService = $this->get('file_system');
        $this->configurationService = $this->get('configuration');
    }

    /**
     * Initialization function
     *
     * @throws NotImplementedException
     */
    public function configure()
    {
        $this->configurationService->initializeConfiguration();
    }

    /**
     * List files
     *
     * @param ListFilesRequest $request
     * @return array
     */
    public function listFiles(ListFilesRequest $request)
    {
        $this->get('hook')->triggerHook(HookService::HOOK_BEFORE_LIST, $request);

        $fullPath = $this->getFullPath($request->getPath());

        // If readable
        return $this->get('permission')->ifReadable($fullPath, function() use($fullPath) {
            $files = $this->fileSystemService->getFolderItems($fullPath);

            $response = [];

            foreach($files as $file) {
                if($this->get('permission')->canRead($file)) {
                    $response[] = new FolderItem($file);
                }
            }

            $this->get('hook')->triggerHook(HookService::HOOK_AFTER_LIST, $response);

            return $response;
        });
    }

    /**
     * Upload file
     *
     * @param UploadRequest $request
     * @param bool $overwrite
     * @return FolderItem
     */
    public function upload(UploadRequest $request, $overwrite = false)
    {
        $fullPath = $this->getFullPath($request->getPath());

        // If writable
        return $this->get('permission')->ifWritable(dirname($fullPath), function() use ($fullPath, $request, $overwrite) {
            $location = $this->fileSystemService->storeBase64File(
                $fullPath,
                $request->getBase64(),
                $overwrite
            );

            return new FolderItem($location);
        });
    }

    /**
     * Multipart upload
     *
     * TODO: Make it better
     *
     * @param MultipartUploadRequest $request
     * @return array
     */
    public function multipartUpload(MultipartUploadRequest $request)
    {

        $this->get('hook')->triggerHook(HookService::HOOK_BEFORE_UPLOAD, $request);

        $status = [];
        $path = $this->getFullPath($request->getPath());
        $newPaths = [];
        foreach($request->getFiles() as $index => $file) {
            try {
                $newPaths[] = $this->fileSystemService->storeUploadedFile($path, $file);
                $status[$index] = true;
            } catch(\Exception $e) {
                $status[$index] = false;
            }
        }
        $this->get('hook')->triggerHook(HookService::HOOK_AFTER_UPLOAD, $newPaths, $status);

        return $status;
    }

    /**
     * Move file
     *
     * @param MoveRequest $request
     * @return FolderItem
     * @throws \Exception
     */
    public function move(MoveRequest $request)
    {
        $this->get('hook')->triggerHook(HookService::HOOK_BEFORE_MOVE, $request);

        $sourceFullPath = $this->getFullPath($request->getSource());
        $destinationFullPath = $this->fillDestination($sourceFullPath, $this->getFullPath($request->getDestination()));

        // If source is readable
        return $this->get('permission')->ifReadable(
            $sourceFullPath,
            function() use($sourceFullPath, $destinationFullPath) {

                // If destination is writable
                return $this->get('permission')->ifWritable(
                    dirname($destinationFullPath),
                    function() use($sourceFullPath, $destinationFullPath) {

                        // Move item
                        $location = $this->fileSystemService->moveItem(
                            $sourceFullPath,
                            $destinationFullPath
                        );

                        if($location) {
                            $this->get('permission')->itemMoved($sourceFullPath, $location);
                        }

                        $folderItem = new FolderItem($location);

                        $this->get('hook')->triggerHook(HookService::HOOK_AFTER_MOVE, $folderItem);

                        return $folderItem;
                });

        });
    }

    /**
     * Copy file
     *
     * @param CopyRequest $request
     * @return FolderItem
     * @throws NotImplementedException
     */
    public function copy(CopyRequest $request)
    {
        $this->get('hook')->triggerHook(HookService::HOOK_BEFORE_COPY, $request);

        $sourceFullPath = $this->getFullPath($request->getSource());
        $destinationFullPath = $this->getFullPath($request->getDestination());

        return $this->get('permission')->ifReadable(
            $sourceFullPath,
            function() use($sourceFullPath, $destinationFullPath) {

                return $this->get('permission')->ifWritable(
                    dirname($destinationFullPath),
                    function() use($sourceFullPath, $destinationFullPath) {
                        $location = $this->fileSystemService->copyItem(
                            $sourceFullPath,
                            $destinationFullPath
                        );

                        if($location) {
                            $this->get('permission')->itemCopied($sourceFullPath, $location);
                        }

                        $folderItem = new FolderItem($location);

                        $this->get('hook')->triggerHook(HookService::HOOK_AFTER_COPY, $folderItem);

                    });

            });
    }

    /**
     * Create file
     *
     * @param CreateRequest $request
     * @return FolderItem
     * @throws NotImplementedException
     */
    public function create(CreateRequest $request)
    {
        $this->get('hook')->triggerHook(HookService::HOOK_BEFORE_CREATE, $request);

        $fullPath = $this->getFullPath($request->getPath());

        return $this->get('permission')->ifWritable(dirname($fullPath), function() use ($fullPath) {
            $folderItem = new FolderItem($this->fileSystemService->createDirectory($fullPath));

            $this->get('hook')->triggerHook(HookService::HOOK_AFTER_CREATE, $folderItem);

            return $folderItem;
        });
    }

    /**
     * Delete item
     *
     * @param DeleteRequest $request
     * @return mixed
     */
    public function delete(DeleteRequest $request)
    {
        $this->get('hook')->triggerHook(HookService::HOOK_BEFORE_DELETE, $request);

        $fullPath = $this->getFullPath($request->getPath());
        $destinationFullPath = $this->fillDestination($fullPath, $this->getTrashLocation());

        return $this->get('permission')->ifWritable($fullPath, function() use ($fullPath, $destinationFullPath) {

            if($this->softDelete()) {
                $destinationFullPath = $this->fileSystemService->uniqueName($destinationFullPath);
                $deleted = !!$this->fileSystemService->moveItem($fullPath, $destinationFullPath);
            } else {
                $deleted = $this->fileSystemService->deleteItem($fullPath);

                if($deleted) {
                    $this->get('permission')->itemDeleted($fullPath);
                }
            }

            $this->get('hook')->triggerHook(HookService::HOOK_AFTER_DELETE, $deleted);

            return new DeleteResponse($deleted);
        });

    }

    /**
     * Edit file
     *
     * @param EditRequest $request
     * @throws NotImplementedException
     */
    public function edit(EditRequest $request)
    {
        throw new NotImplementedException();
    }

    /**
     * Rename file
     *
     * @param RenameRequest $request
     * @return FolderItem
     * @throws UploadFileException
     */
    public function rename(RenameRequest $request)
    {
        $this->get('hook')->triggerHook(HookService::HOOK_BEFORE_RENAME, $request);

        $sourceFullPath = $this->getFullPath($request->getSource());

        $destination = $request->getDestination();

        if($destination[0] === '/') {
            $destinationFullPath = $this->getFullPath($request->getDestination());
        } else {
            $destinationFullPath = Inflector::joinPaths([
                dirname($sourceFullPath),
                $destination
            ]);
        }

        return $this->get('permission')->ifWritable($sourceFullPath, function() use ($sourceFullPath, $destinationFullPath) {

            $location = $this->fileSystemService->renameItem(
                $sourceFullPath,
                $destinationFullPath
            );

            if(!$location) {
                throw new UploadFileException();
            }

            $this->get('permission')->itemRenamed($sourceFullPath, $location);

            $folderItem = new FolderItem($location);

            $this->get('hook')->triggerHook(HookService::HOOK_AFTER_RENAME, $folderItem);

            return $folderItem;
        });

    }

    /**
     * View item
     *
     * @param ViewItemRequest $request
     * @return FolderItem
     * @throws InvalidPathException
     */
    public function view(ViewItemRequest $request)
    {
        $this->get('hook')->triggerHook(HookService::HOOK_BEFORE_VIEW, $request);

        $path = $this->getFullPath($request->getPath());
        return $this->get('permission')->ifReadable($path, function() use ($path, $request) {

            if($this->fileSystemService->itemExist($path)) {

                $folderItem = new FolderItem($path);

                $this->get('hook')->triggerHook(HookService::HOOK_AFTER_VIEW, $folderItem);

                return $folderItem;
            }

            throw new InvalidPathException($request->getPath());
        });
    }

    /**
     * Get working dir
     *
     * @return mixed|null
     */
    private function getWorkingDirectory()
    {
        return Configuration::read('working_dir');
    }

    /**
     * Get full from relative path
     *
     * @param $path
     * @return string
     */
    public function getFullPath($path)
    {
        return Inflector::joinPaths([
            $this->getWorkingDirectory(),
            $path
        ]);
    }

    /**
     * Get trash location
     *
     * @return string
     */
    public function getTrashLocation()
    {
        return $this->getFullPath('.Trash');
    }

    /**
     * Fill destination
     *
     * @param $fullPath
     * @param $destinationFullPath
     * @return string
     */
    private function fillDestination($fullPath, $destinationFullPath)
    {
        // If destination is just location to directory - append name to it
        if($this->fileSystemService->directoryExists($destinationFullPath)) {
            $destinationFullPath = Inflector::joinPaths([
                $destinationFullPath,
                pathinfo($fullPath, PATHINFO_BASENAME)
            ]);
        }

        return $destinationFullPath;
    }

    /**
     * Is soft delete enabled
     *
     * @return bool
     */
    private function softDelete()
    {
        return true;
    }

    /**
     * Output file
     *
     * @param $path
     */
    public function outputFile($path)
    {
        $fullPath = $this->getFullPath($path);
        return $this->fileSystemService->downloadFile($fullPath);
    }

    /**
     * Check if path has children
     *
     * @param $path
     * @return bool
     */
    public function hasChildren($path)
    {
        $fullPath = $this->getFullPath(Inflector::leadingSeparator($path));
        return count($this->fileSystemService->getSubFolders($fullPath)) > 0;
    }
}