<?php

namespace Acms\Services\Image\Engine;

use Acms\Services\Image\Contracts\ImageEngine;
use Acms\Services\Facades\PublicStorage;
use Imagick;
use ImagickPixel;
use RuntimeException;

class ImagickEngine extends ImageEngine
{
    /**
     * 画像を編集（リサイズ・回転）して書き出す
     *
     * @param string $srcPath
     * @param string $destPath
     * @param int|null $width
     * @param int|null $height
     * @param int|null $size
     * @param int|null $angle
     * @return void
     */
    public function editImage(string $srcPath, string $destPath, ?int $width = null, ?int $height = null, ?int $size = null, ?int $angle = null): void
    {
        // 元画像を読み込み
        $imagick = $this->loadImagickFromPath($srcPath);
        // EXIF Orientation をピクセルへ適用し、以降の寸法計算を正しい向き基準で行う
        $imagick->autoOrient();
        $imageprops = $imagick->getImageGeometry();
        $srcWidth = $imageprops['width'];
        $srcHeight = $imageprops['height'];
        // 出力サイズなど計算
        [$srcWidth, $srcHeight, $x, $y, $column, $rows] = $this->computeEditMetrics($srcWidth, $srcHeight, $width, $height, $size);
        // リサイズ・回転して保存（向き補正済みの Imagick インスタンスをそのまま渡す。再読み込みしない）
        $this->doResize($imagick, $srcPath, $destPath, $srcWidth, $srcHeight, $x, $y, $column, $rows, 0, 0, $column, $rows, [255, 255, 255], $angle);
    }

    /**
     * 画像を複製
     *
     * @param string $srcPath
     * @param string $destPath
     * @param string|null $format
     * @return void
     */
    public function copyImage(string $srcPath, string $destPath, ?string $format = null): void
    {
        $mimeType = $this->getMimeType($srcPath);
        if (is_null($mimeType)) {
            throw new RuntimeException(sprintf('Failed to get MIME type from %s.', $srcPath));
        }
        $srcFormat = $this->mimeTypeToExtension($mimeType);
        $normalize = fn($f) => $f === 'jpeg' ? 'jpg' : $f;
        $formatChanges = $format && $normalize($srcFormat) !== $normalize($format);
        // EXIF Orientation を持つ画像はピクセルへ焼き込むため再エンコードが必要
        $needsOrientation = $this->needsOrientationCorrection($srcPath);

        if ($formatChanges || $needsOrientation) {
            $imagick = $this->loadImagickFromPath($srcPath);
            // EXIF Orientation をピクセルへ適用し向きを正規化する
            $imagick->autoOrient();
            if ($formatChanges) {
                if ($normalize($format) === 'webp') {
                    // グレースケールや深い色深度の画像でも libwebp がエンコードできるよう正規化する
                    $this->normalizeImageForWebp($imagick);
                } else {
                    $imagick->setImageFormat($format); // フォーマットだけ変更（圧縮設定はしない）
                }
            }
            $this->outputImage($imagick, $destPath);
            return;
        }
        // 向き補正もフォーマット変更も不要 → 無劣化コピー
        if ($this->shouldReadFromLocalStorage($srcPath)) {
            if ($content = file_get_contents($srcPath)) {
                PublicStorage::put($destPath, $content);
            }
        } else {
            PublicStorage::copy($srcPath, $destPath);
        }
        $this->optimize($destPath);
    }

    /**
     * Imagick の autoOrient はネイティブに EXIF を扱うため、exif 拡張の有無に関わらず常に true
     *
     * @return bool
     */
    public function supportsExifOrientation(): bool
    {
        return true;
    }

    /**
     * 画像が EXIF Orientation による向き補正を要するか、ヘッダのみ読んで（デコードせず）判定する。
     *
     * @param string $path
     * @return bool
     */
    protected function needsOrientationCorrection(string $path): bool
    {
        // TOPLEFT(1) / UNDEFINED(0) 以外（2〜8）は回転・反転が必要
        return $this->readImageOrientation($path) >= 2;
    }

    /**
     * 画像パスから EXIF Orientation（0〜8）を、ヘッダのみ読んで（デコードせず）取得する。
     *
     * 取得に失敗した場合は 0（UNDEFINED）を返す。
     *
     * @param string $path
     * @return int
     */
    protected function readImageOrientation(string $path): int
    {
        try {
            $probe = new Imagick();
            if ($this->shouldReadFromLocalStorage($path)) {
                $probe->pingImage($path);
            } else {
                $imageData = PublicStorage::get($path);
                if ($imageData === false) {
                    return 0;
                }
                $probe->pingImageBlob($imageData);
            }
            $orientation = $probe->getImageOrientation();
            $probe->clear();
            $probe->destroy();
            return $orientation;
        } catch (\Throwable $e) {
            return 0;
        }
    }

    /**
     * WebP画像として画像を複製
     *
     * @param string $srcPath
     * @param string $destPath
     * @return void
     */
    public function copyImageAsWebp(string $srcPath, string $destPath): void
    {
        if (!$this->isWebpSupported()) {
            return;
        }
        $mimeType = $this->getMimeType($srcPath);
        if (!in_array($this->detectImageExtenstion($mimeType), ['png', 'jpg'], true)) {
            return;
        }
        $this->copyImage($srcPath, $destPath, 'webp');
    }

    /**
     * Imagick オブジェクトを WebP 出力用に正規化する
     *
     * グレースケール（Colorspace: Gray）や深いビット深度の画像をそのまま WebP に
     * エンコードすると、一部の libwebp 環境で getImageBlob() が空を返し
     * 「Failed to get the image contents」例外になる。色空間を sRGB に、画像タイプを
     * TrueColor（アルファがあれば TrueColorMatte）に、色深度を 8bit に揃えてから
     * フォーマットと画質を設定することで、どの画像でも安定して WebP 出力できるようにする。
     *
     * @param Imagick $imagick
     * @return void
     */
    private function normalizeImageForWebp(Imagick $imagick): void
    {
        if ($imagick->getImageColorspace() !== Imagick::COLORSPACE_SRGB) {
            $imagick->transformImageColorspace(Imagick::COLORSPACE_SRGB);
        }
        $imagick->setImageType(
            $imagick->getImageAlphaChannel() ? Imagick::IMGTYPE_TRUECOLORMATTE : Imagick::IMGTYPE_TRUECOLOR
        );
        $imagick->setImageDepth(8);
        $imagick->setImageFormat('webp');
        $imagick->setImageCompressionQuality($this->getImageQuality());
    }

    /**
     * 画像をリサイズ
     *
     * ImageResize など、getOrientedSize() で取得した向き補正後の寸法を基準に
     * src/dest/canvas を計算した外部コードから直接呼ばれる公開APIである。
     * 呼び出し元の寸法計算と整合させるため、内部で EXIF Orientation を適用する。
     *
     * @param string $srcPath
     * @param string $destPath
     * @param int $srcWidth
     * @param int $srcHeight
     * @param int $srcX
     * @param int $srcY
     * @param int $destWidth
     * @param int $destHeight
     * @param int $destX
     * @param int $destY
     * @param int $canvasWidth
     * @param int $canvasHeight
     * @param array{0: int, 1: int, 2: int} $color
     * @param int|null $angle
     * @return void
     */
    public function resizeImage(string $srcPath, string $destPath, int $srcWidth, int $srcHeight, int $srcX, int $srcY, int $destWidth, int $destHeight, int $destX, int $destY, int $canvasWidth, int $canvasHeight, array $color, ?int $angle = null): void
    {
        $imagick = $this->loadImagickFromPath($srcPath);
        // EXIF Orientation をピクセルへ適用し向きを正規化する。この後 stripImage() で
        // EXIF を消しても正しい向きが維持される。呼び出し元は getOrientedSize() で
        // 取得した向き補正後の寸法を渡す前提。
        $imagick->autoOrient();
        $this->doResize($imagick, $srcPath, $destPath, $srcWidth, $srcHeight, $srcX, $srcY, $destWidth, $destHeight, $destX, $destY, $canvasWidth, $canvasHeight, $color, $angle);
    }

    /**
     * リサイズ・回転して書き出す実処理
     *
     * $imagick は呼び出し側で向きが確定済みであることを前提とする
     * （本メソッドでは EXIF Orientation の適用を行わない。stripImage() で
     * EXIF を消しても、呼び出し側で autoOrient 済みなら正しい向きが維持される）。
     *
     * @param Imagick $imagick
     * @param string $srcPath
     * @param string $destPath
     * @param int $srcWidth
     * @param int $srcHeight
     * @param int $srcX
     * @param int $srcY
     * @param int $destWidth
     * @param int $destHeight
     * @param int $destX
     * @param int $destY
     * @param int $canvasWidth
     * @param int $canvasHeight
     * @param array{0: int, 1: int, 2: int} $color
     * @param int|null $angle
     * @return void
     */
    private function doResize(Imagick $imagick, string $srcPath, string $destPath, int $srcWidth, int $srcHeight, int $srcX, int $srcY, int $destWidth, int $destHeight, int $destX, int $destY, int $canvasWidth, int $canvasHeight, array $color, ?int $angle = null): void
    {
        // 画像形式
        $mimeType = $this->getMimeType($srcPath);
        if (is_null($mimeType)) {
            throw new RuntimeException(sprintf('Failed to get MIME type from %s.', $srcPath));
        }
        // 出力フォーマット（convert_2webp=on のときは jpg/png をリサイズ画像も含め webp として書き出す。GdEngine と挙動を揃える）
        $format = $this->detectImageExtenstion($mimeType);
        // 背景色
        /** @var array{0:int,1:int,2:int} $color */
        [$red, $green, $blue] = $color;
        $red = max(0, min(255, $red));
        $green = max(0, min(255, $green));
        $blue = max(0, min(255, $blue));
        // もしアニメGIFなら → 変換せずにそのままコピー
        if ($format === 'gif' && $imagick->getNumberImages() > 1) {
            PublicStorage::copy($srcPath, $destPath);
            return;
        }
        // リニア色空間に変換
        $imagick->transformImageColorspace(Imagick::COLORSPACE_RGB);
        // 色深度を保持
        $depth = $imagick->getImageDepth();
        // クロップ
        $imagick->cropImage($srcWidth, $srcHeight, $srcX, $srcY);
        // 縮小前に軽くシャープ（プリシェーピング）
        $imagick->unsharpMaskImage(0, 0.3, 0.6, 0.02);
        // 縮小（縮小率が高い場合は段階的に縮小）
        $factor = 2; // 縮小ステップ
        while (
            $imagick->getImageWidth() / $factor > $destWidth &&
            $imagick->getImageHeight() / $factor > $destHeight
        ) {
            $imagick->resizeImage(
                intval($imagick->getImageWidth() / $factor),
                intval($imagick->getImageHeight() / $factor),
                Imagick::FILTER_LANCZOS,
                1
            );
        }
        $imagick->resizeImage($destWidth, $destHeight, Imagick::FILTER_LANCZOS, 1);
        // 背景色設定（透過 or RGB指定）
        if (in_array($format, ['gif', 'png', 'webp'], true)) {
            $imagick->setImageBackgroundColor(new ImagickPixel('transparent'));
        } else {
            $imagick->setImageBackgroundColor(new ImagickPixel("rgb($red, $green, $blue)"));
        }
        // キャンバス調整（上下 or 左右に余白）
        if ($destWidth === $canvasWidth) {
            // 横幅いっぱい → 上下余白
            $imagick->spliceImage(0, $destY, 0, 0);
            $imagick->spliceImage(0, $destY, 0, $destY + $destHeight);
        } else {
            // 縦幅いっぱい → 左右余白
            $imagick->spliceImage($destX, 0, 0, 0);
            $imagick->spliceImage($destX, 0, $destX + $destWidth, 0);
        }
        // 回転
        if ($angle) {
            $bg = in_array($format, ['png','gif','webp'], true) ? 'none' : "rgb($red, $green, $blue)";
            $imagick->rotateImage(new ImagickPixel($bg), -1 * $angle);
        }
        // メタデータ削除
        $imagick->stripImage();
        $imagick->profileImage('*', null);
        // 出力時に元の depth に合わせる
        $imagick->setImageDepth($depth);
        // リニア → sRGB に戻す
        $imagick->transformImageColorspace(Imagick::COLORSPACE_SRGB);
        // 圧縮
        if ($format === 'png') {
            $imagick->setImageFormat('png');
            $imagick->setOption('png:compression-level', '9');
            $imagick->setOption('png:compression-filter', '5');
            $imagick->setOption('png:compression-strategy', '1');
            $imagick->setOption('png:exclude-chunk', 'all');
        } elseif ($format === 'webp' && $this->isWebpSupported()) {
            // グレースケールや深い色深度でも libwebp が安定して出力できるよう正規化する
            $this->normalizeImageForWebp($imagick);
        } elseif ($format === 'jpg') {
            $imagick->setImageFormat('jpeg');
            $imagick->setImageCompression(Imagick::COMPRESSION_JPEG);
            $imagick->setImageCompressionQuality($this->getImageQuality());
            $imagick->setInterlaceScheme(Imagick::INTERLACE_JPEG);
        } elseif ($format === 'gif') {
            $imagick->setImageFormat('gif');
            $imagick->setImageCompression(Imagick::COMPRESSION_LZW);
        }
        $this->outputImage($imagick, $destPath);
    }

    /**
     * GDでWebP画像がサポートされているか判定
     *
     * @return bool
     */
    public function isWebpSupported(): bool
    {
        static $supported = null;

        if (isset($supported)) {
            return $supported;
        }
        if (!class_exists('Imagick') || config('webp_support') !== 'on') {
            return $supported = false;
        }
        $formats = array_map('strtolower', Imagick::queryFormats());
        $supported = in_array('webp', $formats, true);

        return $supported;
    }

    /**
     * 画像サイズを取得
     *
     * @param string $path
     * @return array{0: int, 1: int}
     */
    public function getSize(string $path): array
    {
        $imagick = $this->loadImagickFromPath($path);
        $imageprops = $imagick->getImageGeometry();
        $srcWidth = $imageprops['width'];
        $srcHeight = $imageprops['height'];
        $imagick->clear();
        $imagick->destroy();

        return [$srcWidth, $srcHeight];
    }

    /**
     * EXIF Orientation を適用した状態の画像サイズを取得する
     *
     * @param string $path
     * @return array{0: int, 1: int}
     */
    public function getOrientedSize(string $path): array
    {
        [$width, $height] = $this->getSize($path);
        $orientation = $this->readImageOrientation($path);
        if (in_array($orientation, [5, 6, 7, 8], true)) {
            return [$height, $width];
        }
        return [$width, $height];
    }

    /**
     * パスからImagickを生成
     *
     * @param string $path
     * @return Imagick
     */
    protected function loadImagickFromPath(string $path): Imagick
    {
        if ($this->shouldReadFromLocalStorage($path)) {
            return new Imagick($path);
        }
        $imageData = PublicStorage::get($path);
        if ($imageData === false) {
            throw new RuntimeException('Failed to load image data.');
        }
        $imagick = new Imagick();
        $imagick->readImageBlob($imageData);

        return $imagick;
    }

    /**
     * 書き出し
     *
     * @param Imagick $imagick
     * @param string $destPath
     * @return void
     */
    protected function outputImage(Imagick $imagick, string $destPath): void
    {
        if ($imageData = $imagick->getImageBlob()) {
            PublicStorage::put($destPath, $imageData);
            $this->optimize($destPath);
            PublicStorage::changeMod($destPath);
        }
        $imagick->clear();
        $imagick->destroy();
    }
}
