<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK IT ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006-2015 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: yunwuxin <448901948@qq.com>
// +----------------------------------------------------------------------

namespace think;

use think\image\Exception as ImageException;
use think\image\gif\Gif;

class Image
{
    /* 缩略图相关常量定义 */
    const THUMB_SCALING   = 1;
    const THUMB_FILLED    = 2;
    const THUMB_CENTER    = 3;
    const THUMB_NORTHWEST = 4;
    const THUMB_SOUTHEAST = 5;
    const THUMB_FIXED     = 6;

    /* 水印相关常量定义 */
    const WATER_NORTHWEST = 1;
    const WATER_NORTH     = 2;
    const WATER_NORTHEAST = 3;
    const WATER_WEST      = 4;
    const WATER_CENTER    = 5;
    const WATER_EAST      = 6;
    const WATER_SOUTHWEST = 7;
    const WATER_SOUTH     = 8;
    const WATER_SOUTHEAST = 9;

    /* 翻转相关常量定义 */
    const FLIP_X = 1;
    const FLIP_Y = 2;

    /**
     * 图像资源对象 (PHP 8.0+ 为 GdImage 对象)
     *
     * @var \GdImage|null
     */
    protected ?\GdImage $im = null;

    /** @var Gif|null */
    protected ?Gif $gif = null;

    /**
     * 图像信息，包括 width, height, type, mime, size
     *
     * @var array{width: int, height: int, type: string, mime: string}
     */
    protected array $info = [];

    /**
     * GD 函数白名单 - 用于安全调用
     */
    private const IMAGE_CREATE_FUNCTIONS = [
        'jpeg' => 'imagecreatefromjpeg',
        'jpg'  => 'imagecreatefromjpeg',
        'png'  => 'imagecreatefrompng',
        'gif'  => 'imagecreatefromgif',
        'webp' => 'imagecreatefromwebp',
        'bmp'  => 'imagecreatefrombmp',
    ];

    private const IMAGE_SAVE_FUNCTIONS = [
        'jpeg' => 'imagejpeg',
        'jpg'  => 'imagejpeg',
        'png'  => 'imagepng',
        'gif'  => 'imagegif',
        'webp' => 'imagewebp',
        'bmp'  => 'imagebmp',
    ];

    protected function __construct(\SplFileInfo $file)
    {
        $pathname = $file->getPathname();

        // 获取图像信息
        $info = @getimagesize($pathname);

        // 检测图像合法性
        if ($info === false || ($info[2] === IMAGETYPE_GIF && empty($info['bits']))) {
            throw new ImageException('Illegal image file');
        }

        // 设置图像信息
        $this->info = [
            'width'  => $info[0],
            'height' => $info[1],
            'type'   => image_type_to_extension($info[2], false),
            'mime'   => $info['mime'],
        ];

        // 打开图像
        if ($this->info['type'] === 'gif') {
            $this->gif = new Gif($pathname);
            $this->im = @imagecreatefromstring($this->gif->image());
        } else {
            $this->im = $this->createImageFromFile($pathname, $this->info['type']);
        }

        if ($this->im === null || $this->im === false) {
            throw new ImageException('Failed to create image resources!');
        }

    }

    /**
     * 安全地从文件创建图像资源
     *
     * @param string $pathname 文件路径
     * @param string $type 图像类型
     * @return \GdImage|false
     */
    private function createImageFromFile(string $pathname, string $type): \GdImage|false
    {
        $function = self::IMAGE_CREATE_FUNCTIONS[$type] ?? null;

        if ($function === null || !function_exists($function)) {
            return false;
        }

        return @$function($pathname);
    }

    /**
     * 打开一个图片文件
     * @param \SplFileInfo|string $file
     * @return Image
     */
    public static function open(\SplFileInfo|string $file): static
    {
        if (is_string($file)) {
            $file = new \SplFileInfo($file);
        }

        if (!$file->isFile()) {
            throw new ImageException('image file not exist');
        }

        return new self($file);
    }

    /**
     * 保存图像
     * @param string $pathname 图像保存路径名称
     * @param string|null $type 图像类型
     * @param int $quality 图像质量
     * @param bool $interlace 是否对JPEG类型图像设置隔行扫描
     * @return $this
     */
    public function save(string $pathname, ?string $type = null, int $quality = 80, bool $interlace = true): static
    {
        // 自动获取图像类型
        if ($type === null) {
            $type = $this->info['type'];
        } else {
            $type = strtolower($type);
        }

        // 保存图像
        match ($type) {
            'jpeg', 'jpg' => $this->saveJpeg($pathname, $quality, $interlace),
            'gif' => $this->saveGif($pathname),
            'png' => $this->savePng($pathname, $quality),
            default => $this->saveOther($pathname, $type),
        };

        return $this;
    }

    /**
     * 保存 JPEG 图像
     */
    private function saveJpeg(string $pathname, int $quality, bool $interlace): void
    {
        imageinterlace($this->im, $interlace);
        imagejpeg($this->im, $pathname, $quality);
    }

    /**
     * 保存 GIF 图像
     */
    private function saveGif(string $pathname): void
    {
        if (!empty($this->gif)) {
            $this->gif->save($pathname);
        }
    }

    /**
     * 保存 PNG 图像
     */
    private function savePng(string $pathname, int $quality): void
    {
        imagesavealpha($this->im, true);
        imagepng($this->im, $pathname, min(intdiv($quality, 10), 9));
    }

    /**
     * 保存其他格式图像
     */
    private function saveOther(string $pathname, string $type): void
    {
        $function = self::IMAGE_SAVE_FUNCTIONS[$type] ?? null;

        if ($function !== null && function_exists($function)) {
            $function($this->im, $pathname);
        } else {
            throw new ImageException("不支持的图像类型: {$type}");
        }
    }

    /**
     * 返回图像宽度
     * @return int 图像宽度
     */
    public function width(): int
    {
        return $this->info['width'];
    }

    /**
     * 返回图像高度
     * @return int 图像高度
     */
    public function height(): int
    {
        return $this->info['height'];
    }

    /**
     * 返回图像类型
     * @return string 图像类型
     */
    public function type(): string
    {
        return $this->info['type'];
    }

    /**
     * 返回图像MIME类型
     * @return string 图像MIME类型
     */
    public function mime(): string
    {
        return $this->info['mime'];
    }

    /**
     * 返回图像尺寸数组 0 - 图像宽度，1 - 图像高度
     * @return array{0: int, 1: int} 图像尺寸
     */
    public function size(): array
    {
        return [$this->info['width'], $this->info['height']];
    }

    /**
     * 旋转图像
     * @param int $degrees 顺时针旋转的度数
     * @return $this
     */
    public function rotate(int $degrees = 90): static
    {
        do {
            $backgroundColor = imagecolorallocatealpha($this->im, 0, 0, 0, 127);
            $img = imagerotate($this->im, -$degrees, $backgroundColor);

            if ($img === false) {
                throw new ImageException('图像旋转失败');
            }

            imagedestroy($this->im);
            $this->im = $img;
        } while (!empty($this->gif) && $this->gifNext());

        $this->info['width']  = imagesx($this->im);
        $this->info['height'] = imagesy($this->im);

        return $this;
    }

    /**
     * 翻转图像
     * @param int $direction 翻转轴,X或者Y
     * @return $this
     */
    public function flip(int $direction = self::FLIP_X): static
    {
        $w = $this->info['width'];
        $h = $this->info['height'];

        do {
            $img = imagecreatetruecolor($w, $h);

            if ($img === false) {
                throw new ImageException('创建图像失败');
            }

            match ($direction) {
                self::FLIP_X => $this->flipX($img, $w, $h),
                self::FLIP_Y => $this->flipY($img, $w, $h),
                default => throw new ImageException('不支持的翻转类型'),
            };

            imagedestroy($this->im);
            $this->im = $img;
        } while (!empty($this->gif) && $this->gifNext());

        return $this;
    }

    /**
     * X轴翻转
     */
    private function flipX(\GdImage $img, int $w, int $h): void
    {
        for ($y = 0; $y < $h; $y++) {
            imagecopy($img, $this->im, 0, $h - $y - 1, 0, $y, $w, 1);
        }
    }

    /**
     * Y轴翻转
     */
    private function flipY(\GdImage $img, int $w, int $h): void
    {
        for ($x = 0; $x < $w; $x++) {
            imagecopy($img, $this->im, $w - $x - 1, 0, $x, 0, 1, $h);
        }
    }

    /**
     * 裁剪图像
     *
     * @param int $w 裁剪区域宽度
     * @param int $h 裁剪区域高度
     * @param int $x 裁剪区域x坐标
     * @param int $y 裁剪区域y坐标
     * @param int|null $width 图像保存宽度
     * @param int|null $height 图像保存高度
     *
     * @return $this
     */
    public function crop(int $w, int $h, int $x = 0, int $y = 0, ?int $width = null, ?int $height = null): static
    {

        // 设置保存尺寸
        $width ??= $w;
        $height ??= $h;

        do {
            // 创建新图像
            $img = imagecreatetruecolor($width, $height);

            if ($img === false) {
                throw new ImageException('创建图像失败');
            }

            // 调整默认颜色
            $color = imagecolorallocate($img, 255, 255, 255);
            imagefill($img, 0, 0, $color);

            // 裁剪
            imagecopyresampled($img, $this->im, 0, 0, $x, $y, $width, $height, $w, $h);
            imagedestroy($this->im);

            // 设置新图像
            $this->im = $img;
        } while (!empty($this->gif) && $this->gifNext());

        $this->info['width']  = $width;
        $this->info['height'] = $height;

        return $this;
    }

    /**
     * 生成缩略图
     *
     * @param int $width 缩略图最大宽度
     * @param int $height 缩略图最大高度
     * @param int $type 缩略图裁剪类型
     *
     * @return $this
     */
    public function thumb(int $width, int $height, int $type = self::THUMB_SCALING): static
    {
        $w = $this->info['width'];
        $h = $this->info['height'];

        // 计算缩略图参数
        [$cropW, $cropH, $cropX, $cropY, $thumbW, $thumbH] = match ($type) {
            self::THUMB_SCALING => $this->calcThumbScaling($w, $h, $width, $height),
            self::THUMB_CENTER => $this->calcThumbCenter($w, $h, $width, $height),
            self::THUMB_NORTHWEST => $this->calcThumbNorthwest($w, $h, $width, $height),
            self::THUMB_SOUTHEAST => $this->calcThumbSoutheast($w, $h, $width, $height),
            self::THUMB_FILLED => $this->calcThumbFilled($w, $h, $width, $height),
            self::THUMB_FIXED => [0, 0, 0, 0, $width, $height],
            default => throw new ImageException('不支持的缩略图裁剪类型'),
        };

        // THUMB_FILLED 特殊处理
        if ($type === self::THUMB_FILLED) {
            return $this;
        }

        // 裁剪图像
        return $this->crop($cropW, $cropH, $cropX, $cropY, $thumbW, $thumbH);
    }

    /**
     * 计算等比例缩放参数
     */
    private function calcThumbScaling(int $w, int $h, int $width, int $height): array
    {
        // 原图尺寸小于缩略图尺寸则不进行缩略
        if ($w < $width && $h < $height) {
            return [$w, $h, 0, 0, $w, $h];
        }

        $scale = min($width / $w, $height / $h);
        return [$w, $h, 0, 0, (int)($w * $scale), (int)($h * $scale)];
    }

    /**
     * 计算居中裁剪参数
     */
    private function calcThumbCenter(int $w, int $h, int $width, int $height): array
    {
        $scale = max($width / $w, $height / $h);
        $cropW = $width / $scale;
        $cropH = $height / $scale;
        $x = ($this->info['width'] - $cropW) / 2;
        $y = ($this->info['height'] - $cropH) / 2;

        return [(int)$cropW, (int)$cropH, (int)$x, (int)$y, $width, $height];
    }

    /**
     * 计算左上角裁剪参数
     */
    private function calcThumbNorthwest(int $w, int $h, int $width, int $height): array
    {
        $scale = max($width / $w, $height / $h);
        $cropW = $width / $scale;
        $cropH = $height / $scale;

        return [(int)$cropW, (int)$cropH, 0, 0, $width, $height];
    }

    /**
     * 计算右下角裁剪参数
     */
    private function calcThumbSoutheast(int $w, int $h, int $width, int $height): array
    {
        $scale = max($width / $w, $height / $h);
        $cropW = $width / $scale;
        $cropH = $height / $scale;
        $x = $this->info['width'] - $cropW;
        $y = $this->info['height'] - $cropH;

        return [(int)$cropW, (int)$cropH, (int)$x, (int)$y, $width, $height];
    }

    /**
     * 计算填充裁剪参数
     */
    private function calcThumbFilled(int $w, int $h, int $width, int $height): array
    {
        $scale = ($w < $width && $h < $height) ? 1 : min($width / $w, $height / $h);
        $newW = (int)($w * $scale);
        $newH = (int)($h * $scale);
        $posX = (int)(($width - $newW) / 2);
        $posY = (int)(($height - $newH) / 2);

        do {
            $img = imagecreatetruecolor($width, $height);

            if ($img === false) {
                throw new ImageException('创建图像失败');
            }

            $color = imagecolorallocate($img, 255, 255, 255);
            imagefill($img, 0, 0, $color);
            imagecopyresampled($img, $this->im, $posX, $posY, 0, 0, $newW, $newH, $w, $h);
            imagedestroy($this->im);
            $this->im = $img;
        } while (!empty($this->gif) && $this->gifNext());

        $this->info['width']  = $width;
        $this->info['height'] = $height;

        return [0, 0, 0, 0, 0, 0];
    }

    /**
     * 添加水印
     *
     * @param string $source 水印图片路径
     * @param int $locate 水印位置
     * @param int $alpha 透明度
     * @return $this
     */
    public function water(string $source, int $locate = self::WATER_SOUTHEAST, int $alpha = 100): static
    {
        if (!is_file($source)) {
            throw new ImageException('水印图像不存在');
        }

        // 获取水印图像信息
        $info = getimagesize($source);
        if ($info === false || ($info[2] === IMAGETYPE_GIF && empty($info['bits']))) {
            throw new ImageException('非法水印文件');
        }

        // 创建水印图像资源
        $waterType = image_type_to_extension($info[2], false);
        $water = $this->createImageFromFile($source, $waterType);

        if ($water === false) {
            throw new ImageException('创建水印图像失败');
        }

        // 设定水印图像的混色模式
        imagealphablending($water, true);

        // 设定水印位置
        [$x, $y] = $this->calcWaterPosition($locate, $info);

        do {
            $src = imagecreatetruecolor($info[0], $info[1]);

            if ($src === false) {
                imagedestroy($water);
                throw new ImageException('创建临时图像失败');
            }

            $color = imagecolorallocate($src, 255, 255, 255);
            imagefill($src, 0, 0, $color);
            imagecopy($src, $this->im, 0, 0, $x, $y, $info[0], $info[1]);
            imagecopy($src, $water, 0, 0, 0, 0, $info[0], $info[1]);
            imagecopymerge($this->im, $src, $x, $y, 0, 0, $info[0], $info[1], $alpha);
            imagedestroy($src);
        } while (!empty($this->gif) && $this->gifNext());

        imagedestroy($water);
        return $this;
    }

    /**
     * 计算水印位置
     */
    private function calcWaterPosition(int $locate, array $info): array
    {
        return match ($locate) {
            self::WATER_SOUTHEAST => [
                $this->info['width'] - $info[0],
                $this->info['height'] - $info[1]
            ],
            self::WATER_SOUTHWEST => [
                0,
                $this->info['height'] - $info[1]
            ],
            self::WATER_NORTHWEST => [0, 0],
            self::WATER_NORTHEAST => [
                $this->info['width'] - $info[0],
                0
            ],
            self::WATER_CENTER => [
                (int)(($this->info['width'] - $info[0]) / 2),
                (int)(($this->info['height'] - $info[1]) / 2)
            ],
            self::WATER_SOUTH => [
                (int)(($this->info['width'] - $info[0]) / 2),
                $this->info['height'] - $info[1]
            ],
            self::WATER_EAST => [
                $this->info['width'] - $info[0],
                (int)(($this->info['height'] - $info[1]) / 2)
            ],
            self::WATER_NORTH => [
                (int)(($this->info['width'] - $info[0]) / 2),
                0
            ],
            self::WATER_WEST => [
                0,
                (int)(($this->info['height'] - $info[1]) / 2)
            ],
            default => is_array($locate) ? $locate : throw new ImageException('不支持的水印位置类型'),
        };
    }

    /**
     * 图像添加文字
     *
     * @param string $text 添加的文字
     * @param string $font 字体路径
     * @param int $size 字号
     * @param string|array $color 文字颜色
     * @param int $locate 文字写入位置
     * @param int|array $offset 文字相对当前位置的偏移量
     * @param int $angle 文字倾斜角度
     *
     * @return $this
     * @throws ImageException
     */
    public function text(
        string $text,
        string $font,
        int $size,
        string|array $color = '#00000000',
        int $locate = self::WATER_SOUTHEAST,
        int|array $offset = 0,
        int $angle = 0
    ): static {
        if (!is_file($font)) {
            throw new ImageException("不存在的字体文件：{$font}");
        }

        // 获取文字信息
        $box = imagettfbbox($size, $angle, $font, $text);
        if ($box === false) {
            throw new ImageException('计算文字边界框失败');
        }

        $minX = min($box[0], $box[2], $box[4], $box[6]);
        $maxX = max($box[0], $box[2], $box[4], $box[6]);
        $minY = min($box[1], $box[3], $box[5], $box[7]);
        $maxY = max($box[1], $box[3], $box[5], $box[7]);

        // 计算文字初始坐标和尺寸
        $x = $minX;
        $y = abs($minY);
        $w = $maxX - $minX;
        $h = $maxY - $minY;

        // 设定文字位置
        [$x, $y] = $this->calcTextPosition($locate, $x, $y, $w, $h);

        // 设置偏移量
        [$ox, $oy] = is_array($offset) ?
            [intval($offset[0]), intval($offset[1])] :
            [intval($offset), intval($offset)];

        // 设置颜色
        $colorArray = $this->parseColor($color);

        do {
            $col = imagecolorallocatealpha($this->im, ...$colorArray);
            imagettftext($this->im, $size, $angle, $x + $ox, $y + $oy, $col, $font, $text);
        } while (!empty($this->gif) && $this->gifNext());

        return $this;
    }

    /**
     * 计算文字位置
     */
    private function calcTextPosition(int $locate, int $x, int $y, int $w, int $h): array
    {
        return match ($locate) {
            self::WATER_SOUTHEAST => [
                $x + $this->info['width'] - $w,
                $y + $this->info['height'] - $h
            ],
            self::WATER_SOUTHWEST => [
                $x,
                $y + $this->info['height'] - $h
            ],
            self::WATER_NORTHWEST => [$x, $y],
            self::WATER_NORTHEAST => [
                $x + $this->info['width'] - $w,
                $y
            ],
            self::WATER_CENTER => [
                $x + (int)(($this->info['width'] - $w) / 2),
                $y + (int)(($this->info['height'] - $h) / 2)
            ],
            self::WATER_SOUTH => [
                $x + (int)(($this->info['width'] - $w) / 2),
                $y + $this->info['height'] - $h
            ],
            self::WATER_EAST => [
                $x + $this->info['width'] - $w,
                $y + (int)(($this->info['height'] - $h) / 2)
            ],
            self::WATER_NORTH => [
                $x + (int)(($this->info['width'] - $w) / 2),
                $y
            ],
            self::WATER_WEST => [
                $x,
                $y + (int)(($this->info['height'] - $h) / 2)
            ],
            default => is_array($locate) ? [
                $x + intval($locate[0]),
                $y + intval($locate[1])
            ] : throw new ImageException('不支持的文字位置类型'),
        };
    }

    /**
     * 解析颜色值
     * @return array{0: int, 1: int, 2: int, 3: int}
     */
    private function parseColor(string|array $color): array
    {
        if (is_string($color) && str_starts_with($color, '#')) {
            $parts = str_split(substr($color, 1), 2);
            $rgba = array_map('hexdec', $parts);

            // 确保有4个通道
            while (count($rgba) < 4) {
                $rgba[] = 0;
            }

            // Alpha 通道处理
            if ($rgba[3] > 127) {
                $rgba[3] = 0;
            }

            return $rgba;
        }

        if (is_array($color) && count($color) >= 3) {
            $rgba = [
                intval($color[0]),
                intval($color[1]),
                intval($color[2]),
                intval($color[3] ?? 0)
            ];

            if ($rgba[3] > 127) {
                $rgba[3] = 0;
            }

            return $rgba;
        }

        throw new ImageException('错误的颜色值');
    }

    /**
     * 切换到GIF的下一帧并保存当前帧
     */
    protected function gifNext(): string|false
    {
        ob_start();
        ob_implicit_flush(false);
        imagegif($this->im);
        $img = ob_get_clean();

        if ($img === false) {
            return false;
        }

        $this->gif->image($img);
        $next = $this->gif->nextImage();

        if ($next) {
            imagedestroy($this->im);
            $this->im = imagecreatefromstring($next);
            return $next;
        }

        imagedestroy($this->im);
        $this->im = imagecreatefromstring($this->gif->image());
        return false;
    }

    /**
     * 析构方法，用于销毁图像资源
     */
    public function __destruct()
    {
        if ($this->im !== null) {
            imagedestroy($this->im);
        }
    }
}
