<?php
namespace App\Models;

use Illuminate\Support\Facades\Storage;
use Illuminate\Http\UploadedFile;
use DOMDocument;
use Exception;

class FileLib
{
    /*
     * Storage 저장 경로 생성
     */
    public function setPath(string $uploadType, string|null $addPath = null): string
    {
        $uploadPath = $this->getPublicPath($uploadType, $addPath);
        $storagePath = $this->getStoragePath($uploadType, $addPath);

        // 해당 경로가 없다면 생성 해주고 index.php 파일 생성
        if (is_dir($storagePath) === false) {
            mkdir($storagePath, 0707, true);
            touch($storagePath . '/index.php');
            chmod($storagePath . '/index.php', 0644);
        }

        return $uploadPath;
    }

    /*
     * Public 저장 경로 조회
     */
    public function getPublicPath(string $uploadType, string|null $addPath = null): string
    {
        return join(DIRECTORY_SEPARATOR, [
            UPLOAD_PATH_PUBLIC,
            $uploadType,
            date('Y'),
            date('m'),
            date('d'),
            $addPath
        ]);
    }

    /*
     * Storage 저장 경로 조회
     */
    public function getStoragePath(string $uploadType, string|null $addPath = null): string
    {
        return storage_path(UPLOAD_PATH_APP . DIRECTORY_SEPARATOR . $this->getPublicPath($uploadType, $addPath));
    }

    /*
     * 경로 완전 삭제
     */
    public function removePath(string $uploadType, string|null $addPath = null): void
    {
        $storagePath = $this->getStoragePath($uploadType, $addPath);
        if (is_dir($storagePath)) {
            foreach (scandir($storagePath) as $file) {
                if (in_array($file, ['.', '..'])) {
                    continue;
                }

                $path = ($storagePath . DIRECTORY_SEPARATOR . $file);

                if (is_dir($path)) {
                    $this->removePath($uploadType, $addPath . DIRECTORY_SEPARATOR . $file);
                } else {
                    unlink($path);
                }
            }
            rmdir($storagePath);
        }
    }

    /*
     * 본문 내용중 외부 이미지 주소를 서버로 가져온 후에 내부 주소로 변경합니다.
     */
    public function saveAsImage(string|null $contents, string $uploadType, string|null $addPath = null): string|null
    {
        return $this->saveImageFromBlob(
            $this->saveImageFromUrl($contents, $uploadType, $addPath), $uploadType, $addPath);
    }

    /*
     * CURL를 이용한 외부 이미지 다운로드
     */
    public function saveImageFromUrl(string|null $contents, string $uploadType, string|null $addPath = null): string|null
    {
        $dom = new DOMDocument('1.0', 'UTF-8');
        libxml_use_internal_errors(true);
        $dom->loadHTML('<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>' . $contents);
        libxml_clear_errors();

        $images = [];

        foreach ($dom->getElementsByTagName('img') as $img)
        {
            $src = $img->attributes->getNamedItem("src")->value;
            $path = canonicalizePath($src);

            if(file_exists(public_path($path))) {
                $images[] = $path;
                continue;
            }

            if(filter_var($src, FILTER_VALIDATE_URL)) {
                $url = parse_url($src);

                if (!empty($url['host']) && $url['host'] !== request()->getHttpHost())
                {
                    $ch = curl_init($src);
                    curl_setopt($ch, CURLOPT_HEADER, 0);
                    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
                    $err = curl_error($ch);

                    $rawData = [];
                    if (empty($err)) {
                        $rawData = curl_exec($ch);
                    }
                    curl_close($ch);

                    if ($rawData)
                    {
                        // 파일 경로 설정
                        $uploadPath = $this->setPath($uploadType, $addPath);

                        $ext = $this->getExtension($src);

                        [$unix, $sec] = explode(' ', microtime());
                        $fileName = (md5(uniqid(mt_rand())) . '_' . str_replace('.', '', $sec . $unix) . '.' . $ext);
                        $savePath = Storage::path($uploadPath . $fileName);
                        $saveURL = Storage::url($uploadPath . $fileName);

                        $fp = fopen($savePath, 'w+');
                        fwrite($fp, $rawData);
                        fclose($fp);

                        if (file_exists($savePath)) {
                            $images[] = $saveURL;
                            $contents = str_replace($src, $saveURL, $contents);
                        }
                    }
                }
            }
        }

        // 기존 이미지 삭제
        $this->clear($images, $uploadType, $addPath);

        unset($dom, $images, $uploadPath, $addPath);

        return $contents;
    }

    /*
     * blob 종류의 이미지 저장
     */
    public function saveImageFromBlob(string|null $contents, string $uploadType, string|null $addPath = null): string|null
    {
        $dom = new DOMDocument('1.0', 'UTF-8');
        libxml_use_internal_errors(true);
        $dom->loadHTML('<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>' . $contents);
        libxml_clear_errors();

        $images = [];

        foreach ($dom->getElementsByTagName('img') as $img)
        {
            $src = $img->attributes->getNamedItem("src")?->value;
            $alt = $img->attributes->getNamedItem("alt")?->value;
            $target = $img->attributes->getNamedItem("data-target")?->value;
            $path = canonicalizePath($src);

            if(file_exists(public_path($path)) || !$target || !strpos($src, 'base64')) {
                $images[] = $path;
                continue;
            }

            try {
                $mime = mime_content_type($src);
                $ext = $this->getMimeExtension($mime);

                if (!$ext) {
                    $ext = $this->getExtension($alt);
                }
            } catch (Exception) {
                return null;
            }

            // 파일 경로 설정
            $uploadPath = $this->setPath($uploadType, $addPath);

            $link = str_replace('data:' . $mime . ';base64,', '', $src);
            $link = str_replace(' ', '+', $link);
            $isBase64 = isBase64($link);
            $link = base64_decode($link);

            [$unix, $sec] = explode(' ', microtime());
            $fileName = (md5(uniqid(mt_rand())) . '_' . str_replace('.', '', $sec . $unix) . '.' . $ext);
            $savePath = Storage::path($uploadPath . $fileName);
            $saveUrl = Storage::url($uploadPath . $fileName);

            // 파일저장
            if ($isBase64) {
                file_put_contents($savePath, $link, FILE_APPEND | LOCK_EX);
            }

            if (file_exists($savePath)) {
                $images[] = $saveUrl;
                $contents = str_replace($src, $saveUrl, $contents);
            }
        }

        // 기존 이미지 삭제
        $this->clear($images, $uploadType, $addPath);

        unset($dom, $images, $uploadPath, $addPath);

        return $contents;
    }

    public function isBase64($s): bool
    {
        // Check if there are valid base64 characters
        if (!preg_match('/^[a-zA-Z0-9\/\r\n+]*={0,2}$/', $s)) {
            return false;
        }

        // Decode the string in strict mode and check the results
        $decoded = base64_decode($s, true);
        if (false === $decoded) {
            return false;
        }

        // if string returned contains not printable chars
        if (0 < preg_match('/((?![[:graph:]])(?!\s)(?!\p{L}))./', $decoded, $matched)) {
            return false;
        }

        // Encode the string again
        if (base64_encode($decoded) != $s) {
            return false;
        }

        return true;
    }

    public function isBase64Encoded(string $s): bool
    {
        if ((bool)preg_match('/^[a-zA-Z0-9\/\r\n+]*={0,2}$/', $s) === false) {
            return false;
        }
        $decoded = base64_decode($s, true);
        if ($decoded === false) {
            return false;
        }
        $encoding = mb_detect_encoding($decoded);
        if (!in_array($encoding, ['UTF-8', 'ASCII'], true)) {
            return false;
        }
        return ($decoded !== false && base64_encode($decoded) === $s);
    }

    /*
     * 파일 확장자 조회
     */
    public function getExtension(string $path): string
    {
        return pathinfo($path, PATHINFO_EXTENSION) ?? 'unknown';
    }

    /*
     * 이미지 파일 여부
     */
    public function isImage(string $path): bool
    {
        if(!file_exists($path)) {
            return false;
        }

        $mime = mime_content_type($path);
        if($mime) {
            return in_array($mime, [
                image_type_to_mime_type(IMAGETYPE_GIF),
                image_type_to_mime_type(IMAGETYPE_JPEG),
                image_type_to_mime_type(IMAGETYPE_PNG),
                image_type_to_mime_type(IMAGETYPE_BMP)]
            );
        }
        return false;
    }

    /*
     * 첨부파일 정보 조회
     */
    public function upload(UploadedFile $file, string $uploadType, string|null $addPath = null): object
    {
        $publicPath = $this->getPublicPath($uploadType, $addPath); // 파일 경로 조회
        $filePath = $file->store($publicPath); // 파일 저장
        $fullPath = Storage::path($filePath);
        $fullURL = Storage::url($filePath);
        $imageInfo = $this->getImageInfo($fullPath);

        return (object)[
            'name' => $file->hashName(),
            'path' => $fullPath,
            'url' => $fullURL,
            'originName' => $file->getClientOriginalName(),
            'extension' => $file->getClientOriginalExtension(),
            'size' => $file->getSize(),
            'isImage' => $imageInfo->answer,
            'imageWidth' => $imageInfo->width,
            'imageHeight' => $imageInfo->height,
            'imageType' => $imageInfo->type
        ];
    }

    /*
     * 이미지 크기 조정
     */
    public function resize(string $filePath, int $width, int $height): void
    {
        [$w, $h, $ext] = getimagesize($filePath);
        $ratio = max($width / $w, $height / $h);
        $h = ceil($height / $ratio);
        $x = ($w - $width / $ratio) / 2;
        $w = ceil($width / $ratio);

        $imgString = file_get_contents($filePath);
        $image = imagecreatefromstring($imgString);
        $tmp = imagecreatetruecolor($width, $height);
        imagecopyresampled($tmp, $image, 0, 0, $x, 0, $width, $height, $w, $h);

        switch ($ext) {
            case IMAGETYPE_JPEG:
                imagejpeg($tmp, $filePath, 100);
                break;
            case IMAGETYPE_PNG:
                imagepng($tmp, $filePath, 0);
                break;
            case IMAGETYPE_GIF:
                imagegif($tmp, $filePath);
                break;
            default:
                break;
        }

        imagedestroy($image);
        imagedestroy($tmp);
    }

    /*
     * mime_content_type 값의 확장자 반환
     */
    public function getMimeExtension(string $imageType): string
    {
        return ([
            'image/bmp' => 'bmp',
            'image/cis-cod' => 'cod',
            'image/gif' => 'gif',
            'image/ief' => 'ief',
            'image/jpeg' => 'jpeg',
            'image/pipeg' => 'jfif',
            'image/tiff' => 'tif',
            'image/x-cmu-raster' => 'ras',
            'image/x-cmx' => 'cmx',
            'image/x-icon' => 'ico',
            'image/x-portable-anymap' => 'pnm',
            'image/x-portable-bitmap' => 'pbm',
            'image/x-portable-graymap' => 'pgm',
            'image/x-portable-pixmap' => 'ppm',
            'image/x-rgb' => 'rgb',
            'image/x-xbitmap' => 'xbm',
            'image/x-xpixmap' => 'xpm',
            'image/x-xwindowdump' => 'xwd',
            'image/png' => 'png',
            'image/x-jps' => 'jps',
            'image/x-freehand' => 'fh'
        ][$imageType] ?? '');
    }

    /*
     * Image type 반환
     */
    public function getImageType(int $imageType): string
    {
        return ([
            IMAGETYPE_UNKNOWN => 'UNKNOWN',
            IMAGETYPE_GIF => 'GIF',
            IMAGETYPE_JPEG => 'JPEG',
            IMAGETYPE_PNG => 'PNG',
            IMAGETYPE_SWF => 'SWF',
            IMAGETYPE_PSD => 'PSD',
            IMAGETYPE_BMP => 'BMP',
            IMAGETYPE_TIFF_II => 'TIFF_II',
            IMAGETYPE_TIFF_MM => 'TIFF_MM',
            IMAGETYPE_JPC => 'JPC',
            IMAGETYPE_JP2 => 'JP2',
            IMAGETYPE_JPX => 'JPX',
            IMAGETYPE_JB2 => 'JB2',
            IMAGETYPE_SWC => 'SWC',
            IMAGETYPE_IFF => 'IFF',
            IMAGETYPE_WBMP => 'WBMP',
            IMAGETYPE_XBM => 'XBM',
            IMAGETYPE_ICO => 'ICO',
            IMAGETYPE_WEBP => 'WEBP',
            IMAGETYPE_AVIF => 'AVIF',
            IMAGETYPE_COUNT => 'COUNT'
        ][$imageType] ?? '');
    }

    /*
     * Image Info 반환
     */
    public function getImageInfo(string $path): object
    {
        $ret = (object)[
            'answer' => $this->isImage($path),
            'name' => basename($path),
            'size' => filesize($path),
            'type' => null,
            'width' => 0,
            'height' => 0,
            'ext' => null
        ];

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

        if($ret->answer) {
            [$ret->width, $ret->height, $ret->ext] = getimagesize($path);
            $ret->type = $this->getImageType($ret->ext);
        }

        return $ret;
    }

    /*
     * 파일 삭제
     */
    public function remove(string $path): bool
    {
        if (file_exists($path)) {
            return unlink($path);
        }
        $path = public_path($path);
        if (file_exists($path)) {
            return unlink($path);
        }
        return false;
    }

    /*
     * 본문 내용에 없는 이미지를 삭제한다.
     */
    public function clear(array $images, string $uploadType, string|null $addPath = null): void
    {
        $images = array_map(function ($src) {
            return last(explode(DIRECTORY_SEPARATOR, $src));
        }, $images);

        $storagePath = $this->getStoragePath($uploadType, $addPath);

        if (is_dir($storagePath)) {
            foreach (scandir($storagePath) as $file) {
                if (in_array($file, ['.', '..', 'index.php'])) {
                    continue;
                }

                if (!in_array($file, $images)) { // 에디터에 없는 이미지면 삭제
                    $filePath = ($storagePath . DIRECTORY_SEPARATOR . $file);
                    if (is_dir($filePath)) {
                        continue;
                    }

                    unlink($filePath);
                }
            }

            // 이미지가 없으면 디렉토리 삭제
            if (count($images) <= 0) {
                if(unlink($storagePath . DIRECTORY_SEPARATOR . 'index.php')) {
                    rmdir($storagePath);
                }
            }
        }

        unset($images, $uploadType, $addPath, $storagePath);
    }

    /*
     * 이미지 정보 반환
     */
    public function getImageFromContent(?string $content): array
    {
        $dom = new DOMDocument('1.0', 'UTF-8');
        libxml_use_internal_errors(true);
        $dom->loadHTML('<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>' . $content);
        libxml_clear_errors();

        $images = [];
        foreach($dom->getElementsByTagName('img') as $img) {
            $src = canonicalizePath($img->attributes->getNamedItem("src")?->value);
            $origin = $img->attributes->getNamedItem("alt")?->value;
            $path = public_path($src);

            if(!file_exists($path)) {
                continue;
            }

            $imageInfo = $this->getImageInfo($path);

            $images[] = (object)[
                'origin' => $origin,
                'name' => $imageInfo->name,
                'path' => $path,
                'url' => $src,
                'size' => $imageInfo->size,
                'type' => $imageInfo->type,
                'width' => $imageInfo->width,
                'height' => $imageInfo->height
            ];
        }

        return $images;
    }
}
