<?php
namespace AngularFilemanager\LocalBridge;

use AngularFilemanager\LocalBridge\Translate;

/**
 * File Manager API Class
 *
 * Made for PHP Local filesystem bridge for angular-filemanager to handle file manipulations
 * @author Jakub Ďuraš <jakub@duras.me>
 */
class FileManagerApi
{
    private $basePath = null;

    private $translate;

    public function __construct($basePath = null, $lang = 'en', $muteErrors = true)
    {
        if ($muteErrors) {
            ini_set('display_errors', 0);
        }

        $this->basePath = $basePath ?: dirname(__DIR__);
        $this->translate = new Translate($lang);
    }

    public function postHandler($query, $request, $files)
    {
        $t = $this->translate;
        
        // Probably file upload
        if (!isset($request['action'])
            && (isset($_SERVER["CONTENT_TYPE"])
            && strpos($_SERVER["CONTENT_TYPE"], 'multipart/form-data') !== false)
        ) {
            $uploaded = $this->uploadAction($request['destination'], $files);
            if ($uploaded === true) {
                $response = $this->simpleSuccessResponse();
            } else {
                $response = $this->simpleErrorResponse($t->upload_failed);
            }

            return $response;
        }

        switch ($request['action']) {
            case 'list':
                $list = $this->listAction($request['path']);

                if (!is_array($list)) {
                    $response = $this->simpleErrorResponse($t->listing_failed);
                } else {
                    $response = new Response();
                    $response->setData([
                        'result' => $list
                    ]);
                }
                break;

            case 'rename':
                $renamed = $this->renameAction($request['item'], $request['newItemPath']);
                if ($renamed === true) {
                    $response = $this->simpleSuccessResponse();
                } elseif ($renamed === 'notfound') {
                    $response = $this->simpleErrorResponse($t->file_not_found);
                } else {
                    $response = $this->simpleErrorResponse($t->renaming_failed);
                }
                break;

            case 'move':
                $moved = $this->moveAction($request['items'], $request['newPath']);
                if ($moved === true) {
                    $response = $this->simpleSuccessResponse();
                } else {
                    $response = $this->simpleErrorResponse($t->moving_failed);
                }
                break;

            case 'copy':
                $copied = $this->copyAction($request['items'], $request['newPath']);
                if ($copied === true) {
                    $response = $this->simpleSuccessResponse();
                } else {
                    $response = $this->simpleErrorResponse($t->copying_failed);
                }
                break;

            case 'remove':
                $removed = $this->removeAction($request['items']);
                if ($removed === true) {
                    $response = $this->simpleSuccessResponse();
                } elseif ($removed === 'notempty') {
                    $response = $this->simpleErrorResponse($t->removing_failed_directory_not_empty);
                } else {
                    $response = $this->simpleErrorResponse($t->removing_failed);
                }
                break;

            case 'edit':
                $edited = $this->editAction($request['item'], $request['content']);
                if ($edited !== false) {
                    $response = $this->simpleSuccessResponse();
                } else {
                    $response = $this->simpleErrorResponse($t->saving_failed);
                }
                break;

            case 'getContent':
                $content = $this->getContentAction($request['item']);
                if ($content !== false) {
                    $response = new Response();
                    $response->setData([
                        'result' => $content
                    ]);
                } else {
                    $response = $this->simpleErrorResponse($t->file_not_found);
                }
                break;

            case 'createFolder':
                $created = $this->createFolderAction($request['newPath']);
                if ($created === true) {
                    $response = $this->simpleSuccessResponse();
                } elseif ($created === 'exists') {
                    $response = $this->simpleErrorResponse($t->folder_already_exists);
                } else {
                    $response = $this->simpleErrorResponse($t->folder_creation_failed);
                }
                break;

            case 'changePermissions':
                $changed = $this->changePermissionsAction($request['items'], $request['perms'], $request['recursive']);
                if ($changed === true) {
                    $response = $this->simpleSuccessResponse();
                } elseif ($changed === 'missing') {
                    $response = $this->simpleErrorResponse($t->file_not_found);
                } else {
                    $response = $this->simpleErrorResponse($t->permissions_change_failed);
                }
                break;

            case 'compress':
                $compressed = $this->compressAction(
                    $request['items'],
                    $request['destination'],
                    $request['compressedFilename']
                );
                if ($compressed === true) {
                    $response = $this->simpleSuccessResponse();
                } else {
                    $response = $this->simpleErrorResponse($t->compression_failed);
                }
                break;

            case 'extract':
                $extracted = $this->extractAction($request['destination'], $request['item'], $request['folderName']);
                if ($extracted === true) {
                    $response = $this->simpleSuccessResponse();
                } elseif ($extracted === 'unsupported') {
                    $response = $this->simpleErrorResponse($t->archive_opening_failed);
                } else {
                    $response = $this->simpleErrorResponse($t->extraction_failed);
                }
                break;
            
            default:
                $response = $this->simpleErrorResponse($t->function_not_implemented);
                break;
        }

        return $response;
    }

    public function getHandler($queries)
    {
        $t = $this->translate;

        switch ($queries['action']) {
            case 'download':
                $downloaded = $this->downloadAction($queries['path']);
                if ($downloaded === true) {
                    exit;
                } else {
                    $response = $this->simpleErrorResponse($t->file_not_found);
                }
                
                break;

            case 'downloadMultiple':
                $downloaded = $this->downloadMultipleAction($queries['items'], $queries['toFilename']);
                if ($downloaded === true) {
                    exit;
                } else {
                    $response = $this->simpleErrorResponse($t->file_not_found);
                }

                break;

            default:
                $response = $this->simpleErrorResponse($t->function_not_implemented);
                break;
        }

        return $response;
    }

    private function downloadAction($path)
    {
        $file_name = basename($path);
        $path = $this->canonicalizePath($this->basePath . $path);

        if (!file_exists($path)) {
            return false;
        }

        $finfo = finfo_open(FILEINFO_MIME_TYPE);
        $mime_type = finfo_file($finfo, $path);
        finfo_close($finfo);

        if (ob_get_level()) {
            ob_end_clean();
        }

        header("Content-Disposition: attachment; filename=\"$file_name\"");
        header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
        header("Content-Type: $mime_type");
        header('Pragma: public');
        header('Content-Length: ' . filesize($path));
        readfile($path);

        return true;
    }

    private function downloadMultipleAction($items, $archiveName)
    {
        $archivePath = tempnam('../', 'archive');

        $zip = new \ZipArchive();
        if ($zip->open($archivePath, \ZipArchive::CREATE) !== true) {
            unlink($archivePath);
            return false;
        }

        foreach ($items as $path) {
            $zip->addFile($this->basePath . $path, basename($path));
        }

        $zip->close();

        header("Content-Disposition: attachment; filename=\"$archiveName\"");
        header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
        header("Content-Type: application/zip");
        header('Pragma: public');
        header('Content-Length: ' . filesize($archivePath));
        readfile($archivePath);

        unlink($archivePath);

        return true;
    }

    private function uploadAction($path, $files)
    {
        $path = $this->canonicalizePath($this->basePath . $path);

        foreach ($_FILES as $file) {
            $fileInfo = pathinfo($file['name']);
            $fileName = $this->normalizeName($fileInfo['filename']) . '.' . $fileInfo['extension'];

            $uploaded = move_uploaded_file(
                $file['tmp_name'],
                $path . DIRECTORY_SEPARATOR . $fileName
            );
            if ($uploaded === false) {
                return false;
            }
        }

        return true;
    }

    private function listAction($path)
    {
        $files = array_values(array_filter(
            scandir($this->basePath . $path),
            function ($path) {
                return !($path === '.' || $path === '..');
            }
        ));

        $files = array_map(function ($file) use ($path) {
            $file = $this->canonicalizePath(
                $this->basePath . $path . DIRECTORY_SEPARATOR . $file
            );
            $date = new \DateTime('@' . filemtime($file));

            return [
                'name' => basename($file),
                'rights' => $this->parsePerms(fileperms($file)),
                'size' => filesize($file),
                'date' => $date->format('Y-m-d H:i:s'),
                'type' => is_dir($file) ? 'dir' : 'file'
            ];
        }, $files);

        return $files;
    }
    
    public function listActionData($path)
    {
        $files = array_values(array_filter(
            scandir($this->basePath . $path),
            function ($path) {
                return !($path === '.' || $path === '..');
            }
        ));

        $files = array_map(function ($file) use ($path) {
            $file = $this->canonicalizePath(
                $this->basePath . $path . DIRECTORY_SEPARATOR . $file
            );
            $date = new \DateTime('@' . filemtime($file));

            return [
                'name' => basename($file),
                'rights' => $this->parsePerms(fileperms($file)),
                'size' => filesize($file),
                'date' => $date->format('Y-m-d H:i:s'),
                'type' => is_dir($file) ? 'dir' : 'file'
            ];
        }, $files);

        return $files;
    }

    private function renameAction($oldPath, $newPath)
    {
        $oldPath = $this->basePath . $oldPath;
        $newPath = $this->basePath . $newPath;

        if (! file_exists($oldPath)) {
            return 'notfound';
        }

        return rename($oldPath, $newPath);
    }

    private function moveAction($oldPaths, $newPath)
    {
        $newPath = $this->basePath . $this->canonicalizePath($newPath) . DIRECTORY_SEPARATOR;

        foreach ($oldPaths as $oldPath) {
            if (!file_exists($this->basePath . $oldPath)) {
                return false;
            }

            $renamed = rename($this->basePath . $oldPath, $newPath . basename($oldPath));
            if ($renamed === false) {
                return false;
            }
        }

        return true;
    }

    private function copyAction($oldPaths, $newPath)
    {
        $newPath = $this->basePath . $this->canonicalizePath($newPath) . DIRECTORY_SEPARATOR;

        foreach ($oldPaths as $oldPath) {
            if (!file_exists($this->basePath . $oldPath)) {
                return false;
            }

            $copied = copy(
                $this->basePath . $oldPath,
                $newPath . basename($oldPath)
            );
            if ($copied === false) {
                return false;
            }
        }

        return true;
    }

    private function removeAction($paths)
    {
        foreach ($paths as $path) {
            $path = $this->canonicalizePath($this->basePath . $path);

            if (is_dir($path)) {
                $dirEmpty = (new \FilesystemIterator($path))->valid();

                if ($dirEmpty) {
                    return 'notempty';
                } else {
                    $removed = rmdir($path);
                }
            } else {
                $removed = unlink($path);
            }

            if ($removed === false) {
                return false;
            }
        }

        return true;
    }

    private function editAction($path, $content)
    {
        $path = $this->basePath . $path;
        return file_put_contents($path, $content);
    }

    private function getContentAction($path)
    {
        $path = $this->basePath . $path;

        if (! file_exists($path)) {
            return false;
        }

        return file_get_contents($path);
    }

    private function createFolderAction($path)
    {
        $path = $this->basePath . $path;

        if (file_exists($path) && is_dir($path)) {
            return 'exists';
        }

        return mkdir($path);
    }

    private function changePermissionsAction($paths, $permissions, $recursive)
    {
        foreach ($paths as $path) {
            if (!file_exists($this->basePath . $path)) {
                return 'missing';
            }

            if (is_dir($path) && $recursive === true) {
                $iterator = new RecursiveIteratorIterator(
                    new RecursiveDirectoryIterator($path),
                    RecursiveIteratorIterator::SELF_FIRST
                );

                foreach ($iterator as $item) {
                    $changed = chmod($this->basePath . $item, octdec($permissions));
                    
                    if ($changed === false) {
                        return false;
                    }
                }
            }

            return chmod($this->basePath . $path, octdec($permissions));
        }
    }

    private function compressAction($paths, $destination, $archiveName)
    {
        $archivePath = $this->basePath . $destination . $archiveName;

        $zip = new \ZipArchive();
        if ($zip->open($archivePath, \ZipArchive::CREATE) !== true) {
            return false;
        }

        foreach ($paths as $path) {
            $fullPath = $this->basePath . $path;

            if (is_dir($fullPath)) {
                $dirs = [
                    [
                        'dir' => basename($path),
                        'path' => $this->canonicalizePath($this->basePath . $path),
                    ]
                ];

                while (count($dirs)) {
                    $dir = current($dirs);
                    $zip->addEmptyDir($dir['dir']);

                    $dh = opendir($dir['path']);
                    while ($file = readdir($dh)) {
                        if ($file != '.' && $file != '..') {
                            $filePath = $dir['path'] . DIRECTORY_SEPARATOR . $file;
                            if (is_file($filePath)) {
                                $zip->addFile(
                                    $dir['path'] . DIRECTORY_SEPARATOR . $file,
                                    $dir['dir'] . '/' . basename($file)
                                );
                            } elseif (is_dir($filePath)) {
                                $dirs[] = [
                                    'dir' => $dir['dir'] . '/' . $file,
                                    'path' => $dir['path'] . DIRECTORY_SEPARATOR . $file
                                ];
                            }
                        }
                    }
                    closedir($dh);
                    array_shift($dirs);
                }
            } else {
                $zip->addFile($path, basename($path));
            }
        }

        return $zip->close();
    }

    private function extractAction($destination, $archivePath, $folderName)
    {
        $archivePath = $this->basePath . $archivePath;
        $folderPath = $this->basePath . $this->canonicalizePath($destination) . DIRECTORY_SEPARATOR . $folderName;

        $zip = new \ZipArchive;
        if ($zip->open($archivePath) === false) {
            return 'unsupported';
        }

        mkdir($folderPath);
        $zip->extractTo($folderPath);
        return $zip->close();
    }

    private function simpleSuccessResponse()
    {
        $response = new Response();
        $response->setData([
            'result' => [
                'success' => true
            ]
        ]);

        return $response;
    }

    private function simpleErrorResponse($message)
    {
        $response = new Response();
        $response
            ->setStatus(500, 'Internal Server Error')
            ->setData([
                'result' => [
                    'success' => false,
                    'error' => $message
                ]
            ]);

        return $response;
    }

    private function parsePerms($perms)
    {
        if (($perms & 0xC000) == 0xC000) {
            // Socket
            $info = 's';
        } elseif (($perms & 0xA000) == 0xA000) {
            // Symbolic Link
            $info = 'l';
        } elseif (($perms & 0x8000) == 0x8000) {
            // Regular
            $info = '-';
        } elseif (($perms & 0x6000) == 0x6000) {
            // Block special
            $info = 'b';
        } elseif (($perms & 0x4000) == 0x4000) {
            // Directory
            $info = 'd';
        } elseif (($perms & 0x2000) == 0x2000) {
            // Character special
            $info = 'c';
        } elseif (($perms & 0x1000) == 0x1000) {
            // FIFO pipe
            $info = 'p';
        } else {
            // Unknown
            $info = 'u';
        }

        // Owner
        $info .= (($perms & 0x0100) ? 'r' : '-');
        $info .= (($perms & 0x0080) ? 'w' : '-');
        $info .= (($perms & 0x0040) ?
                    (($perms & 0x0800) ? 's' : 'x' ) :
                    (($perms & 0x0800) ? 'S' : '-'));

        // Group
        $info .= (($perms & 0x0020) ? 'r' : '-');
        $info .= (($perms & 0x0010) ? 'w' : '-');
        $info .= (($perms & 0x0008) ?
                    (($perms & 0x0400) ? 's' : 'x' ) :
                    (($perms & 0x0400) ? 'S' : '-'));

        // World
        $info .= (($perms & 0x0004) ? 'r' : '-');
        $info .= (($perms & 0x0002) ? 'w' : '-');
        $info .= (($perms & 0x0001) ?
                    (($perms & 0x0200) ? 't' : 'x' ) :
                    (($perms & 0x0200) ? 'T' : '-'));

        return $info;
    }

    private function canonicalizePath($path)
    {
        $dirSep = DIRECTORY_SEPARATOR;
        $wrongDirSep = DIRECTORY_SEPARATOR === '/' ? '\\' : '/';

        // Replace incorrect dir separators
        $path = str_replace($wrongDirSep, $dirSep, $path);

        $path = explode($dirSep, $path);
        $stack = array();
        foreach ($path as $seg) {
            if ($seg == '..') {
                // Ignore this segment, remove last segment from stack
                array_pop($stack);
                continue;
            }

            if ($seg == '.') {
                // Ignore this segment
                continue;
            }

            $stack[] = $seg;
        }

        // Remove last /
        if (empty($stack[count($stack) - 1])) {
            array_pop($stack);
        }

        return implode($dirSep, $stack);
    }

    /**
    * Creates ASCII name
    *
    * @param string name encoded in UTF-8
    * @return string name containing only numbers, chars without diacritics, underscore and dash
    * @copyright Jakub Vrána, https://php.vrana.cz/
    */
    private function normalizeName($name)
    {
        $name = preg_replace('~[^\\pL0-9_]+~u', '-', $name);
        $name = trim($name, "-");
        //$name = iconv("utf-8", "us-ascii//TRANSLIT", $name);
        $name = preg_replace('~[^-a-z0-9_]+~', '', $name);
        return $name;
    }
}
