<?php

namespace dynamicimages\traits;

trait dynamicimages_asset_t
{
    /**
     * In-memory Imagick instance for this asset (lazy-loaded)
     */
    private ?\Imagick $memoryImage = null;

    /**
     * Ensure the in-memory image is loaded from disk.
     */
    private function ensureMemoryImageLoaded(): void
    {
        if ($this->memoryImage instanceof \Imagick) {
            return;
        }
        if (!isset($this->path) || !is_readable($this->path)) {
            throw new \RuntimeException('Asset path is not readable: ' . ($this->path ?? '(unset)'));
        }
        $img = new \Imagick();
        $img->readImage($this->path);
        // Normalize to RGBA with alpha channel enabled
        $img->setImageAlphaChannel(\Imagick::ALPHACHANNEL_SET);
        $img->setImageFormat('png');
        $this->memoryImage = $img;
    }

    /**
     * Normalize a color input to a string acceptable by ImagickPixel.
     * Supports:
     * - string color names or hex (returned as-is after trim)
     * - arrays: [r,g,b] or [r,g,b,a] or ['r'=>..,'g'=>..,'b'=>..,'a'=>..]
     */
    private function normalizeColor(mixed $color): string
    {
        if (is_array($color)) {
            $r = $color['r'] ?? ($color[0] ?? null);
            $g = $color['g'] ?? ($color[1] ?? null);
            $b = $color['b'] ?? ($color[2] ?? null);
            $a = $color['a'] ?? ($color[3] ?? 1.0);

            if ($r === null || $g === null || $b === null) {
                throw new \InvalidArgumentException('Color array must contain r, g, b values.');
            }

            $r = max(0, min(255, (int)$r));
            $g = max(0, min(255, (int)$g));
            $b = max(0, min(255, (int)$b));
            $a = (float)$a;
            if (!is_finite($a)) { $a = 1.0; }
            $a = max(0.0, min(1.0, $a));

            return sprintf('rgba(%d,%d,%d,%.3f)', $r, $g, $b, $a);
        }

        if (is_string($color)) {
            return trim($color);
        }

        // Fallback to black
        return '#000000';
    }

    /**
     * Resize the asset in-memory without modifying the source file.
     *
     * @param int $width Target width in pixels
     * @param int $height Target height in pixels
     * @param bool $keepAspect If true, keeps aspect ratio using Imagick's best fit
     * @param string|null $fit When keeping aspect: 'contain' (default) fits inside box, 'cover' fills box and then crops center
     * @return $this
     */
    public function resize(int $width, int $height, bool $keepAspect = false, ?string $fit = null): self
    {
        if (!extension_loaded('imagick')) {
            throw new \RuntimeException('Imagick extension is required to resize assets.');
        }
        if ($width <= 0 || $height <= 0) {
            throw new \InvalidArgumentException('Resize width and height must be positive integers.');
        }
        $this->ensureMemoryImageLoaded();

        // Normalize alpha and format
        $this->memoryImage->setImageAlphaChannel(\Imagick::ALPHACHANNEL_SET);
        $this->memoryImage->setImageFormat('png');

        if ($keepAspect) {
            $mode = strtolower($fit ?? 'contain');
            if ($mode === 'cover') {
                // Scale to cover, then center-crop
                $origW = (int)$this->memoryImage->getImageWidth();
                $origH = (int)$this->memoryImage->getImageHeight();
                $scale = max($width / $origW, $height / $origH);
                $newW = (int)ceil($origW * $scale);
                $newH = (int)ceil($origH * $scale);
                $this->memoryImage->resizeImage($newW, $newH, \Imagick::FILTER_LANCZOS, 1.0);
                // Center crop to target
                $x = max(0, (int)floor(($newW - $width) / 2));
                $y = max(0, (int)floor(($newH - $height) / 2));
                $this->memoryImage->cropImage($width, $height, $x, $y);
                $this->memoryImage->setImagePage(0, 0, 0, 0);
            } else {
                // contain: best-fit inside box, may leave empty space around when composited
                $this->memoryImage->thumbnailImage($width, $height, true, true);
            }
        } else {
            // Force exact size (no aspect preserve)
            $this->memoryImage->resizeImage($width, $height, \Imagick::FILTER_LANCZOS, 1.0);
        }

        // Update cached size if present on host class
        if (property_exists($this, 'width')) {
            $this->width = (int)$this->memoryImage->getImageWidth();
        }
        if (property_exists($this, 'height')) {
            $this->height = (int)$this->memoryImage->getImageHeight();
        }

        return $this;
    }

    /**
     * Rotate the asset in-memory without modifying the source file.
     *
     * @param float $degrees Rotation angle in degrees (clockwise)
     * @param string $background Background color for exposed areas (default transparent)
     * @return $this
     */
    public function rotate(float $degrees, string $background = 'transparent'): self
    {
        if (!extension_loaded('imagick')) {
            throw new \RuntimeException('Imagick extension is required to rotate assets.');
        }
        $this->ensureMemoryImageLoaded();
        $bg = new \ImagickPixel($background === 'transparent' ? 'transparent' : $background);
        $this->memoryImage->rotateImage($bg, $degrees);
        // Reset virtual canvas/page so top-left becomes (0,0) and rotation is treated around center
        $this->memoryImage->setImagePage(0, 0, 0, 0);
        // Update cached size info if the class maintains it
        if (method_exists($this, 'getWidth') && method_exists($this, 'getHeight')) {
            if (property_exists($this, 'width')) {
                $this->width = (int)$this->memoryImage->getImageWidth();
            }
            if (property_exists($this, 'height')) {
                $this->height = (int)$this->memoryImage->getImageHeight();
            }
        }
        return $this;
    }

    /**
     * Get a clone of the in-memory Imagick image if available; otherwise null.
     */
    public function getMemoryImage(): ?\Imagick
    {
        if ($this->memoryImage instanceof \Imagick) {
            return clone $this->memoryImage;
        }
        return null;
    }

    /**
     * Clear the in-memory image to free resources.
     * @return void
     */
    public function clearMemoryImage(): void
    {
        if ($this->memoryImage instanceof \Imagick) {
            $this->memoryImage->clear();
            $this->memoryImage->destroy();
        }
        $this->memoryImage = null;
    }

    /**
     * Draw a text overlay on top of the in-memory image.
     *
     * Supported options:
     * - font_size (int): default 16
     * - font_color (string hex or color name): default '#000000'
     * - font_path (string): absolute path to a .ttf/.otf font file (recommended)
     * - x_offset (int): additional X shift in pixels (default 0)
     * - y_offset (int): additional Y shift in pixels (default 0)
     * - align ('left'|'center'|'right'): horizontal anchor within image (default 'left')
     * - valign ('top'|'center'|'bottom'): vertical anchor within image (default 'top')
     *
     * @param string $text
     * @param array $options
     * @return $this
     */
    public function addTextOverlay(string $text, array $options = []): self
    {
        if (!extension_loaded('imagick')) {
            throw new \RuntimeException('Imagick extension is required to add text overlay.');
        }

        $this->ensureMemoryImageLoaded();

        $fontSize  = (int)($options['font_size'] ?? 16);
        $fontColor = $this->normalizeColor($options['font_color'] ?? '#000000');
        $fontPath  = $options['font_path'] ?? null;
        $xOffset   = (int)($options['x_offset'] ?? 0);
        $yOffset   = (int)($options['y_offset'] ?? 0);
        $align     = strtolower((string)($options['align'] ?? 'left'));
        $valign    = strtolower((string)($options['valign'] ?? 'top'));

        $draw = new \ImagickDraw();
        $draw->setFontSize($fontSize);
        if (!empty($fontPath)) {
            if (!is_readable($fontPath)) {
                throw new \RuntimeException('Font file is not readable: ' . $fontPath);
            }
            $draw->setFont($fontPath);
        }
        try {
            $draw->setFillColor(new \ImagickPixel($fontColor));
        } catch (\Throwable $e) {
            throw new \RuntimeException('Unable to construct ImagickPixel from font_color option', 0, $e);
        }

        // Measure text to compute alignment offsets
        $metrics = $this->memoryImage->queryFontMetrics($draw, $text, false);
        $textW = (int)ceil($metrics['textWidth'] ?? 0);
        $textH = (int)ceil(($metrics['ascender'] ?? 0) - ($metrics['descender'] ?? 0));
        $asc   = (float)($metrics['ascender'] ?? 0);
        $desc  = (float)($metrics['descender'] ?? 0);

        $imgW = (int)$this->memoryImage->getImageWidth();
        $imgH = (int)$this->memoryImage->getImageHeight();

        // Horizontal anchor
        switch ($align) {
            case 'center':
                $x = (int)floor(($imgW - $textW) / 2);
                break;
            case 'right':
                $x = (int)($imgW - $textW);
                break;
            default:
                $x = 0;
        }

        // Vertical anchor: Imagick uses baseline Y for annotateImage
        switch ($valign) {
            case 'center':
                // center box then move to baseline by adding ascender
                $y = (int)floor(($imgH - $textH) / 2 + $asc);
                break;
            case 'bottom':
                // bottom aligned baseline: image height minus small descender
                $y = (int)floor($imgH - max(0, -$desc));
                break;
            default: // top
                $y = (int)ceil($asc);
        }

        $x += $xOffset;
        $y += $yOffset;

        $this->memoryImage->annotateImage($draw, $x, $y, 0.0, $text);

        return $this;
    }
}