<?php

namespace Seafile\Client\Resource;

use Exception;
use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\Psr7\Response;
use http\Env\Request;
use InvalidArgumentException;
use Psr\Http\Message\ResponseInterface;
use Seafile\Client\Type\DirectoryItem;
use Seafile\Client\Type\FileHistoryItem;
use \Seafile\Client\Type\Library as LibraryType;

/**
 * Handles everything regarding Seafile files.
 *
 * @package   Seafile\Resource
 * @author    Rene Schmidt DevOps UG (haftungsbeschränkt) & Co. KG <rene+_seafile_github@sdo.sh>
 * @copyright 2015-2020 Rene Schmidt DevOps UG (haftungsbeschränkt) & Co. KG <rene+_seafile_github@sdo.sh>
 * @license   https://opensource.org/licenses/MIT MIT
 * @link      https://github.com/Schmidt-DevOps/seafile-php-sdk
 */
class File extends Resource
{
    const API_VERSION = '2';

    /**
     * Mode of operation: copy
     */
    const OPERATION_COPY = 1;

    /**
     * Mode of operation: move
     */
    const OPERATION_MOVE = 2;

    /**
     * Mode of operation: create
     */
    const OPERATION_CREATE = 3;

    /**
     * Get download URL of a file
     *
     * @param LibraryType $library Library instance
     * @param DirectoryItem $item Item instance
     * @param string $dir Dir string
     * @param int $reuse Reuse more than once per hour
     *
     * @return string
     * @throws GuzzleException
     */
    public function getDownloadUrl(LibraryType $library, DirectoryItem $item, string $dir = '/', int $reuse = 1)
    {
        $url = $this->getApiBaseUrl()
            . '/repos/'
            . $library->id
            . '/file/'
            . '?reuse=' . $reuse
            . '&p=' . $this->urlEncodePath($dir . $item->name);

        $response = $this->client->request('GET', $url);
        $downloadUrl = (string)$response->getBody();

        return preg_replace("/\"/", '', $downloadUrl);
    }

    /**
     * URL-encode path string
     *
     * @param string $path Path string
     *
     * @return string
     */
    protected function urlEncodePath(string $path)
    {
        return implode('/', array_map('rawurlencode', explode('/', (string)$path)));
    }

    /**
     * Get download URL of a file from a Directory item
     *
     * @param LibraryType $library Library instance
     * @param DirectoryItem $item Item instance
     * @param string $localFilePath Save file to path
     * @param string $dir Dir string
     * @param int $reuse Reuse more than once per hour
     *
     * @return ResponseInterface
     * @throws Exception
     * @throws GuzzleException
     */
    public function downloadFromDir(
        LibraryType $library,
        DirectoryItem $item,
        string $localFilePath,
        string $dir,
        int $reuse = 1
    ): ResponseInterface
    {
        if (is_readable($localFilePath)) {
            throw new Exception('File already exists');
        }

        $downloadUrl = $this->getDownloadUrl($library, $item, $dir, $reuse);

        return $this->client->request('GET', $downloadUrl, ['save_to' => $localFilePath]);
    }

    /**
     * Get download URL of a file
     *
     * @param LibraryType $library Library instance
     * @param string $filePath Save file to path
     * @param string $localFilePath Local file path
     * @param int $reuse Reuse more than once per hour
     *
     * @return ResponseInterface
     * @throws Exception
     * @throws GuzzleException
     */
    public function download(LibraryType $library, string $filePath, string $localFilePath, int $reuse = 1)
    {
        $item = new DirectoryItem();
        $item->name = basename($filePath);

        $dir = str_replace("\\", "/", dirname($filePath)); // compatibility for windows

        return $this->downloadFromDir($library, $item, $localFilePath, $dir, $reuse);
    }

    /**
     * Update file
     *
     * @param LibraryType $library Library instance
     * @param string $localFilePath Local file path
     * @param string $dir Library dir
     * @param mixed $filename File name, or false to use the name from $localFilePath
     *
     * @return ResponseInterface
     * @throws Exception
     * @throws GuzzleException
     */
    public function update(LibraryType $library, string $localFilePath, string $dir = '/', $filename = false): ResponseInterface
    {
        return $this->upload($library, $localFilePath, $dir, $filename, false);
    }

    /**
     * Get upload URL
     *
     * @param LibraryType $library Library instance
     * @param bool $newFile Is new file (=upload) or not (=update)
     * @param string $dir Directory to upload to
     *
     * @return String Upload link
     * @throws GuzzleException
     */
    public function getUploadUrl(LibraryType $library, bool $newFile = true, string $dir = "/"): string
    {
        $url = $this->getApiBaseUrl()
            . '/repos/'
            . $library->id
            . '/' . ($newFile ? 'upload' : 'update') . '-link/'
            . '?p=' . $dir;

        $response = $this->client->request('GET', $url);
        $uploadLink = (string)$response->getBody();

        return preg_replace("/\"/", '', $uploadLink);
    }

    /**
     * Get multipart params for uploading/updating file
     *
     * @param string $localFilePath Local file path
     * @param string $dir Library dir
     * @param bool $newFile Is new file (=upload) or not (=update)
     * @param mixed $newFilename New file name, or false to use the name from $localFilePath
     *
     * @return array
     */
    public function getMultiPartParams(
        string $localFilePath,
        string $dir,
        bool $newFile = true,
        $newFilename = false
    ): array
    {
        if ($newFilename === false) {
            $fileBaseName = basename($localFilePath);
        } else {
            $fileBaseName = $newFilename;
        }

        $multiPartParams = [
            [
                'headers' => ['Content-Type' => 'application/octet-stream'],
                'name' => 'file',
                'contents' => fopen($localFilePath, 'r'),
            ],
            [
                'name' => 'name',
                'contents' => $fileBaseName,
            ],
            [
                'name' => 'filename',
                'contents' => $fileBaseName,
            ],
        ];

        if ($newFile) {
            $multiPartParams[] = [
                'name' => 'parent_dir',
                'contents' => $dir,
            ];
        } else {
            $multiPartParams[] = [
                'name' => 'target_file',
                'contents' => rtrim($dir, "/") . "/" . $fileBaseName,
            ];
        }

        return $multiPartParams;
    }

    /**
     * Upload file
     *
     * @param LibraryType $library Library instance
     * @param string $localFilePath Local file path
     * @param string $dir Library dir
     * @param mixed $newFilename New file name, or false to use the name from $localFilePath
     * @param bool $newFile Is new file (=upload) or not (=update)
     *
     * @return ResponseInterface
     * @throws Exception
     * @throws GuzzleException
     */
    public function upload(
        LibraryType $library,
        string $localFilePath,
        string $dir = '/',
        $newFilename = false,
        bool $newFile = true
    ): ResponseInterface
    {
        if (!is_readable($localFilePath)) {
            throw new Exception('File ' . $localFilePath . ' could not be read or does not exist');
        }

        return $this->client->request(
            'POST',
            $this->getUploadUrl($library, $newFile, $dir),
            [
                'headers' => ['Accept' => '*/*'],
                'multipart' => $this->getMultiPartParams($localFilePath, $dir, $newFile, $newFilename),
            ]
        );
    }

    /**
     * Get file detail
     *
     * @param LibraryType $library Library instance
     * @param string $remoteFilePath Remote file path
     *
     * @return DirectoryItem
     * @throws GuzzleException
     * @throws Exception
     */
    public function getFileDetail(LibraryType $library, string $remoteFilePath): DirectoryItem
    {
        $url = $this->getApiBaseUrl()
            . '/repos/'
            . $library->id
            . '/file/detail/'
            . '?p=' . $this->urlEncodePath($remoteFilePath);

        $response = $this->client->request('GET', $url);

        $json = json_decode((string)$response->getBody());

        return (new DirectoryItem)->fromJson($json);
    }

    /**
     * Remove a file
     *
     * @param LibraryType $library Library object
     * @param string $filePath File path
     *
     * @return bool
     * @throws GuzzleException
     */
    public function remove(LibraryType $library, string $filePath): bool
    {
        // do not allow empty paths
        if (empty($filePath)) {
            return false;
        }

        $uri = sprintf(
            '%s/repos/%s/file/?p=%s',
            $this->clipUri($this->getApiBaseUrl()),
            $library->id,
            $this->urlEncodePath($filePath)
        );

        $response = $this->client->request(
            'DELETE',
            $uri,
            [
                'headers' => ['Accept' => 'application/json'],
            ]
        );

        return $response->getStatusCode() === 200;
    }

    /**
     * Rename a file
     *
     * @param LibraryType $library Library object
     * @param DirectoryItem $dirItem Directory item to rename
     * @param string $newFilename New file name; see "Issues" in the readme
     *
     * @return bool
     * @throws GuzzleException
     */
    public function rename(LibraryType $library, DirectoryItem $dirItem, string $newFilename): bool
    {
        $filePath = $dirItem->dir . $dirItem->name;

        if (empty($filePath)) {
            throw new InvalidArgumentException('Invalid file path: must not be empty');
        }

        if (empty($newFilename) || strpos($newFilename, '/') === 0) {
            throw new InvalidArgumentException('Invalid new file name: length must be >0 and must not start with /');
        }

        $uri = sprintf(
            '%s/repos/%s/file/?p=%s',
            $this->clipUri($this->getApiBaseUrl()),
            $library->id,
            $this->urlEncodePath($filePath)
        );

        $response = $this->client->request(
            'POST',
            $uri,
            [
                'headers' => ['Accept' => 'application/json'],
                'multipart' => [
                    [
                        'name' => 'operation',
                        'contents' => 'rename',
                    ],
                    [
                        'name' => 'newname',
                        'contents' => $newFilename,
                    ],
                ],
            ]
        );

        $success = $response->getStatusCode() === 200;

        if ($success) {
            $dirItem->name = $newFilename;
        }

        return $success;
    }

    /**
     * Copy a file
     *
     * @param LibraryType $srcLibrary Source library object
     * @param string $srcFilePath Source file path
     * @param LibraryType $dstLibrary Destination library object
     * @param string $dstDirectoryPath Destination directory path
     * @param int $operation Operation mode
     *
     * @return bool
     * @throws GuzzleException
     */
    public function copy(
        LibraryType $srcLibrary,
        string $srcFilePath,
        LibraryType $dstLibrary,
        string $dstDirectoryPath,
        int $operation = self::OPERATION_COPY
    ): bool
    {
        // do not allow empty paths
        if (empty($srcFilePath) || empty($dstDirectoryPath)) {
            return false;
        }

        $operationMode = 'copy';
        $returnCode = 200;

        if ($operation === self::OPERATION_MOVE) {
            $operationMode = 'move';
            $returnCode = 301;
        }

        $uri = sprintf(
            '%s/repos/%s/file/?p=%s',
            $this->clipUri($this->getApiBaseUrl()),
            $srcLibrary->id,
            $this->urlEncodePath($srcFilePath)
        );

        $response = $this->client->request(
            'POST',
            $uri,
            [
                'headers' => ['Accept' => 'application/json'],
                'multipart' => [
                    [
                        'name' => 'operation',
                        'contents' => $operationMode,
                    ],
                    [
                        'name' => 'dst_repo',
                        'contents' => $dstLibrary->id,
                    ],
                    [
                        'name' => 'dst_dir',
                        'contents' => $dstDirectoryPath,
                    ],
                ],
            ]
        );

        return $response->getStatusCode() === $returnCode;
    }

    /**
     * Move a file
     *
     * @param LibraryType $srcLibrary Source library object
     * @param string $srcFilePath Source file path
     * @param LibraryType $dstLibrary Destination library object
     * @param string $dstDirectoryPath Destination directory path
     *
     * @return bool
     * @throws GuzzleException
     */
    public function move(
        LibraryType $srcLibrary,
        string $srcFilePath,
        LibraryType $dstLibrary,
        string $dstDirectoryPath
    ): bool
    {
        return $this->copy($srcLibrary, $srcFilePath, $dstLibrary, $dstDirectoryPath, self::OPERATION_MOVE);
    }

    /**
     * Get file revision download URL
     *
     * @param LibraryType $library Source library object
     * @param DirectoryItem $dirItem Item instance
     * @param FileHistoryItem $fileHistoryItem FileHistory item instance
     *
     * @return string|string[]
     * @throws GuzzleException
     */
    public function getFileRevisionDownloadUrl(
        LibraryType $library,
        DirectoryItem $dirItem,
        FileHistoryItem $fileHistoryItem
    )
    {
        $url = $this->getApiBaseUrl()
            . '/repos/'
            . $library->id
            . '/file/revision/'
            . '?p=' . $this->urlEncodePath($dirItem->path . $dirItem->name)
            . '&commit_id=' . $fileHistoryItem->id;

        $response = $this->client->request('GET', $url);

        return preg_replace("/\"/", '', (string)$response->getBody());
    }

    /**
     * Download file revision
     *
     * @param LibraryType $library Source library object
     * @param DirectoryItem $dirItem Item instance
     * @param FileHistoryItem $fileHistoryItem FileHistory item instance
     * @param string $localFilePath Save file to path. Existing files will be overwritten without warning
     *
     * @return ResponseInterface
     * @throws GuzzleException
     */
    public function downloadRevision(
        LibraryType $library,
        DirectoryItem $dirItem,
        FileHistoryItem $fileHistoryItem,
        string $localFilePath
    ): ResponseInterface
    {
        $downloadUrl = $this->getFileRevisionDownloadUrl($library, $dirItem, $fileHistoryItem);

        return $this->client->request('GET', $downloadUrl, ['save_to' => $localFilePath]);
    }

    /**
     * Get history of a file DirectoryItem
     *
     * @param LibraryType $library Library instance
     * @param DirectoryItem $item Item instance
     *
     * @return FileHistoryItem[]
     * @throws GuzzleException
     * @throws Exception
     */
    public function getHistory(LibraryType $library, DirectoryItem $item)
    {
        $url = $this->getApiBaseUrl()
            . '/repos/'
            . $library->id
            . '/file/history/'
            . '?p=' . $this->urlEncodePath($item->path . $item->name);

        $response = $this->client->request('GET', $url);

        $json = json_decode($response->getBody());

        $fileHistoryCollection = [];

        foreach ($json->commits as $lib) {
            $fileHistoryCollection[] = (new FileHistoryItem)->fromJson($lib);
        }

        return $fileHistoryCollection;
    }

    /**
     * Create empty file on Seafile server
     *
     * @param LibraryType $library Library instance
     * @param DirectoryItem $item Item instance
     *
     * @return bool
     * @throws GuzzleException
     */
    public function create(LibraryType $library, DirectoryItem $item): bool
    {
        // do not allow empty paths
        if (empty($item->path)) {
            return false;
        }

        $uri = sprintf(
            '%s/repos/%s/file/?p=%s',
            $this->clipUri($this->getApiBaseUrl()),
            $library->id,
            $this->urlEncodePath($item->path . $item->name)
        );

        $response = $this->client->request(
            'POST',
            $uri,
            [
                'headers' => ['Accept' => 'application/json'],
                'multipart' => [
                    [
                        'name' => 'operation',
                        'contents' => 'create',
                    ],
                ],
            ]
        );
// @todo Return the actual response instead of bool
        return $response->getStatusCode() === 201;
    }

//    /**
//     * Lock file. Only supported in Seafile Professional which I currently do not have.
//     *
//     * @param LibraryType   $library Library instance
//     * @param DirectoryItem $item    Item instance
//     *
//     * @return bool
//     */
//    public function lock(LibraryType $library, DirectoryItem $item)
//    {
//        // do not allow empty paths
//        if (empty($item->path) || empty($item->name)) {
//            return false;
//        }
//
//        $uri = sprintf(
//            '%s/repos/%s/file/',
//            $this->clipUri($this->getApiBaseUrl()),
//            $library->id
//        );
//
//        $response = $this->client->request(
//            'PUT',
//            $uri,
//            [
//                'headers' => ['Accept' => 'application/json'],
//                'multipart' => [
//                    [
//                        'name' => 'operation',
//                        'contents' => 'lock'
//                    ],
//                    [
//                        'name' => 'p',
//                        'contents' => $item->path . $item->name
//                    ],
//                ],
//            ]
//        );
//
//        return $response->getStatusCode() === 200;
//    }
}
