<?php

declare(strict_types=1);

namespace HeroQR\Contracts\Customs;

use Endroid\QrCode\{Matrix\MatrixInterface, QrCodeInterface};
use Endroid\QrCode\{Color\ColorInterface, Logo\LogoInterface};
use HeroQR\Customs\{ImageOverlay, ShapeDrawers};

/**
 * Handles the rendering of QR codes using GD image processing library.
 *
 * This class provides methods for drawing QR code matrices, handling image overlays,
 * and preparing corner images for QR codes. It serves as a base for specific QR code
 * renderers that utilize GD to create QR code images.
 *
 * @package HeroQR\Contracts\Customs
 */
final class Drawer
{
    private const FINDER_PATTERN_SIZE = 7;

    /**
     * Draws the QR code on the base image, including corner images and matrix.
     *
     * Prepares and resizes the corner image, then renders the QR code matrix
     * with the specified block size, color, and shape, and applies the image overlay.
     *
     * @param \GdImage $baseImage The base image for drawing the QR code.
     * @param MatrixInterface $matrix The QR code matrix.
     * @param int $baseBlockSize The pixel size of each QR matrix block.
     * @param int $foregroundColor The color of the QR code blocks.
     * @param QrCodeInterface $qrCode The QR code object.
     * @param string $blockShape The shape of the QR code blocks.
     * @param ?LogoInterface $logo The logo to embed in the QR code (optional)
     * @param ImageOverlay $imageOverlay Optional image overlay.
     * @return void
     * @throws \Exception
     */
    public static function drawQrCode(
        \GdImage        $baseImage,
        MatrixInterface $matrix,
        int             $baseBlockSize,
        int             $foregroundColor,
        QrCodeInterface $qrCode,
        string          $blockShape,
        ?LogoInterface  $logo,
        ImageOverlay    $imageOverlay
    ): void
    {
        $cornerImage = self::prepareCornerImage($qrCode, $imageOverlay);
        $resizedCorner = self::resizeCornerImage($cornerImage, $baseBlockSize);
        self::drawMatrix($baseImage, $matrix, $baseBlockSize, $foregroundColor, $resizedCorner, $blockShape, $qrCode->getData(), $logo);

        imagedestroy($cornerImage);
        imagedestroy($resizedCorner);
    }

    /**
     * Draws the QR code matrix onto the base image.
     *
     * @param \GdImage $baseImage The base image for the QR code.
     * @param MatrixInterface $matrix The QR code matrix.
     * @param int $baseBlockSize The pixel size of each QR matrix block.
     * @param int $foregroundColor The fill color for the blocks.
     * @param \GdImage $resizedCorner Resized corner image for rounded blocks.
     * @param string $blockShape The shape of the block in the QR code
     * @param LogoInterface|null $logo The logo to embed in the QR code (optional).
     * @param string $data The data encoded in the QR code (optional).
     * @return void
     */
    public static function drawMatrix(
        \GdImage        $baseImage,
        MatrixInterface $matrix,
        int             $baseBlockSize,
        int             $foregroundColor,
        \GdImage        $resizedCorner,
        string          $blockShape,
        string          $data,
        ?LogoInterface  $logo = null,
    ): void
    {
        $blockCount = $matrix->getBlockCount();
        [$logoWidth, $logoHeight, $padding] = self::prepareLogoDimensions($logo, $data, $blockCount, $baseBlockSize);

        [$logoStartRow, $logoEndRow, $logoStartCol, $logoEndCol] = self::calculateLogoBounds([
            'blockCount' => $blockCount,
            'width' => $logoWidth,
            'height' => $logoHeight,
            'padding' => $padding
        ]);

        for ($rowIndex = 0; $rowIndex < $blockCount; ++$rowIndex) {
            for ($columnIndex = 0; $columnIndex < $blockCount; ++$columnIndex) {
                if (self::isWithinLogoBounds([
                    'rowIndex' => $rowIndex,
                    'columnIndex' => $columnIndex,
                    'logoStartRow' => intval($logoStartRow),
                    'logoEndRow' => intval($logoEndRow),
                    'logoStartCol' => intval($logoStartCol),
                    'logoEndCol' => intval($logoEndCol)
                ])) {
                    continue;
                }

                if ($matrix->getBlockValue($rowIndex, $columnIndex) === 1) {
                    self::drawMatrixBlock($baseImage, $matrix, $rowIndex, $columnIndex, $baseBlockSize, $foregroundColor, $blockShape, $resizedCorner);
                }
            }
        }
    }

    /**
     * Draws a block of the matrix on the QR code image, handling special corner blocks and filling others with a foreground color
     *
     * @param \GdImage $baseImage The base image where the block will be drawn.
     * @param MatrixInterface $matrix The matrix representing the QR code.
     * @param int $rowIndex The row index of the block.
     * @param int $columnIndex The column index of the block.
     * @param int $baseBlockSize The pixel size of each QR matrix block.
     * @param int $foregroundColor The color to fill the block.
     * @param string $blockShape The shape of the block in the QR code.
     * @param \GdImage $resizedCorner The resized corner image (used for corner blocks).
     * @return void
     */
    private static function drawMatrixBlock(
        \GdImage        $baseImage,
        MatrixInterface $matrix,
        int             $rowIndex,
        int             $columnIndex,
        int             $baseBlockSize,
        int             $foregroundColor,
        string          $blockShape,
        \GdImage        $resizedCorner
    ): void
    {
        $isTopLeft = $rowIndex < self::FINDER_PATTERN_SIZE && $columnIndex < self::FINDER_PATTERN_SIZE;
        $isTopRight = $rowIndex < self::FINDER_PATTERN_SIZE && $columnIndex >= $matrix->getBlockCount() - self::FINDER_PATTERN_SIZE;
        $isBottomLeft = $rowIndex >= $matrix->getBlockCount() - self::FINDER_PATTERN_SIZE && $columnIndex < self::FINDER_PATTERN_SIZE;

        if ($isTopLeft || $isTopRight || $isBottomLeft) {
            self::drawCornerBlock($baseImage, $matrix, $rowIndex, $columnIndex, $baseBlockSize, $resizedCorner, $isTopLeft, $isTopRight, $isBottomLeft);
        } else {
            ShapeDrawers::{$blockShape}($baseImage, $rowIndex, $columnIndex, $baseBlockSize, $foregroundColor);
        }
    }

    /**
     * Draws a corner block on the QR code image (top-left, top-right, or bottom-left)
     *
     * @param \GdImage $baseImage The base image where the corner block will be drawn.
     * @param MatrixInterface $matrix The matrix representing the QR code.
     * @param int $rowIndex The row index of the block.
     * @param int $columnIndex The column index of the block.
     * @param int $baseBlockSize The pixel size of each QR matrix block.
     * @param \GdImage $resizedCorner The resized corner image.
     * @param bool $isTopLeft Whether the corner is top-left.
     * @param bool $isTopRight Whether the corner is top-right.
     * @param bool $isBottomLeft Whether the corner is bottom-left.
     * @return void
     */
    private static function drawCornerBlock(
        \GdImage        $baseImage,
        MatrixInterface $matrix,
        int             $rowIndex,
        int             $columnIndex,
        int             $baseBlockSize,
        \GdImage        $resizedCorner,
        bool            $isTopLeft,
        bool            $isTopRight,
        bool            $isBottomLeft
    ): void
    {
        if (self::rotateCornerImage($rowIndex, $matrix, $isTopLeft, $isTopRight, $isBottomLeft, $columnIndex)) {
            $rotatedCorner = $resizedCorner;

            if ($isTopRight) {
                $rotatedCorner = imagerotate($resizedCorner, 270, imagecolorallocatealpha($resizedCorner, 0, 0, 0, 127));
            } elseif ($isBottomLeft) {
                $rotatedCorner = imagerotate($resizedCorner, 90, imagecolorallocatealpha($resizedCorner, 0, 0, 0, 127));
            }

            self::copyResampledImage(
                $baseImage,
                $rotatedCorner,
                ['X' => intval($columnIndex * $baseBlockSize), 'Y' => intval($rowIndex * $baseBlockSize), 'Width' => self::FINDER_PATTERN_SIZE * $baseBlockSize, 'Height' => self::FINDER_PATTERN_SIZE * $baseBlockSize],
                ['X' => 0, 'Y' => 0, 'Width' => self::FINDER_PATTERN_SIZE * $baseBlockSize, 'Height' => self::FINDER_PATTERN_SIZE * $baseBlockSize,]
            );
        }
    }

    /**
     * Checks if the corner image needs to be rotated based on the matrix position.
     *
     * @param int $rowIndex The current row index in the matrix.
     * @param MatrixInterface $matrix The matrix object.
     * @param bool $isTopLeft Whether it's the top-left corner.
     * @param bool $isTopRight Whether it's the top-right corner.
     * @param bool $isBottomLeft Whether it's the bottom-left corner.
     * @param int $columnIndex The current column index in the matrix.
     * @return bool True if the corner image needs to be rotated, false otherwise.
     */
    private static function rotateCornerImage(
        int             $rowIndex,
        MatrixInterface $matrix,
        bool            $isTopLeft,
        bool            $isTopRight,
        bool            $isBottomLeft,
        int             $columnIndex
    ): bool
    {
        return ($isTopLeft && $rowIndex === 0 && $columnIndex === 0) ||
            ($isTopRight && $rowIndex === 0 && $columnIndex === $matrix->getBlockCount() - self::FINDER_PATTERN_SIZE) ||
            ($isBottomLeft && $rowIndex === $matrix->getBlockCount() - self::FINDER_PATTERN_SIZE && $columnIndex === 0);
    }

    /**
     * Resamples and copies a portion of the source image to the destination image
     *
     * @param \GdImage $dstImage Destination image resource
     * @param \GdImage $srcImage Source image resource
     * @param array $dst_X_Y_W_H Destination coordinates and dimensions (X, Y, Width, Height)
     * @param array $src_X_Y_W_H Source coordinates and dimensions (X, Y, Width, Height)
     * @return bool True on success, false on failure
     */
    public static function copyResampledImage(
        \GdImage $dstImage,
        \GdImage $srcImage,
        array    $dst_X_Y_W_H,
        array    $src_X_Y_W_H
    ): bool
    {
        return imagecopyresampled(
            $dstImage,
            $srcImage,
            $dst_X_Y_W_H['X'],
            $dst_X_Y_W_H['Y'],
            $src_X_Y_W_H['X'],
            $src_X_Y_W_H['Y'],
            $dst_X_Y_W_H['Width'],
            $dst_X_Y_W_H['Height'],
            $src_X_Y_W_H['Width'],
            $src_X_Y_W_H['Height'],
        );
    }

    /**
     * Prepares the logo dimensions and calculates the necessary padding based on the data length.
     *
     * @param LogoInterface|null $logo The logo to be used in the QR code.
     * @param string $data The data encoded in the QR code.
     * @param int $blockCount The number of blocks per QR matrix side.
     * @param int $baseBlockSize The pixel size of each QR matrix block.
     * @return array The width, height, and padding for the logo.
     */
    private static function prepareLogoDimensions(
        ?LogoInterface $logo,
        string         $data,
        int            $blockCount,
        int            $baseBlockSize
    ): array
    {
        if ($logo === null) {
            return [0, 0, 0];
        }

        [$logoWidth, $logoHeight] = self::calculateLogoDimensions($logo, $baseBlockSize);

        $logoRatio = ($logoWidth * $logoHeight) / ($blockCount * $blockCount);

        $length = strlen($data ?? '');

        $padding = max(
            2,
            (int)ceil($blockCount * 0.045 + $logoRatio * $blockCount * 0.7 + log10($length ?: 1))
        );

        return [$logoWidth, $logoHeight, $padding];
    }

    /**
     * Calculates the bounds for the logo's placement within the QR code.
     *
     * @param array $logo Contains logo dimensions, block count, and padding information.
     * @return array The start and end row and column indices for logo placement.
     */
    private static function calculateLogoBounds(
        array $logo
    ): array
    {
        $logoStartRow = max(0, ($logo['blockCount'] - $logo['height']) / 2 - $logo['padding']);
        $logoEndRow = min($logo['blockCount'], ($logo['blockCount'] + $logo['height']) / 2 + $logo['padding']);
        $logoStartCol = max(0, ($logo['blockCount'] - $logo['width']) / 2 - $logo['padding']);
        $logoEndCol = min($logo['blockCount'], ($logo['blockCount'] + $logo['width']) / 2 + $logo['padding']);

        return [$logoStartRow, $logoEndRow, $logoStartCol, $logoEndCol];
    }

    /**
     * Checks if the given row and column are within the calculated logo bounds.
     *
     * @param array $columns Contains row and column indices, and logo bounds.
     * @return bool True if the position is within the logo bounds, false otherwise.
     */
    private static function isWithinLogoBounds(
        array $columns
    ): bool
    {
        return $columns['rowIndex'] >= $columns['logoStartRow'] && $columns['rowIndex'] < $columns['logoEndRow'] &&
            $columns['columnIndex'] >= $columns['logoStartCol'] && $columns['columnIndex'] < $columns['logoEndCol'];
    }

    /**
     * Calculates the dimensions of the logo based on the base block size.
     *
     * @param LogoInterface|null $logo The logo to be resized.
     * @param int $baseBlockSize The pixel size of each QR matrix block.
     * @return array The resized width and height of the logo in blocks.
     */
    private static function calculateLogoDimensions(
        ?LogoInterface $logo,
        int            $baseBlockSize
    ): array
    {
        return $logo === null
            ? [0, 0] : [
                (int)ceil($logo->getResizeToWidth() / $baseBlockSize),
                (int)ceil($logo->getResizeToHeight() / $baseBlockSize)
            ];
    }

    /**
     * Prepares the corner image for the QR code
     *
     * @param QrCodeInterface $qrCode The QR code object
     * @return \GdImage The prepared corner image
     * @throws \Exception If the corner image cannot be loaded
     */
    private static function prepareCornerImage(
        QrCodeInterface $qrCode,
        ImageOverlay    $imageOverlay
    ): \GdImage
    {
        $cornerImage = $imageOverlay->getImage();

        imageantialias($cornerImage, true);
        imagesavealpha($cornerImage, true);

        return self::tintImage($cornerImage, $qrCode->getForegroundColor());
    }

    /**
     * Tints the image with a specified color, applying the color to non-transparent pixels
     *
     * @param \GdImage $image The image to tint
     * @param ColorInterface $color The color to apply to the image
     * @return \GdImage The tinted image
     */
    private static function tintImage(
        \GdImage       $image,
        ColorInterface $color
    ): \GdImage
    {
        [$width, $height] = [imagesx($image), imagesy($image)];

        $tinted = imagecreatetruecolor($width, $height);

        imagesavealpha($tinted, true);
        imagealphablending($tinted, false);

        $transparent = imagecolorallocatealpha($tinted, 0, 0, 0, 127);
        imagefill($tinted, 0, 0, $transparent);

        for ($x = 0; $x < $width; $x++) {
            for ($y = 0; $y < $height; $y++) {
                $pixelColor = imagecolorsforindex($image, imagecolorat($image, $x, $y));

                if ($pixelColor['alpha'] < 120) {
                    $newColor = imagecolorallocatealpha(
                        $tinted,
                        $color->getRed(),
                        $color->getGreen(),
                        $color->getBlue(),
                        $color->getAlpha()
                    );
                    imagesetpixel($tinted, $x, $y, $newColor);
                }
            }
        }
        return $tinted;
    }

    /**
     * Resizes the corner image to fit the QR code matrix
     *
     * @param \GdImage $cornerImage The original corner image
     * @param int $baseBlockSize The pixel size of each QR matrix block.
     * @return \GdImage The resized corner image for the QR code.
     */
    private static function resizeCornerImage(
        \GdImage $cornerImage,
        int      $baseBlockSize
    ): \GdImage
    {
        $cornerSize = self::FINDER_PATTERN_SIZE * $baseBlockSize;
        $resizedCorner = imagecreatetruecolor($cornerSize, $cornerSize);

        imageantialias($resizedCorner, true);
        imagesavealpha($resizedCorner, true);
        imagealphablending($resizedCorner, false);

        $transparent = imagecolorallocatealpha($resizedCorner, 0, 0, 0, 127);
        imagefill($resizedCorner, 0, 0, $transparent);

        imagealphablending($resizedCorner, true);

        Drawer::copyResampledImage(
            $resizedCorner,
            $cornerImage,
            ['X' => 0, 'Y' => 0, 'Width' => $cornerSize, 'Height' => $cornerSize,],
            ['X' => 0, 'Y' => 0, 'Width' => imagesy($cornerImage), 'Height' => imagesy($cornerImage)]
        );

        return $resizedCorner;
    }
}
