<?php

namespace Kylin\Admin\Form\Field;

use Kylin\Admin\Exception\UploadException;
use Kylin\Admin\Traits\HasUploadedFile;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\URL;
use Illuminate\Support\Facades\Validator;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\Response;

trait UploadField
{
    use HasUploadedFile {
        disk as _disk;
    }

    /**
     * Upload directory.
     *
     * @var string
     */
    protected $directory = '';

    /**
     * File name.
     *
     * @var null
     */
    protected $name = null;

    /**
     * Storage instance.
     *
     * @var \Illuminate\Filesystem\Filesystem
     */
    protected $storage;

    /**
     * If use unique name to store upload file.
     *
     * @var bool
     */
    protected $useUniqueName = false;

    /**
     * If use sequence name to store upload file.
     *
     * @var bool
     */
    protected $useSequenceName = false;

    /**
     * Controls the storage permission. Could be 'private' or 'public'.
     *
     * @var string
     */
    protected $storagePermission;

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

    /**
     * Retain file when delete record from DB.
     *
     * @var bool
     */
    protected $retainable = false;

    /**
     * @var bool
     */
    protected $saveFullUrl = false;

    /**
     * Initialize the storage instance.
     *
     * @return void.
     */
    protected function initStorage()
    {
        $this->disk(config('admin.upload.disk'));

        if (!$this->storage) {
            $this->storage = false;
        }
    }

    /**
     * If name already exists, rename it.
     *
     * @param $file
     * @return void
     */
    public function renameIfExists(UploadedFile $file)
    {
        if ($this->getStorage()->exists("{$this->getDirectory()}/$this->name")) {
            $this->name = $this->generateUniqueName($file);
        }
    }

    /**
     * @return string
     */
    protected function getUploadPath()
    {
        return "{$this->getDirectory()}/$this->name";
    }

    /**
     * Get store name of upload file.
     *
     * @param UploadedFile $file
     * @return string
     */
    protected function getStoreName(UploadedFile $file)
    {
        if ($this->useUniqueName) {
            return $this->generateUniqueName($file);
        }

        if ($this->useSequenceName) {
            return $this->generateSequenceName($file);
        }

        if ($this->name instanceof \Closure) {
            $this->name = $this->name->call($this->values(), $file);
        }

        if ($this->name !== '' && is_string($this->name)) {
            return $this->name;
        }

        return $file->getClientOriginalName();
    }

    /**
     * Get directory for store file.
     *
     * @return mixed|string
     */
    public function getDirectory()
    {
        if ($this->directory instanceof \Closure) {
            $this->directory = $this->directory->call($this->values(), $this->form);
        }

        return $this->directory ?: $this->defaultDirectory();
    }

    /**
     * Indicates if the underlying field is retainable.
     *
     * @param bool $retainable
     * @return $this
     */
    public function retainable(bool $retainable = true)
    {
        $this->retainable = $retainable;

        return $this;
    }

    public function saveFullUrl(bool $value = true)
    {
        $this->saveFullUrl = $value;

        return $this;
    }

    /**
     * Upload File.
     *
     * @param UploadedFile $file
     * @return Response
     */
    public function upload(UploadedFile $file)
    {
        $request = request();

        $id = $request->get('_id');

        if (!$id) {
            return $this->responseErrorMessage('Missing id');
        }

        if ($errors = $this->getValidationErrors($file)) {
            return $this->responseValidationMessage($errors);
        }

        $this->name = $this->getStoreName($file);

        if ($this->options['override']) {
            $this->remove();
        }

        $this->renameIfExists($file);

        $this->prepareFile($file);

        if (!is_null($this->storagePermission)) {
            $result = $this->getStorage()->putFileAs($this->getDirectory(), $file, $this->name, $this->storagePermission);
        } else {
            $result = $this->getStorage()->putFileAs($this->getDirectory(), $file, $this->name);
        }

        if ($result) {
            $path = $this->getUploadPath();
            $url = $this->objectUrl($path);

            // 上传成功
            return $this->responseUploaded($this->saveFullUrl ? $url : $path, $url);
        }

        // 上传失败
        throw new UploadException(trans('admin.uploader.upload_failed'));
    }

    public function remove()
    {
        if ($this->getStorage()->exists("{$this->getDirectory()}/$this->name")) {
            $this->getStorage()->delete("{$this->getDirectory()}/$this->name");
        }
    }

    /**
     * @param UploadedFile $file
     */
    protected function prepareFile(UploadedFile $file)
    {
    }

    /**
     * Specify the directory and name for upload file.
     *
     * @param string|\Closure $directory
     * @param null|string $name
     * @return $this
     */
    public function move($directory, $name = null)
    {
        $this->dir($directory);

        $this->name($name);

        return $this;
    }

    /**
     * Specify the directory upload file.
     *
     * @param string|\Closure $dir
     * @return $this
     */
    public function dir($dir)
    {
        if ($dir) {
            $this->directory = $dir;
        }

        return $this;
    }

    /**
     * Set name of store name.
     *
     * @param string|callable $name
     * @return $this
     */
    public function name($name)
    {
        if ($name) {
            $this->name = $name;
        }

        return $this;
    }

    /**
     * Use unique name for store upload file.
     *
     * @return $this
     */
    public function uniqueName()
    {
        $this->useUniqueName = true;

        return $this;
    }

    /**
     * Use sequence name for store upload file.
     *
     * @return $this
     */
    public function sequenceName()
    {
        $this->useSequenceName = true;

        return $this;
    }

    /**
     * Generate a unique name for uploaded file.
     *
     * @param UploadedFile $file
     * @return string
     */
    protected function generateUniqueName(UploadedFile $file)
    {
        return md5(uniqid()) . '.' . $file->getClientOriginalExtension();
    }

    /**
     * Generate a sequence name for uploaded file.
     *
     * @param UploadedFile $file
     * @return string
     */
    protected function generateSequenceName(UploadedFile $file)
    {
        $index = 1;
        $extension = $file->getClientOriginalExtension();
        $originalName = pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME);
        $newName = $originalName . '_' . $index . '.' . $extension;

        while ($this->getStorage()->exists("{$this->getDirectory()}/$newName")) {
            $index++;
            $newName = $originalName . '_' . $index . '.' . $extension;
        }

        return $newName;
    }

    /**
     * @param UploadedFile $file
     * @return bool|\Illuminate\Support\MessageBag
     */
    protected function getValidationErrors(UploadedFile $file)
    {
        $data = $rules = $attributes = [];

        // 如果文件上传有错误，则直接返回错误信息
        if ($file->getError() !== UPLOAD_ERR_OK) {
            return $file->getErrorMessage();
        }

        if (!$fieldRules = $this->getRules()) {
            return false;
        }

        Arr::set($rules, $this->column, $fieldRules);
        Arr::set($attributes, $this->column, $this->label);
        Arr::set($data, $this->column, $file);

        /* @var \Illuminate\Validation\Validator $validator */
        $validator = Validator::make($data, $rules, $this->validationMessages, $attributes);

        if (!$validator->passes()) {
            $errors = $validator->errors()->getMessages()[$this->column];

            return implode('<br> ', $errors);
        }
    }

    /**
     * Destroy original files.
     *
     * @return void.
     */
    public function destroy()
    {
        $this->deleteFile($this->original);
    }

    /**
     * Destroy original files.
     *
     * @param $file
     */
    public function destroyIfChanged($file)
    {
        if (!$file || !$this->original) {
            return $this->destroy();
        }

        $file = array_filter((array)$file);
        $original = (array)$this->original;

        $this->deleteFile(Arr::except(array_combine($original, $original), $file));
    }

    /**
     * Destroy files.
     *
     * @param string|array $path
     */
    public function deleteFile($paths)
    {
        if (!$paths || $this->retainable) {
            return;
        }

        if (method_exists($this, 'destroyThumbnail')) {
            $this->destroyThumbnail($paths);
        }

        $storage = $this->getStorage();

        foreach ((array)$paths as $path) {
            if ($storage->exists($path)) {
                $storage->delete($path);
            } else {
                $prefix = $storage->url('');
                $path = str_replace($prefix, '', $path);

                if ($storage->exists($path)) {
                    $storage->delete($path);
                }
            }
        }
    }

    /**
     * Get storage instance.
     *
     * @return \Illuminate\Filesystem\Filesystem|null
     */
    public function getStorage()
    {
        if ($this->storage === null) {
            $this->initStorage();
        }

        return $this->storage;
    }

    /**
     * Set disk for storage.
     *
     * @param string $disk Disks defined in `config/filesystems.php`.
     * @return $this
     *
     * @throws \Exception
     */
    public function disk($disk)
    {
        try {
            $this->storage = Storage::disk($disk);
        } catch (\Exception $exception) {
            if (!array_key_exists($disk, config('filesystems.disks'))) {
                admin_error(
                    'Config error.',
                    "Disk [$disk] not configured, please add a disk config in `config/filesystems.php`."
                );

                return $this;
            }

            throw $exception;
        }

        return $this;
    }

    /**
     * Get file visit url.
     *
     * @param string $path
     * @return string
     */
    public function objectUrl($path)
    {
        if (URL::isValidUrl($path)) {
            return $path;
        }

        return $this->getStorage()->url($path);
    }

    /**
     * @param $permission
     * @return $this
     */
    public function storagePermission($permission)
    {
        $this->storagePermission = $permission;

        return $this;
    }
}
