<?php

namespace Dcat\Admin\Support;

use Illuminate\Http\Request;
use Symfony\Component\Finder\Finder;
use Symfony\Component\HttpFoundation\File\UploadedFile;

/**
 * WebUploader文件上传处理.
 *
 * @property string $_id
 * @property int $chunk
 * @property int $chunks
 * @property string $upload_column
 * @property UploadedFile $file
 */
class WebUploader
{
    const FILE_NAME = '_file_';

    public $temporaryDirectory = 'tmp';

    protected $temporaryFilePath;

    protected $completeFile;

    public function __construct(Request $request = null)
    {
        $request = $this->prepareRequest($request ?: request());

        $this->_id = $request->get('_id');
        $this->chunk = $request->get('chunk');
        $this->chunks = $request->get('chunks');
        $this->upload_column = $request->get('upload_column');
        $this->file = $request->file(static::FILE_NAME);
    }

    protected function prepareRequest($request)
    {
        $relation = $request->get('_relation');
        if (! $relation || ! is_string($relation)) {
            return $request;
        }

        return $request->merge([
            '_relation' => trim($relation, ','),
        ]);
    }

    /**
     * 判断是否是分块上传.
     *
     * @return bool
     */
    public function hasChunkFile()
    {
        return $this->chunks > 1;
    }

    /**
     * 判断是否是文件上传请求.
     *
     * @return bool
     */
    public function isUploading()
    {
        $file = $this->file;

        if (
            ! $file
            || ! $this->upload_column
            || ! $file instanceof UploadedFile
        ) {
            return false;
        }

        return true;
    }

    /**
     * 获取完整的上传文件.
     *
     * @return UploadedFile|void
     */
    public function getUploadedFile()
    {
        $file = $this->file;

        if (! $file || ! $file instanceof UploadedFile) {
            return;
        }

        if (! $this->hasChunkFile()) {
            return $file;
        }

        if ($this->completeFile !== null) {
            return $this->completeFile;
        }

        return $this->completeFile = $this->mergeChunks($file);
    }

    /**
     * 移除临时文件以及文件夹.
     */
    public function deleteTemporaryFile()
    {
        if (! $this->temporaryFilePath) {
            return;
        }
        @unlink($this->temporaryFilePath);

        if (
            ! Finder::create()
                ->in($dir = dirname($this->temporaryFilePath))
                ->files()
                ->count()
        ) {
            @rmdir($dir);
        }
    }

    /**
     * 合并分块文件.
     *
     * @param  UploadedFile  $file
     * @return UploadedFile|false
     */
    protected function mergeChunks(UploadedFile $file)
    {
        $tmpDir = $this->getTemporaryPath($this->_id);
        $newFilename = $this->generateChunkFileName($file);

        // 移动当前分块到临时目录.
        $this->moveChunk($file, $tmpDir, $newFilename);

        // 判断所有分块是否上传完毕.
        if (! $this->isComplete($tmpDir, $newFilename)) {
            return false;
        }

        $this->temporaryFilePath = $tmpDir.'/'.$newFilename.'.tmp';

        $this->putTempFileContent($this->temporaryFilePath, $tmpDir, $newFilename);

        return new UploadedFile(
            $this->temporaryFilePath,
            $file->getClientOriginalName(),
            null,
            null,
            true
        );
    }

    /**
     * 判断所有分块是否上传完毕.
     *
     * @param  string  $tmpDir
     * @param  string  $newFilename
     * @return bool
     */
    protected function isComplete($tmpDir, $newFilename)
    {
        for ($index = 0; $index < $this->chunks; $index++) {
            if (! is_file("{$tmpDir}/{$newFilename}.{$index}.part")) {
                return false;
            }
        }

        return true;
    }

    /**
     * 移动分块文件到临时目录.
     *
     * @param  UploadedFile  $file
     * @param  string  $tmpDir
     * @param  string  $newFilename
     */
    protected function moveChunk(UploadedFile $file, $tmpDir, $newFilename)
    {
        $file->move($tmpDir, "{$newFilename}.{$this->chunk}.part");
    }

    /**
     * @param  string  $path
     * @param  string  $tmpDir
     * @param  string  $newFilename
     */
    protected function putTempFileContent($path, $tmpDir, $newFileame)
    {
        $out = fopen($path, 'wb');

        if (flock($out, LOCK_EX)) {
            for ($index = 0; $index < $this->chunks; $index++) {
                $partPath = "{$tmpDir}/{$newFileame}.{$index}.part";
                if (! $in = @fopen($partPath, 'rb')) {
                    break;
                }

                while ($buff = fread($in, 4096)) {
                    fwrite($out, $buff);
                }

                @fclose($in);
                @unlink($partPath);
            }

            flock($out, LOCK_UN);
        }

        fclose($out);
    }

    /**
     * 生成分块文件名称.
     *
     * @param  UploadedFile  $file
     * @return string
     */
    protected function generateChunkFileName(UploadedFile $file)
    {
        return md5($file->getClientOriginalName());
    }

    /**
     * 获取临时文件路径.
     *
     * @param  mixed  $path
     * @return string
     */
    public function getTemporaryPath($path)
    {
        return $this->getTemporaryDirectory().'/'.$path;
    }

    /**
     * 获取临时文件目录.
     *
     * @return string
     */
    public function getTemporaryDirectory()
    {
        $dir = storage_path($this->temporaryDirectory);

        if (! is_dir($dir)) {
            app('files')->makeDirectory($dir, 0755, true);
        }

        return rtrim($dir, '/');
    }
}