<?php

declare(strict_types=1);

namespace App\Support\Uploads;

use Filament\Forms\Components\BaseFileUpload;
use Illuminate\Contracts\Filesystem\Filesystem;
use Illuminate\Support\Facades\Storage;
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;

final class ImageVariants
{
    public const LARGE_WIDTH = 1200;
    public const MEDIUM_WIDTH = 600;
    public const THUMB_96_SIZE = 96;

    private const MEDIUM_SUFFIX = '@600w';
    private const THUMB_96_SUFFIX = '@96x96';

    public static function store(BaseFileUpload $component, TemporaryUploadedFile $file, bool $createThumb96 = false): ?string
    {
        $diskName = $component->getDiskName();
        $directory = trim((string) $component->getDirectory(), '/');
        $visibility = $component->getVisibility();

        $uploadedFileName = $component->getUploadedFileNameForStorage($file);
        $basePath = trim($directory . '/' . $uploadedFileName, '/');

        $extension = strtolower(pathinfo($uploadedFileName, PATHINFO_EXTENSION));

        if ($extension === '' || in_array($extension, ['svg', 'gif'], true)) {
            $storeMethod = $visibility === 'public' ? 'storePubliclyAs' : 'storeAs';

            return $file->{$storeMethod}(
                $directory,
                $uploadedFileName,
                $diskName,
            );
        }

        $sourceBinary = $file->get();
        $sourceImage = @imagecreatefromstring($sourceBinary);

        if ($sourceImage === false) {
            $storeMethod = $visibility === 'public' ? 'storePubliclyAs' : 'storeAs';

            return $file->{$storeMethod}(
                $directory,
                $uploadedFileName,
                $diskName,
            );
        }

        $sourceImage = self::orientFromExifIfPossible($file->getRealPath(), $sourceImage, $extension);

        try {
            $largeImage = self::resizeToMaxWidth($sourceImage, self::LARGE_WIDTH, $extension);
            $largeBinary = self::encode($largeImage, $extension);

            if ($largeBinary === null) {
                $storeMethod = $visibility === 'public' ? 'storePubliclyAs' : 'storeAs';

                return $file->{$storeMethod}(
                    $directory,
                    $uploadedFileName,
                    $diskName,
                );
            }

            Storage::disk($diskName)->put($basePath, $largeBinary, $visibility);

            $mediumPath = self::mediumPath($basePath);
            $mediumImage = self::resizeToMaxWidth($sourceImage, self::MEDIUM_WIDTH, $extension);
            $mediumBinary = self::encode($mediumImage, $extension);
            if (($mediumBinary !== null) && filled($mediumPath)) {
                Storage::disk($diskName)->put($mediumPath, $mediumBinary, $visibility);
            }

            if ($createThumb96) {
                $thumbPath = self::thumb96Path($basePath);
                $thumbImage = self::centerCropSquareAndResize($sourceImage, self::THUMB_96_SIZE, $extension);
                $thumbBinary = self::encode($thumbImage, $extension);
                if (($thumbBinary !== null) && filled($thumbPath)) {
                    Storage::disk($diskName)->put($thumbPath, $thumbBinary, $visibility);
                }
            }

            return $basePath;
        } finally {
            if (is_resource($sourceImage) || $sourceImage instanceof \GdImage) {
                @imagedestroy($sourceImage);
            }
        }
    }

    public static function delete(BaseFileUpload $component, string $filePath, bool $hasThumb96 = false): void
    {
        if (blank($filePath)) {
            return;
        }

        $diskName = $component->getDiskName();
        $disk = Storage::disk($diskName);

        $paths = [
            $filePath,
            self::mediumPath($filePath),
        ];

        if ($hasThumb96) {
            $paths[] = self::thumb96Path($filePath);
        }

        $paths = array_values(array_filter($paths, static fn (?string $path): bool => filled($path)));
        if ($paths === []) {
            return;
        }

        $disk->delete($paths);
    }

    public static function largeUrl(?string $path, string $diskName = 'public'): ?string
    {
        if (blank($path)) {
            return null;
        }

        /** @var \Illuminate\Filesystem\FilesystemAdapter $disk */
        $disk = Storage::disk($diskName);

        return $disk->url($path);
    }

    public static function mediumUrl(?string $path, string $diskName = 'public'): ?string
    {
        $preferredPath = self::preferredPath($path, variant: 'medium', diskName: $diskName);

        /** @var \Illuminate\Filesystem\FilesystemAdapter $disk */
        $disk = Storage::disk($diskName);

        return filled($preferredPath)
            ? $disk->url($preferredPath)
            : null;
    }

    public static function thumb96Url(?string $path, string $diskName = 'public'): ?string
    {
        $preferredPath = self::preferredPath($path, variant: 'thumb96', diskName: $diskName);

        /** @var \Illuminate\Filesystem\FilesystemAdapter $disk */
        $disk = Storage::disk($diskName);

        return filled($preferredPath)
            ? $disk->url($preferredPath)
            : null;
    }

    public static function preferredPath(?string $path, string $variant, string $diskName = 'public'): ?string
    {
        if (blank($path)) {
            return null;
        }

        $candidate = match ($variant) {
            'medium' => self::mediumPath($path),
            'thumb96' => self::thumb96Path($path),
            default => null,
        };

        if (blank($candidate)) {
            return $path;
        }

        $disk = Storage::disk($diskName);

        try {
            return $disk->exists($candidate) ? $candidate : $path;
        } catch (\Throwable) {
            return $path;
        }
    }

    public static function mediumPath(?string $path): ?string
    {
        return self::appendSuffixToPath($path, self::MEDIUM_SUFFIX);
    }

    public static function thumb96Path(?string $path): ?string
    {
        return self::appendSuffixToPath($path, self::THUMB_96_SUFFIX);
    }

    private static function appendSuffixToPath(?string $path, string $suffix): ?string
    {
        if (blank($path)) {
            return null;
        }

        $extension = pathinfo($path, PATHINFO_EXTENSION);
        if ($extension === '') {
            return null;
        }

        $withoutExtension = substr($path, 0, -1 * (strlen($extension) + 1));

        return $withoutExtension . $suffix . '.' . $extension;
    }

    /**
     * @return \GdImage|resource
     */
    private static function resizeToMaxWidth(\GdImage $sourceImage, int $targetWidth, string $extension): \GdImage
    {
        $sourceWidth = imagesx($sourceImage);
        $sourceHeight = imagesy($sourceImage);

        if ($sourceWidth <= 0 || $sourceHeight <= 0) {
            return $sourceImage;
        }

        $scale = min(1, $targetWidth / $sourceWidth);
        $destWidth = max(1, (int) round($sourceWidth * $scale));
        $destHeight = max(1, (int) round($sourceHeight * $scale));

        $destImage = self::createDestinationImage($destWidth, $destHeight, $extension);

        imagecopyresampled(
            $destImage,
            $sourceImage,
            0,
            0,
            0,
            0,
            $destWidth,
            $destHeight,
            $sourceWidth,
            $sourceHeight,
        );

        return $destImage;
    }

    /**
     * @return \GdImage|resource
     */
    private static function centerCropSquareAndResize(\GdImage $sourceImage, int $size, string $extension): \GdImage
    {
        $sourceWidth = imagesx($sourceImage);
        $sourceHeight = imagesy($sourceImage);

        $cropSize = min($sourceWidth, $sourceHeight);
        $sourceX = (int) floor(($sourceWidth - $cropSize) / 2);
        $sourceY = (int) floor(($sourceHeight - $cropSize) / 2);

        $destImage = self::createDestinationImage($size, $size, $extension);

        imagecopyresampled(
            $destImage,
            $sourceImage,
            0,
            0,
            $sourceX,
            $sourceY,
            $size,
            $size,
            $cropSize,
            $cropSize,
        );

        return $destImage;
    }

    /**
     * @return \GdImage|resource
     */
    private static function createDestinationImage(int $width, int $height, string $extension): \GdImage
    {
        $image = imagecreatetruecolor($width, $height);

        if (in_array($extension, ['png', 'webp'], true)) {
            imagealphablending($image, false);
            imagesavealpha($image, true);
            $transparent = imagecolorallocatealpha($image, 0, 0, 0, 127);
            imagefilledrectangle($image, 0, 0, $width, $height, $transparent);
        }

        return $image;
    }

    private static function encode(\GdImage $image, string $extension): ?string
    {
        $extension = strtolower($extension);

        ob_start();

        $ok = match ($extension) {
            'jpg', 'jpeg' => imagejpeg($image, null, 85),
            'png' => imagepng($image, null, 7),
            'webp' => function_exists('imagewebp') ? imagewebp($image, null, 82) : false,
            default => false,
        };

        $binary = $ok ? (string) ob_get_contents() : null;
        ob_end_clean();

        return filled($binary) ? $binary : null;
    }

    /**
     * @return \GdImage|resource
     */
    private static function orientFromExifIfPossible(string $realPath, \GdImage $image, string $extension): \GdImage
    {
        if (! in_array($extension, ['jpg', 'jpeg'], true)) {
            return $image;
        }

        if ($realPath === '' || ! function_exists('exif_read_data')) {
            return $image;
        }

        $exif = @exif_read_data($realPath);
        $orientation = is_array($exif) ? ($exif['Orientation'] ?? null) : null;

        if (! is_int($orientation)) {
            return $image;
        }

        $rotated = match ($orientation) {
            3 => imagerotate($image, 180, 0),
            6 => imagerotate($image, -90, 0),
            8 => imagerotate($image, 90, 0),
            default => false,
        };

        return ($rotated instanceof \GdImage) ? $rotated : $image;
    }
}
