<?php

namespace dynamicimages\traits;

use dynamicimages\classes\dynamicimages_asset;
use dynamicimages\helpers\dynamicimages_tool;
use dynamicimages\interfaces\dynamicimages_image_i;

trait dynamicimages_image_t
{

    /**
     * Internal Imagick canvas (temporary, changeable image object)
     */
    private ?\Imagick $image = null;
    private ?int $canvasWidth = null;
    private ?int $canvasHeight = null;

    /**
     * @var dynamicimages_tool[]
     */
    private array $tools = [];

    /**
     * @inheritDoc
     */
    public function setAssets(array $assets): dynamicimages_image_i
    {
        $this->assets = $assets;
        return $this;
    }

    /**
     * @inheritDoc
     */
    public function getAssets(): array
    {
        return $this->assets;
    }

    /**
     * @inheritDoc
     */
    public function addAsset(dynamicimages_asset $asset): dynamicimages_image_i
    {
        $this->assets[] = $asset;
        return $this;
    }

    /**
     * @inheritDoc
     */
    public function setTools(array $tools): dynamicimages_image_i
    {
        $this->tools = $tools;
        return $this;
    }

    /**
     * @inheritDoc
     */
    public function addTool(dynamicimages_tool $tool): dynamicimages_image_i
    {
        $this->tools[] = $tool;
        return $this;
    }

    /**
     * @inheritDoc
     */
    public function getTools(): array
    {
        return $this->tools;
    }

    /**
     * Debug
     */
    public function debugAssets(): void
    {
        echo 'Assets count: ' . count($this->assets) . PHP_EOL;
        foreach ($this->assets as $asset) {
            echo 'Asset path: ' . $asset->getPath() . ' - (Width: ' . $asset->getWidth() . ', Height: ' . $asset->getHeight() . ')' . PHP_EOL;
        }
    }

    public function getAssetPath(string $asset_name): string
    {
        return $this->module_asset_path . $asset_name;
    }

    /**
     * Resolve an absolute filesystem path to a font file located under the shared dynamicimages assets/fonts directory.
     *
     * Example result: WD . '/modules/dynamicimages/assets/fonts/YourFont.ttf'
     *
     * @param string $fontFile
     * @return string absolute path
     */
    public function getFontPath(string $fontFile): string
    {
        $fontFile = ltrim($fontFile, '/\\');
        $path = WD . '/' . self::asset_path . 'fonts/' . $fontFile;
        if (!is_readable($path)) {
            throw new \RuntimeException('Font not found or not readable: ' . $path);
        }
        return $path;
    }

    public function setAssetPath(string $asset_path): dynamicimages_image_i
    {
        $this->module_asset_path = $asset_path;
        return $this;
    }

    // ===== Canvas lifecycle =====
    public function initImageCanvas(int $width, int $height, string $background = 'transparent'): dynamicimages_image_i
    {
        if (!extension_loaded('imagick')) {
            throw new \RuntimeException('Imagick extension is required to initialize image canvas.');
        }
        if ($width <= 0 || $height <= 0) {
            throw new \InvalidArgumentException('Canvas width and height must be positive integers.');
        }
        $bg = new \ImagickPixel($background === 'transparent' ? 'transparent' : $background);
        $this->image = new \Imagick();
        $this->image->newImage($width, $height, $bg, 'png'); // ensure alpha channel
        $this->image->setImageFormat('png');
        $this->canvasWidth = $width;
        $this->canvasHeight = $height;
        return $this;
    }

    public function clearImage(): dynamicimages_image_i
    {
        if ($this->image instanceof \Imagick) {
            $this->image->clear();
            $this->image->destroy();
        }
        $this->image = null;
        $this->canvasWidth = null;
        $this->canvasHeight = null;
        return $this;
    }

    public function getCanvasSize(): array
    {
        $this->assertCanvasInitialized();
        return ['width' => $this->canvasWidth, 'height' => $this->canvasHeight];
    }

    // ===== Explicit asset compositing =====
    public function drawAsset(dynamicimages_asset $asset, int $x, int $y, float $opacity = 1.0): dynamicimages_image_i
    {
        $this->assertCanvasInitialized();
        if ($opacity < 0.0 || $opacity > 1.0) {
            throw new \InvalidArgumentException('Opacity must be between 0.0 and 1.0');
        }
        // If the asset has an in-memory image (e.g., rotated), use it; otherwise load from path
        $memoryImage = method_exists($asset, 'getMemoryImage') ? $asset->getMemoryImage() : null;
        if ($memoryImage instanceof \Imagick) {
            $layer = $memoryImage; // already a clone from asset
        } else {
            if (!is_readable($asset->getPath())) {
                throw new \RuntimeException('Asset is not readable: ' . $asset->getPath());
            }
            $layer = new \Imagick();
            $layer->readImage($asset->getPath());
        }
        // Normalize to support alpha
        $layer->setImageAlphaChannel(\Imagick::ALPHACHANNEL_SET);
        if ($opacity < 1.0) {
            $layer->evaluateImage(\Imagick::EVALUATE_MULTIPLY, $opacity, \Imagick::CHANNEL_ALPHA);
        }
        $this->image->compositeImage($layer, \Imagick::COMPOSITE_DEFAULT, $x, $y);
        $layer->clear();
        $layer->destroy();
        return $this;
    }

    /**
     * Draw an asset centered around the given coordinates.
     * This is useful for rotated layers whose bounding box changes per angle,
     * but should visually share the same pivot point.
     */
    public function drawAssetCentered(dynamicimages_asset $asset, int $centerX, int $centerY, float $opacity = 1.0): dynamicimages_image_i
    {
        $this->assertCanvasInitialized();
        if ($opacity < 0.0 || $opacity > 1.0) {
            throw new \InvalidArgumentException('Opacity must be between 0.0 and 1.0');
        }
        $memoryImage = method_exists($asset, 'getMemoryImage') ? $asset->getMemoryImage() : null;
        if ($memoryImage instanceof \Imagick) {
            $layer = $memoryImage; // already a clone from asset
        } else {
            if (!is_readable($asset->getPath())) {
                throw new \RuntimeException('Asset is not readable: ' . $asset->getPath());
            }
            $layer = new \Imagick();
            $layer->readImage($asset->getPath());
        }
        $layer->setImageAlphaChannel(\Imagick::ALPHACHANNEL_SET);
        if ($opacity < 1.0) {
            $layer->evaluateImage(\Imagick::EVALUATE_MULTIPLY, $opacity, \Imagick::CHANNEL_ALPHA);
        }
        $w = (int)$layer->getImageWidth();
        $h = (int)$layer->getImageHeight();
        $x = (int)floor($centerX - $w / 2);
        $y = (int)floor($centerY - $h / 2);
        $this->image->compositeImage($layer, \Imagick::COMPOSITE_DEFAULT, $x, $y);
        $layer->clear();
        $layer->destroy();
        return $this;
    }

    // ===== Transformations on the temporary image object =====
    public function resize(int $width, int $height): dynamicimages_image_i
    {
        $this->assertCanvasInitialized();
        if ($width <= 0 || $height <= 0) {
            throw new \InvalidArgumentException('Resize width and height must be positive integers.');
        }
        $this->image->resizeImage($width, $height, \Imagick::FILTER_LANCZOS, 1.0);
        $this->canvasWidth = $width;
        $this->canvasHeight = $height;
        return $this;
    }

    public function crop(int $width, int $height, int $x, int $y): dynamicimages_image_i
    {
        $this->assertCanvasInitialized();
        if ($width <= 0 || $height <= 0) {
            throw new \InvalidArgumentException('Crop width and height must be positive integers.');
        }
        $this->image->cropImage($width, $height, $x, $y);
        // Reset canvas virtual page to avoid offsets
        $this->image->setImagePage(0, 0, 0, 0);
        $this->canvasWidth = $width;
        $this->canvasHeight = $height;
        return $this;
    }

    public function rotate(float $degrees, string $background = 'transparent'): dynamicimages_image_i
    {
        $this->assertCanvasInitialized();
        $bg = new \ImagickPixel($background === 'transparent' ? 'transparent' : $background);
        $this->image->rotateImage($bg, $degrees);
        // Reset virtual page so top-left is (0,0) after rotation; rotation occurs around center
        $this->image->setImagePage(0, 0, 0, 0);
        // Update stored dimensions
        $this->canvasWidth = (int)$this->image->getImageWidth();
        $this->canvasHeight = (int)$this->image->getImageHeight();
        return $this;
    }

    public function setOpacity(float $opacity): dynamicimages_image_i
    {
        $this->assertCanvasInitialized();
        if ($opacity < 0.0 || $opacity > 1.0) {
            throw new \InvalidArgumentException('Opacity must be between 0.0 and 1.0');
        }
        $this->image->setImageAlphaChannel(\Imagick::ALPHACHANNEL_SET);
        $this->image->evaluateImage(\Imagick::EVALUATE_MULTIPLY, $opacity, \Imagick::CHANNEL_ALPHA);
        return $this;
    }

    /**
     * Draw a straight line on the current canvas.
     *
     * @param int $x1 Starting X coordinate
     * @param int $y1 Starting Y coordinate
     * @param int $x2 Ending X coordinate
     * @param int $y2 Ending Y coordinate
     * @param string $color Any Imagick-compatible color string (e.g., 'red', '#FF0000', 'rgba(255,0,0,0.7)')
     * @param int $thickness Stroke width in pixels
     * @param array<int,float>|null $dash Optional dash pattern (e.g., [10,5] for dashed line)
     * @return dynamicimages_image_i
     */
    public function drawLine(int $x1, int $y1, int $x2, int $y2, string $color = 'rgba(255,0,0,1.0)', int $thickness = 1, ?array $dash = null): dynamicimages_image_i
    {
        $this->assertCanvasInitialized();
        $draw = new \ImagickDraw();
        $draw->setStrokeColor(new \ImagickPixel($color));
        $draw->setStrokeWidth(max(1, $thickness));
        $draw->setFillOpacity(0.0);
        if ($dash !== null && count($dash) > 0) {
            // Imagick expects an array of floats for dash and a dash offset
            $draw->setStrokeDashArray(array_map('floatval', $dash));
            $draw->setStrokeDashOffset(0);
        }
        $draw->line($x1, $y1, $x2, $y2);
        $this->image->drawImage($draw);
        $draw->clear();
        $draw->destroy();
        return $this;
    }

    private function assertCanvasInitialized(): void
    {
        if (!$this->image instanceof \Imagick) {
            throw new \RuntimeException('Canvas is not initialized. Call initImageCanvas() first.');
        }
    }

    /**
     * Export the first asset as a base64 data URI. If no assets exist, throws an exception.
     */
    public function exportAsBase64(?string $format = null, int $quality = 90): string
    {
        // If a canvas is initialized, export that as PNG by default
        if ($this->image instanceof \Imagick) {
            $img = clone $this->image;
            $img->setImageFormat('png');
            // Quality influences compression for PNG differently; keep as hint
            if ($format !== null && strtolower($format) !== 'png') {
                // For now we only support PNG for composed images as requested
            }
            // Strip metadata to reduce size
            $img->stripImage();
            $blob = $img->getImageBlob();
            $img->clear();
            $img->destroy();
            return 'data:image/png;base64,' . base64_encode($blob);
        }

        // Fallback: export first asset as-is
        if (empty($this->assets)) {
            throw new \RuntimeException('No assets available to export.');
        }
        $asset = $this->assets[0];
        $path = $asset->getPath();
        if (!is_readable($path)) {
            throw new \RuntimeException('Asset is not readable: ' . $path);
        }
        $imgInfo = @getimagesize($path);
        $mime = is_array($imgInfo) && isset($imgInfo['mime']) ? $imgInfo['mime'] : 'application/octet-stream';
        $data = file_get_contents($path);
        if ($data === false) {
            throw new \RuntimeException('Failed to read asset: ' . $path);
        }
        return 'data:' . $mime . ';base64,' . base64_encode($data);
    }

    public function getAsset(string $asset_name): ?dynamicimages_asset
    {
        foreach ($this->assets as $asset) {
            if (basename($asset->getPath()) === $asset_name) {
                return $asset;
            }
        }
        return null;
    }

    /**
     * Return the image file as a response directly to the browser
     *
     */
    public function outputImage(?string $format = null, int $quality = 90): void
    {
        $dataUri = $this->exportAsBase64($format, $quality);
        // Extract mime type and base64 data
        if (preg_match('/^data:(image\/[a-zA-Z0-9+.-]+);base64,(.*)$/', $dataUri, $matches)) {
            $mimeType = $matches[1];
            $base64Data = $matches[2];
            // Decode base64 data
            $imageData = base64_decode($base64Data);
            if ($imageData !== false) {
                // Send appropriate headers
                header('Content-Type: ' . $mimeType);
                header('Content-Length: ' . strlen($imageData));
                // Output the image data
                echo $imageData;
                exit;
            }
        }
    }

    /**
     * Serve the composed picture directly to the client. Wrapper for outputImage.
     */
    public function servePicture(?string $format = null, int $quality = 90): void
    {
        $this->outputImage($format, $quality);
    }
}