<?php

namespace Ignite\Core\Files;

use Ignite\Core\Contracts\Files\FileEncrypter;
use Ignite\Core\Facades\FileEncryption;
use Illuminate\Support\Facades\Storage;

/**
 * Interact with files in a more OOP way.
 * This is helpful when wanting to not only pass around a path to a file, but also the disk it resides on.
 * @see \Illuminate\Filesystem\FilesystemAdapter
 */
class File
{
    /**
     * @var string
     */
    protected $path;

    /**
     * @var string
     */
    protected $disk;

    /**
     * @var \Illuminate\Contracts\Filesystem\Filesystem|\Illuminate\Filesystem\FilesystemAdapter
     */
    protected $filesystem;

    public function __construct(string $path, ?string $disk = null)
    {
        $this->path = $path;
        $this->disk = $disk ?? config('filesystems.default');
        $this->filesystem = Storage::disk($this->disk);
    }

    public static function new(string $path, ?string $disk = null): self
    {
        return new static($path, $disk);
    }

    /**
     * Get the path to the file, relative to the disk's root.
     *
     * @return string
     */
    public function getRelativePath(): string
    {
        return $this->path;
    }

    /**
     * The disk the file is on.
     *
     * @return string
     */
    public function getDisk(): string
    {
        return $this->disk;
    }

    /**
     * Is the given file the same file as this file?
     *
     * @param File $file
     * @return bool
     */
    public function is(File $file): bool
    {
        return $file->getDisk() == $this->getDisk() &&
            $file->getRelativePath() == $this->getRelativePath();
    }

    /**
     * The filesystem the file is on.
     *
     * @return \Illuminate\Contracts\Filesystem\Filesystem|\Illuminate\Filesystem\FilesystemAdapter
     */
    public function getFilesystem()
    {
        return $this->filesystem;
    }

    /**
     * Get the basename of this file.
     * Based on: https://www.php.net/manual/en/splfileinfo.getbasename.php
     *
     * @return string
     */
    public function getBasename(string $suffix = '')
    {
        return basename($this->path, $suffix);
    }

    /**
     * Copy this file to the given file's disk & path.
     *
     * @param File $file
     * @return bool
     * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException If this file does not exist.
     */
    public function copyToFile(File $file): bool
    {
        if ($this->is($file)) {
            return true;
        }

        return ($readStream = $this->readStream()) && $file->writeStream($readStream);
    }

    /**
     * Copy this file to a new temporary file.
     *
     * @return TemporaryFile
     * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
     */
    public function copyToTemporary(): TemporaryFile
    {
        $tempFile = TemporaryFileBuilder::start()->make();

        $this->copyToFile($tempFile);

        return $tempFile;
    }

    /**
     * Encrypt this file.
     *
     * @param FileEncrypter|string|null $fileEncrypter
     * @param File|null $saveTo If null given, we'll overwrite this file.
     * @return File|TemporaryFile
     * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
     */
    public function encrypt($fileEncrypter = null, ?File $saveTo = null): File
    {
        $fileEncrypter = $fileEncrypter instanceof FileEncrypter
            ? $fileEncrypter
            : FileEncryption::encrypter($fileEncrypter);

        return $fileEncrypter->encrypt($this, $saveTo ?? $this);
    }

    /**
     * Decrypt this file.
     *
     * @param FileEncrypter|string|null $fileEncrypter
     * @param File|null $saveTo If null given, we'll overwrite this file.
     * @return File|TemporaryFile
     * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
     */
    public function decrypt($fileEncrypter = null, ?File $saveTo = null): File
    {
        $fileEncrypter = $fileEncrypter instanceof FileEncrypter
            ? $fileEncrypter
            : FileEncryption::encrypter($fileEncrypter);

        return $fileEncrypter->decrypt($this, $saveTo ?? $this);
    }

    /**
     * Proxy calls to Laravel's filesystem package.
     *
     * @param $name
     * @param $arguments
     * @return mixed
     */
    public function __call($name, $arguments)
    {
        // Drop the first argument on these methods when working with this class:
        // $filesystem->get($path) => $file->get()
        // $filesystem->put($path, $contents, $options) => $file->put($contents, $options)
        // @see \Illuminate\Filesystem\FilesystemAdapter
        $supportedMethods = [
            'assertExists',
            'assertMissing',
            'exists',
            'missing',
            'get',
            'response',
            'download',
            'path',
            'put',
            'putFile',
            'putFileAs',
            'getVisibility',
            'setVisibility',
            'prepend',
            'append',
            'delete',
            'copy',
            'move',
            'size',
            'mimeType',
            'lastModified',
            'url',
            'readStream',
            'writeStream',
            'temporaryUrl',
        ];

        if (! in_array($name, $supportedMethods) || ! method_exists($this->filesystem, $name)) {
            throw new \BadMethodCallException("Call to undefined method $name on ".static::class);
        }

        return $this->filesystem->{$name}($this->path, ...$arguments);
    }
}
