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

use IntellexApps\PHP\UploadFile\Domain\Exception\FileSystem\DirectoryAlreadyExistsException;
use IntellexApps\PHP\UploadFile\Domain\Exception\FileSystem\DirectoryDoesntExistException;
use IntellexApps\PHP\UploadFile\Domain\Exception\FileSystem\DirectoryNotWritableException;
use IntellexApps\PHP\UploadFile\Domain\Exception\FileSystem\FileAlreadyExistsException;
use IntellexApps\PHP\UploadFile\Domain\Exception\FileSystem\FileNotFoundException;
use IntellexApps\PHP\UploadFile\Domain\Exception\FileSystem\InvalidPathException;
use IntellexApps\PHP\UploadFile\Domain\Exception\UploadFileException;
use IntellexApps\PHP\UploadFile\Util\Configuration;
use IntellexApps\PHP\UploadFile\Util\Inflector;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;

class FileSystemService extends BaseService
{

    /**
     * Get folder items
     *
     * @param $path
     * @return array
     * @throws InvalidPathException
     */
    public function getFolderItems($path, $foldersOnly = false)
    {
        // Check if directory
        if (!is_dir($path)) {
            throw new InvalidPathException($path);
        }

        $path = Inflector::followingSeparator($path);

        $items = glob($path . '*');


        if (!$foldersOnly) {
            return $this->foldersFirstOrder($items);
        }

        return $this->filterFolders($items);
    }

    /**
     * Get subfolders
     *
     * @param $path
     * @return array
     */
    public function getSubFolders($path)
    {
        return $this->getFolderItems($path, true);
    }

    /**
     * Store base64encoded file
     *
     * @param $fullPath
     * @param $getBase64
     * @param bool $overwrite
     * @return mixed
     * @throws UploadFileException
     */
    public function storeBase64File($fullPath, $getBase64, $overwrite = false)
    {

        // Get path info
        $pathInfo = pathinfo($fullPath);

        if ($this->directoryWritable($pathInfo['dirname'])) {
            return $this->writeFile($fullPath, base64_decode($getBase64), $overwrite);
        }

        throw new UploadFileException();
    }

    /**
     * Store uploaded file
     *
     * @param $path
     * @param $file
     * @return string
     * @throws UploadFileException
     */
    public function storeUploadedFile($path, $file)
    {
        if ($this->directoryWritable($path)) {
            $newPath = Inflector::joinPaths([
                $path,
                $file['name']
            ]);

            $newPath = $this->uniqueName($newPath);

            if (move_uploaded_file($file['tmp_name'], $newPath)) {
                return $newPath;
            }
        }

        throw new UploadFileException();
    }

    /**
     * @param $source
     * @param $destination
     * @return mixed
     * @throws InvalidPathException
     */
    public function copyItem($source, $destination)
    {
        if (!$this->directoryExists($source) && !$this->fileExists($source)) {
            throw new InvalidPathException($source);
        }

        $this->directoryWritable($destination);

        $pathInfo = $this->getPathInfo($source);
        $fullPath = Inflector::joinPaths([
            $destination,
            $pathInfo['basename']
        ]);


        if ($this->fileExists($source)) {
            return $this->copyFile($source, $fullPath);
        }

        if ($this->directoryExists($source)) {
            return $this->copyDirectory($source, $fullPath);
        }

        throw new InvalidPathException($source);
    }

    /**
     * @param $source
     * @param $destination
     * @return mixed
     * @throws InvalidPathException
     */
    public function moveItem($source, $destination)
    {
        if ($this->fileExists($source)) {
            return $this->moveFile($source, $destination);
        }

        if ($this->directoryExists($source)) {
            return $this->moveDirectory($source, $destination);
        }

        throw new InvalidPathException($source);
    }

    /**
     * Move file
     *
     * @param $source
     * @param $destination
     * @return bool
     * @throws FileAlreadyExistsException
     */
    public function moveFile($source, $destination)
    {
        if ($this->fileExists($destination)) {
            throw new FileAlreadyExistsException($destination);
        }

        $this->directoryWritable(dirname($destination));

        if ($this->move($source, $destination)) {
            return $destination;
        }

        return false;
    }

    /**
     * Move directory
     *
     * @param $source
     * @param $destination
     * @return bool
     * @throws DirectoryAlreadyExistsException
     */
    public function moveDirectory($source, $destination)
    {
        if ($this->directoryExists($destination)) {
            throw new DirectoryAlreadyExistsException($destination);
        }

        $this->directoryWritable(dirname($destination));

        if ($this->move($source, $destination)) {
            return $destination;
        }

        return false;
    }

    /**
     * Create directory on path
     *
     * @param $path
     * @return mixed
     * @throws DirectoryAlreadyExistsException
     * @throws UploadFileException
     */
    public function createDirectory($path)
    {
        if ($this->directoryExists($path)) {
            throw new DirectoryAlreadyExistsException($path);
        }

        if ($this->directoryWritable(dirname($path))) {
            if (mkdir($path)) {
                return $path;
            } else {
                throw new UploadFileException();
            }
        }

        throw new UploadFileException();
    }

    /**
     * Delete item
     *
     * @param $path
     * @return mixed
     * @throws InvalidPathException
     */
    public function deleteItem($path)
    {

        // Check if file
        if ($this->fileExists($path)) {
            return $this->deleteFile($path);
        }

        // Check if directory
        if ($this->directoryExists($path)) {
            return $this->deleteDirectory($path);
        }

        throw new InvalidPathException($path);
    }

    /**
     * Copy file
     *
     * @param $fromPath
     * @param $toPath
     * @return bool
     */
    public function copyFile($fromPath, $toPath)
    {
        $toPath = $this->uniqueName($toPath);

        return copy($fromPath, $toPath);
    }

    /**
     * Copy directory
     *
     * @param $fromPath
     * @param $toPath
     * @throws UploadFileException
     */
    public function copyDirectory($fromPath, $toPath)
    {
        $toPath = $this->uniqueName($toPath);

        if(strpos($toPath, $fromPath) === 0) {
            throw new UploadFileException(__('You can\'t copy folder inside existing child folder'));
        }

        $dir = opendir($fromPath);
        @mkdir($toPath);
        while(false !== ($file = readdir($dir)) ) {
            if (($file === '.') || ($file === '..')) {
                continue;
            }

            $sourcePath = Inflector::joinPaths([$fromPath, $file]);
            $destinationPath = Inflector::joinPaths([$toPath, $file]);

            if (is_dir($sourcePath)) {
                $this->copyDirectory($sourcePath, $destinationPath);
            }
            else {
                copy($sourcePath, $destinationPath);
            }
        }
        closedir($dir);
    }

    /**
     * Delete file
     *
     * @param $path
     */
    public function deleteFile($path)
    {
        unlink($path);
    }

    /**
     * Delete directory
     *
     * @param $path
     * @return bool
     */
    public function deleteDirectory($path)
    {
        $files = new RecursiveIteratorIterator(
            new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::SKIP_DOTS),
            RecursiveIteratorIterator::CHILD_FIRST
        );


        foreach ($files as $fileinfo) {
            $todo = ($fileinfo->isDir() ? 'rmdir' : 'unlink');
            $todo($fileinfo->getRealPath());
        }

        rmdir($path);

        return true;
    }

    /**
     * Rename item
     *
     * @param $source
     * @param $destination
     * @return bool
     * @throws InvalidPathException
     */
    public function renameItem($source, $destination)
    {
        if (dirname($source) != dirname($destination)) {
            throw new InvalidPathException($destination);
        }

        if ($this->fileExists($source)) {
            return $this->renameFile($source, $destination);
        }

        if ($this->directoryExists($source)) {
            return $this->renameDirectory($source, $destination);
        }

        throw new InvalidPathException($source);
    }

    /**
     * Rename file
     *
     * @param $source
     * @param $destination
     * @return bool
     */
    public function renameFile($source, $destination)
    {
        if ($this->moveFile($source, $destination)) {
            return $destination;
        }

        return false;
    }

    /**
     * Rename directory
     *
     * @param $source
     * @param $destination
     * @return bool
     */
    public function renameDirectory($source, $destination)
    {
        if ($this->moveFile($source, $destination)) {
            return $destination;
        }
        return false;
    }

    /**
     * Get relative path
     *
     * @param $path
     * @return mixed
     */
    public static function relativePath($path)
    {
        if (is_dir($path)) {
            $path = Inflector::followingSeparator($path);
        }

        if (strpos(Inflector::followingSeparator($path), Configuration::read('working_dir')) === 0) {
            return Inflector::leadingSeparator(substr($path, strlen(Configuration::read('working_dir'))));
        }

        return Inflector::leadingSeparator($path);
    }

    /**
     *
     * @param $path
     * @param $permissions
     * @return bool
     */
    public function setPermissions($path, $permissions)
    {
        $oldMask = umask(0);
        $result = chmod($path, octdec($permissions));
        umask($oldMask);

        return $result;
    }

    /**
     * Get permissions for path
     *
     * @param $path
     * @return string
     */
    public static function getPermissions($path)
    {
        $oldMask = umask(0);
        $result = substr(sprintf('%o', fileperms($path)), -4);
        umask($oldMask);
        return $result;
    }

    /**
     * Check if directory is writable
     *
     * @param $path
     * @return bool
     * @throws DirectoryDoesntExistException
     * @throws DirectoryNotWritableException
     */
    public function directoryWritable($path)
    {
        if(!is_dir($path)) {
            throw new DirectoryDoesntExistException($path);
        }

        if(!is_writable($path)) {
            throw new DirectoryNotWritableException($path);
        }

        return true;
    }

    /**
     * Check if item exists
     *
     * @param $path
     * @return bool
     */
    public function itemExist($path)
    {
        return $this->fileExists($path) || $this->directoryExists($path);
    }

    /**
     * Check if file exists
     *
     * @param $source
     * @return bool
     */
    public function fileExists($source)
    {
        return file_exists($source) && !is_dir($source);
    }

    /**
     * Check if directory exists
     *
     * @param $directory
     * @return bool
     */
    public function directoryExists($directory)
    {
        return is_dir($directory);
    }

    /**
     * Get path info
     *
     * @param $path
     * @return mixed
     */
    public function getPathInfo($path)
    {
        return pathinfo($path);
    }

    /**
     * Write file
     *
     * @param $destination
     * @param $contents
     * @param bool $overwrite
     * @return mixed
     * @throws FileAlreadyExistsException
     */
    public function writeFile($destination, $contents, $overwrite = false)
    {

        if(!$overwrite && $this->fileExists($destination)) {
            throw new FileAlreadyExistsException($destination);
        }

        file_put_contents($destination, $contents);

        return $destination;
    }

    /**
     * Read file contents
     *
     * @param $destination
     * @return string
     * @throws FileNotFoundException
     */
    public function readFile($destination)
    {
        if(!$this->fileExists($destination)) {
            throw new FileNotFoundException($destination);
        }

        return file_get_contents($destination);
    }

    /**
     * Move file
     *
     * @param $source
     * @param $destination
     * @return bool
     */
    public function move($source, $destination)
    {
        return rename($source, $destination);
    }

    /**
     * Filter folders in path
     *
     * @param $paths
     * @return array
     */
    private function filterFolders($paths)
    {
        $folders = [];
        foreach($paths as $item) {
            if(is_dir($item)) {
                $folders[] = $item;
            }
        }

        return $folders;
    }

    /**
     * Folders first
     *
     * @param $paths
     * @return array
     */
    private function foldersFirstOrder($paths) {
        $files = [];
        $dirs = [];

        foreach($paths as $path) {
            if(is_dir($path)) {
                $dirs[] = $path;
            } else{
                $files[] = $path;
            }
        }

        return array_merge($dirs, $files);
    }

    /**
     * Return unique name in path
     *
     * @param $newPath
     * @return string
     */
    public function uniqueName($newPath)
    {
        if(!file_exists($newPath)) {
            return $newPath;
        }

        $pathInfo = pathinfo($newPath);

        $dirname = $pathInfo['dirname'];
        $filename = $pathInfo['filename'];
        $extension = $pathInfo['extension'];
        $basename = $pathInfo['basename'];

        $counter = 0;

        while(file_exists($dirname . DS . $basename)) {
            $counter++;
            $basename = $filename . '(' . $counter . ')' . ($extension ? ('.' . $extension) : '');
        }

        return Inflector::joinPaths([
            $dirname,
            $basename
        ]);
    }

    /**
     * Download file
     *
     * @param $fullPath
     * @throws FileNotFoundException
     */
    public function downloadFile($fullPath)
    {
        if(!$this->fileExists($fullPath)) {
            throw new FileNotFoundException($fullPath);
        }

        header("Content-Description: File Transfer");
        header("Content-Type: application/octet-stream");
        header("Content-Disposition: attachment; filename='" . basename($fullPath) . "'");

        readfile($fullPath);
        exit();
    }

}