#!/usr/bin/env python3
"""
Savage Primal VFX Factory for FreeExile (Grimdark ARPG 2.5D).
Synthesizes procedural Savage Primal visual effect frame sequences:
1. Savage Blood Cleave (Liệt Huyết Cuồng Trảm - Gore & Blood Splatter)
2. Primal Blood Lightning (Huyết Lôi Ma Kích - Crimson-Violet Bolt & Ground Crater)
3. Putrid Necrotic Frost (Hủ Cốt Hàn Độc - Swamp Bone Spikes & Toxic Mists)
4. Feral Evasion Claws (Cuồng Dã Thân Pháp - Mud Spray & Gore Trail)

Packs into Power-of-Two Sprite Sheet Atlas with Tangent-Space Normal Maps for PBR.
"""

from __future__ import annotations
import json
import math
import os
import sys
from pathlib import Path
from typing import Dict, List, Tuple, Any

import cv2
import numpy as np
from PIL import Image, ImageDraw

PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent


def create_blood_cleave_frame(step: float, size: int = 128) -> np.ndarray:
    """Renders a single frame of Savage Blood Cleave (gore arc + blood beads)."""
    img = np.zeros((size, size, 4), dtype=np.uint8)
    center = (size // 2, size // 2)
    radius = int(size * 0.38)

    # Arc angle progression
    start_angle = -30.0 + step * 140.0
    end_angle = start_angle + 65.0

    # Draw outer dark coagulated blood arc
    cv2.ellipse(img, center, (radius, radius), 0, start_angle, end_angle, (18, 18, 127, 230), 10)
    # Draw inner fiery arterial crimson core
    cv2.ellipse(img, center, (radius, radius), 0, start_angle + 10, end_angle - 5, (20, 40, 220, 255), 4)

    # Blood splatter droplets along tangential velocity
    np.random.seed(int(step * 100))
    rad = math.radians(end_angle)
    tip_x = int(center[0] + radius * math.cos(rad))
    tip_y = int(center[1] + radius * math.sin(rad))

    for _ in range(8):
        drop_x = tip_x + int(np.random.normal(0, 8 * (1.0 - step)))
        drop_y = tip_y + int(np.random.normal(0, 8 * (1.0 - step)))
        drop_r = max(1, int(np.random.uniform(1.5, 4.0)))
        if 0 <= drop_x < size and 0 <= drop_y < size:
            cv2.circle(img, (drop_x, drop_y), drop_r, (15, 20, 185, 240), -1)

    return img


def create_blood_lightning_frame(step: float, size: int = 128) -> np.ndarray:
    """Renders a single frame of Primal Blood Lightning (crimson & violet bolt)."""
    img = np.zeros((size, size, 4), dtype=np.uint8)
    np.random.seed(int(step * 777))

    start = (size // 2 + int(np.random.uniform(-15, 15)), 6)
    target = (size // 2, size - 14)

    # Multi-segment fractal bolt
    points = [start]
    segments = 6
    for i in range(1, segments):
        t = i / segments
        px = int(start[0] + (target[0] - start[0]) * t + np.random.uniform(-18, 18))
        py = int(start[1] + (target[1] - start[1]) * t)
        points.append((px, py))
    points.append(target)

    # Draw violet outer aura
    for i in range(len(points) - 1):
        cv2.line(img, points[i], points[i + 1], (130, 20, 110, 180), 8)
    # Draw crimson energy sheath
    for i in range(len(points) - 1):
        cv2.line(img, points[i], points[i + 1], (30, 30, 220, 230), 4)
    # Draw white-hot necrotic core
    for i in range(len(points) - 1):
        cv2.line(img, points[i], points[i + 1], (235, 240, 255, 255), 2)

    # Impact shockwave ring at ground
    impact_r = int(10 + step * 35)
    cv2.ellipse(img, target, (impact_r, impact_r // 2), 0, 0, 360, (20, 20, 180, int(200 * (1 - step))), 2)
    return img


def create_necrotic_frost_frame(step: float, size: int = 128) -> np.ndarray:
    """Renders a frame of Putrid Necrotic Frost (bone spikes + mossy decay)."""
    img = np.zeros((size, size, 4), dtype=np.uint8)
    center = (size // 2, size // 2)

    # Necrotic mist base
    mist_r = int(18 + step * 28)
    cv2.circle(img, center, mist_r, (40, 80, 25, int(150 * (1.0 - step))), -1)

    # Jagged bone spike cluster
    num_spikes = 5
    for k in range(num_spikes):
        angle = (k / num_spikes) * math.pi * 2 + (step * 0.4)
        length = int(24 + 18 * math.sin(step * math.pi))
        tip_x = int(center[0] + length * math.cos(angle))
        tip_y = int(center[1] + (length * 0.6) * math.sin(angle))

        # Draw bone spike body
        pts = np.array([
            center,
            (int(center[0] + 6 * math.cos(angle + 0.5)), int(center[1] + 4 * math.sin(angle + 0.5))),
            (tip_x, tip_y),
            (int(center[0] + 6 * math.cos(angle - 0.5)), int(center[1] + 4 * math.sin(angle - 0.5)))
        ], np.int32)
        cv2.fillPoly(img, [pts], (180, 200, 210, 230))
        # Draw putrid moss rim
        cv2.polylines(img, [pts], True, (30, 120, 45, 255), 1)

    return img


def generate_sobel_normal(diffuse_bgra: np.ndarray, intensity: float = 2.2) -> np.ndarray:
    """Synthesizes Tangent-Space Normal Map using Sobel operators."""
    alpha = diffuse_bgra[:, :, 3]
    gray = cv2.cvtColor(diffuse_bgra[:, :, :3], cv2.COLOR_BGR2GRAY).astype(np.float32) / 255.0
    gray = cv2.GaussianBlur(gray, (3, 3), 0)

    sobel_x = cv2.Sobel(gray, cv2.CV_32F, 1, 0, ksize=3)
    sobel_y = cv2.Sobel(gray, cv2.CV_32F, 0, 1, ksize=3)

    dx = -sobel_x * intensity
    dy = -sobel_y * intensity
    dz = np.ones_like(dx)

    mag = np.sqrt(dx * dx + dy * dy + dz * dz)
    mag[mag == 0] = 1.0

    norm_x = (dx / mag) * 0.5 + 0.5
    norm_y = (dy / mag) * 0.5 + 0.5
    norm_z = (dz / mag) * 0.5 + 0.5

    normal_bgr = np.stack([
        (norm_x * 255).astype(np.uint8),
        (norm_y * 255).astype(np.uint8),
        (norm_z * 255).astype(np.uint8),
        alpha
    ], axis=-1)
    return normal_bgr


def build_savage_vfx_atlas() -> Tuple[Path, Path, Path]:
    """Generates 4x4 Power-of-Two Savage VFX Atlas with Normal Maps and JSON metadata."""
    frame_size = 128
    cols, rows = 4, 4
    atlas_w = cols * frame_size  # 512
    atlas_h = rows * frame_size  # 512

    diffuse_atlas = np.zeros((atlas_h, atlas_w, 4), dtype=np.uint8)
    metadata: Dict[str, Any] = {
        "meta": {
            "name": "savage_primal_vfx_atlas",
            "format": "RGBA8888",
            "size": {"w": atlas_w, "h": atlas_h},
            "scale": 1.0,
            "art_style": "Grimdark Savage Primal (Path of Exile 2 pure spirit)"
        },
        "frames": {}
    }

    # Row 0: Savage Blood Cleave (4 frames)
    for c in range(4):
        step = (c + 1) / 4.0
        frame = create_blood_cleave_frame(step, frame_size)
        x, y = c * frame_size, 0
        diffuse_atlas[y:y+frame_size, x:x+frame_size] = frame
        name = f"blood_cleave_{c}"
        metadata["frames"][name] = {"frame": {"x": x, "y": y, "w": frame_size, "h": frame_size}, "duration_ms": 65}

    # Row 1: Primal Blood Lightning (4 frames)
    for c in range(4):
        step = (c + 1) / 4.0
        frame = create_blood_lightning_frame(step, frame_size)
        x, y = c * frame_size, frame_size
        diffuse_atlas[y:y+frame_size, x:x+frame_size] = frame
        name = f"blood_lightning_{c}"
        metadata["frames"][name] = {"frame": {"x": x, "y": y, "w": frame_size, "h": frame_size}, "duration_ms": 65}

    # Row 2: Putrid Necrotic Frost (4 frames)
    for c in range(4):
        step = (c + 1) / 4.0
        frame = create_necrotic_frost_frame(step, frame_size)
        x, y = c * frame_size, frame_size * 2
        diffuse_atlas[y:y+frame_size, x:x+frame_size] = frame
        name = f"necrotic_frost_{c}"
        metadata["frames"][name] = {"frame": {"x": x, "y": y, "w": frame_size, "h": frame_size}, "duration_ms": 70}

    # Row 3: Feral Evasion Claws & Dust (4 frames)
    for c in range(4):
        step = (c + 1) / 4.0
        f_dust = np.zeros((frame_size, frame_size, 4), dtype=np.uint8)
        np.random.seed(c * 99)
        for _ in range(25):
            dx = int(np.random.normal(frame_size // 2, 20 * step))
            dy = int(np.random.normal(frame_size // 2 + 10, 10))
            if 0 <= dx < frame_size and 0 <= dy < frame_size:
                cv2.circle(f_dust, (dx, dy), int(np.random.uniform(1.5, 3.5)), (30, 35, 45, int(180 * (1 - step))), -1)
        x, y = c * frame_size, frame_size * 3
        diffuse_atlas[y:y+frame_size, x:x+frame_size] = f_dust
        name = f"feral_dodge_{c}"
        metadata["frames"][name] = {"frame": {"x": x, "y": y, "w": frame_size, "h": frame_size}, "duration_ms": 60}

    # Synthesize Normal Map Atlas
    normal_atlas = generate_sobel_normal(diffuse_atlas, intensity=2.4)

    # Save outputs to client/webapp/assets/savage_vfx/
    web_dir = PROJECT_ROOT / "client" / "webapp" / "assets" / "savage_vfx"
    web_dir.mkdir(parents=True, exist_ok=True)

    diffuse_path = web_dir / "savage_vfx_atlas.png"
    normal_path = web_dir / "savage_vfx_atlas_normal.png"
    json_path = web_dir / "savage_vfx_atlas.json"

    cv2.imwrite(str(diffuse_path), diffuse_atlas)
    cv2.imwrite(str(normal_path), normal_atlas)
    with open(json_path, "w", encoding="utf-8") as f:
        json.dump(metadata, f, indent=2, ensure_ascii=False)

    print(f"[+] Savage Primal VFX Atlas generated: {diffuse_path} ({atlas_w}x{atlas_h})")
    print(f"[+] Normal Map PBR Atlas: {normal_path}")
    return diffuse_path, normal_path, json_path


if __name__ == "__main__":
    build_savage_vfx_atlas()
