<?php declare(strict_types=1);
/*
 * This file is part of the SplendidBear Websites' projects.
 *
 * Copyright (c) 2026 @ www.splendidbear.org
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace App\Service;

use App\Entity\PlayedGame;
use Exception;
use GdImage;
use League\Flysystem\FilesystemOperator;
use Psr\Log\LoggerInterface;
use RuntimeException;
use Symfony\Component\Uid\Uuid;

/**
 * Class BattleCardGenerator
 *
 * Generates a 1200x630 PNG battle card for Open Graph sharing.
 *
 * @package  App\Service
 * @author   Lang <https://www.splendidbear.org>
 * @category Class
 * @license  https://www.gnu.org/licenses/lgpl-3.0.en.html GNU Lesser General Public License
 * @link     www.splendidbear.org
 * @since    2026. 04. 14.
 */
final class BattleCardGenerator
{
    private const int    WIDTH       = 1200;
    private const int    HEIGHT      = 630;
    private const int    AVATAR_SIZE = 120;

    public function __construct(
        private readonly string             $cacheDir,
        private readonly string             $fontPath,
        private readonly FilesystemOperator $minioMediaStorage,
        private readonly LoggerInterface    $logger,
    ) {
    }

    /** Returns a deterministic UUID v5 for the given battle ID — same battle always maps to the same filename. */
    public function cachePath(int $battleId): string
    {
        $uuid = Uuid::v5(Uuid::fromString(Uuid::NAMESPACE_URL), 'mineseeker-battle-' . $battleId);

        return $this->cacheDir . '/' . $uuid->toRfc4122() . '.png';
    }

    public function generate(PlayedGame $game): string
    {
        $path = $this->cachePath((int)$game->id);

        // Always regenerate to ensure bonus points are included
        if (is_file($path)) {
            unlink($path);
        }

        if (!is_dir($this->cacheDir)) {
            if (
                !mkdir($concurrentDirectory = $this->cacheDir, 0755, true)
                && !is_dir($concurrentDirectory)
            ) {
                $this->logger->error(sprintf('Failed to create directory "%s" for battle card cache', $concurrentDirectory));
                throw new RuntimeException(sprintf('Directory "%s" was not created', $concurrentDirectory));
            }
        }

        $this->render($game, $path);

        return $path;
    }

    private function render(PlayedGame $game, string $dest): void
    {
        $im = imagecreatetruecolor(self::WIDTH, self::HEIGHT);

        /** Palette*/
        $bg = imagecolorallocate($im, 13, 13, 28);
        $dot = imagecolorallocate($im, 30, 30, 55);
        $divider = imagecolorallocate($im, 40, 40, 70);
        $white = imagecolorallocate($im, 230, 230, 240);
        $muted = imagecolorallocate($im, 90, 90, 115);
        $red = imagecolorallocate($im, 246, 125, 82);
        $blue = imagecolorallocate($im, 149, 207, 245);
        $gold = imagecolorallocate($im, 255, 200, 50);

        /** Background*/
        imagefill($im, 0, 0, $bg);

        /** Dot-grid texture*/
        for ($x = 40; $x < self::WIDTH; $x += 40) {
            for ($y = 40; $y < self::HEIGHT; $y += 40) {
                imagesetpixel($im, $x, $y, $dot);
            }
        }

        /** Horizontal accent lines*/
        imageline($im, 0, 90, self::WIDTH, 90, $divider);
        imageline($im, 0, self::HEIGHT - 60, self::WIDTH, self::HEIGHT - 60, $divider);

        /** Vertical centre divider*/
        imageline($im, self::WIDTH / 2, 110, self::WIDTH / 2, self::HEIGHT - 80, $divider);

        /** Resolve names*/
        $redName = $game->red?->getUsername()
            ?? ($game->redAnon !== null ? 'Anonymous' : 'Guest');
        $blueName = $game->blue?->getUsername()
            ?? ($game->blueAnon !== null ? 'Anonymous' : 'Guest');
        $redPts = $game->redPoints;
        $bluePts = $game->bluePoints;
        $resign = $game->resign;

        /** Winner*/
        $winner = null;
        if ($resign === 'red') {
            $winner = 'blue';
        } elseif ($resign === 'blue') {
            $winner = 'red';
        } elseif ($redPts !== null && $bluePts !== null) {
            if ($redPts > $bluePts) $winner = 'red';
            elseif ($bluePts > $redPts) $winner = 'blue';
            else $winner = 'draw';
        }

        $this->centeredText($im, 'MineSeeker', 20, self::WIDTH / 2, 58, $muted);

        /** RED and BLUE labels aligned with avatars horizontally*/
        $this->centeredText($im, 'RED', 16, 220, 130, $red);
        $this->centeredText($im, 'BLUE', 16, 980, 130, $blue);

        /** Draw avatars below the team labels (moved down by 60px total: 200 → 260)*/
        $redAvatar = $game->red?->avatarPath;
        $blueAvatar = $game->blue?->avatarPath;

        $this->drawAvatar($im, $redAvatar, 220, 260, $red, $redName);
        $this->drawAvatar($im, $blueAvatar, 980, 260, $blue, $blueName);

        $redColor = $winner === 'red' ? $gold : ($winner === 'draw' ? $white : $red);
        $blueColor = $winner === 'blue' ? $gold : ($winner === 'draw' ? $white : $blue);

        /** Truncate long usernames (max 10 chars + "...")*/
        $redNameDisplay = mb_strlen($redName) > 10 ? mb_substr($redName, 0, 10) . '...' : $redName;
        $blueNameDisplay = mb_strlen($blueName) > 10 ? mb_substr($blueName, 0, 10) . '...' : $blueName;

        /** Player names lower below avatars (moved down by 60px total: 310 → 370)*/
        $this->centeredTextFit($im, $redNameDisplay, 36, 220, 370, $redColor, 400);
        $this->centeredTextFit($im, $blueNameDisplay, 36, 980, 370, $blueColor, 400);

        $scoreText = $redPts !== null && $bluePts !== null ? $redPts . '  :  ' . $bluePts : 'VS';
        $this->centeredText($im, $scoreText, 72, self::WIDTH / 2, 390, $white);

        /** Bonus points below score*/
        $redBonusPoints = $game->redBonusPoints ?? 0;
        $blueBonusPoints = $game->blueBonusPoints ?? 0;
        $bonusText = number_format((float)$redBonusPoints, 1, '.', '') . '  *  :  *  ' . number_format((float)$blueBonusPoints, 1, '.', '');
        $this->centeredText($im, $bonusText, 24, self::WIDTH / 2, 445, $gold);

        if ($winner === 'red') {
            $resultText = $redName . ' wins';
            $resultColor = $gold;
        } elseif ($winner === 'blue') {
            $resultText = $blueName . ' wins';
            $resultColor = $gold;
        } elseif ($winner === 'draw') {
            $resultText = 'Draw';
            $resultColor = $muted;
        } else {
            $resultText = '';
            $resultColor = $muted;
        }

        if ($resultText !== '') {
            $this->centeredText($im, $resultText, 30, self::WIDTH / 2, 495, $resultColor);
        }

        if ($resign) {
            $this->centeredText($im, ucfirst($resign) . ' resigned', 18, self::WIDTH / 2, 528, $muted);
        }

        $this->centeredText($im, 'mineseeker.hu', 16, self::WIDTH / 2, self::HEIGHT - 20, $muted);

        imagepng($im, $dest);
        imagedestroy($im);
    }

    /** Draw avatar or initials centered on $cx, $cy. */
    private function drawAvatar(GdImage $im, ?string $avatarPath, int $cx, int $cy, int $color, string $name): void
    {
        $avatarImg = null;

        /** Try to load avatar from MinIO if path exists*/
        if ($avatarPath) {
            try {
                /** Remove 'avatar/' prefix if it exists since storage already has media/ prefix*/
                $path = str_starts_with($avatarPath, 'avatar/') ? $avatarPath : 'avatar/' . $avatarPath;
                $avatarData = $this->minioMediaStorage->read($path);
                $avatarImg = imagecreatefromstring($avatarData);
            } catch (Exception $e) {
                /** Failed to load avatar, will use initials*/
                $avatarImg = null;
            }
        }

        $x = $cx - self::AVATAR_SIZE / 2;
        $y = $cy - self::AVATAR_SIZE / 2;

        if ($avatarImg) {
            /** Draw circular avatar image*/
            $mask = imagecreatetruecolor(self::AVATAR_SIZE, self::AVATAR_SIZE);
            $transparent = imagecolorallocatealpha($mask, 0, 0, 0, 127);
            imagefill($mask, 0, 0, $transparent);

            /** Create circular mask*/
            imagefilledellipse(
                $mask,
                self::AVATAR_SIZE / 2,
                self::AVATAR_SIZE / 2,
                self::AVATAR_SIZE,
                self::AVATAR_SIZE,
                imagecolorallocate($mask, 255, 255, 255),
            );

            /** Resize and crop avatar*/
            $resized = imagecreatetruecolor(self::AVATAR_SIZE, self::AVATAR_SIZE);
            imagealphablending($resized, false);
            imagesavealpha($resized, true);
            $bg = imagecolorallocatealpha($resized, 0, 0, 0, 127);
            imagefill($resized, 0, 0, $bg);

            $srcW = imagesx($avatarImg);
            $srcH = imagesy($avatarImg);
            $size = min($srcW, $srcH);
            $srcX = ($srcW - $size) / 2;
            $srcY = ($srcH - $size) / 2;

            imagecopyresampled(
                $resized,
                $avatarImg,
                0,
                0,
                (int)$srcX,
                (int)$srcY,
                self::AVATAR_SIZE,
                self::AVATAR_SIZE,
                $size,
                $size,
            );

            /** Apply circular mask*/
            for ($py = 0; $py < self::AVATAR_SIZE; $py++) {
                for ($px = 0; $px < self::AVATAR_SIZE; $px++) {
                    $maskColor = imagecolorat($mask, $px, $py);
                    if (($maskColor >> 16) & 0xFF) {
                        $resizedColor = imagecolorat($resized, $px, $py);
                        imagesetpixel($im, (int)($x + $px), (int)($y + $py), $resizedColor);
                    }
                }
            }

            imagedestroy($avatarImg);
            imagedestroy($resized);
            imagedestroy($mask);
        } else {
            /** Draw circular background with initials*/
            imagefilledellipse($im, (int)$cx, (int)$cy, self::AVATAR_SIZE, self::AVATAR_SIZE, $color);

            /** Draw initials */
            $initials = mb_strtoupper(mb_substr($name, 0, 2));
            $fontSize = 48;
            $bbox = imagettfbbox($fontSize, 0, $this->fontPath, $initials);
            $textW = $bbox[2] - $bbox[0];
            $textH = $bbox[1] - $bbox[7];
            $textX = $cx - $textW / 2;
            $textY = $cy + $textH / 2;

            $white = imagecolorallocate($im, 255, 255, 255);
            imagettftext($im, $fontSize, 0, (int)$textX, (int)$textY, $white, $this->fontPath, $initials);
        }
    }

    /** Render text centered on $cx. */
    private function centeredText(GdImage $im, string $text, int $size, int $cx, int $y, int $color): void
    {
        $bbox = imagettfbbox($size, 0, $this->fontPath, $text);
        $w = $bbox[2] - $bbox[0];
        imagettftext($im, $size, 0, (int)($cx - $w / 2), $y, $color, $this->fontPath, $text);
    }

    /** Render text centered on $cx, shrinking font size to fit $maxWidth. */
    private function centeredTextFit(
        GdImage $im,
        string  $text,
        int     $size,
        int     $cx,
        int     $y,
        int     $color,
        int     $maxWidth
    ): void {
        $bbox = imagettfbbox($size, 0, $this->fontPath, $text);
        $w = $bbox[2] - $bbox[0];
        if ($w > $maxWidth) {
            $size = (int)($size * $maxWidth / $w);
        }
        $this->centeredText($im, $text, $size, $cx, $y, $color);
    }
}
