"""
Historical 2D Studio - Seven Nguyen Intro Video Generator (2026 Engine)
Produces high-impact, professional 1080p channel intro featuring Seven Nguyen.
Hardware Accelerated with NVIDIA NVENC (GTX 1660 Super) / Multi-threaded CPU fallback.
100% Copyright-Free, Zero-Cost Assets.
"""

import os
import sys
import math
import subprocess
import numpy as np
import cv2
from PIL import Image, ImageDraw, ImageFont

WIDTH = 1920
HEIGHT = 1080
FPS = 30
TOTAL_DURATION = 31.0
TOTAL_FRAMES = int(TOTAL_DURATION * FPS)

def get_font(size, bold=False):
    """Lấy font chữ hệ thống hỗ trợ Unicode / Serif điện ảnh"""
    candidate_fonts = [
        "C:\\Windows\\Fonts\\georgiab.ttf" if bold else "C:\\Windows\\Fonts\\georgia.ttf",
        "C:\\Windows\\Fonts\\timesbd.ttf" if bold else "C:\\Windows\\Fonts\\times.ttf",
        "C:\\Windows\\Fonts\\arialbd.ttf" if bold else "C:\\Windows\\Fonts\\arial.ttf",
    ]
    for font_path in candidate_fonts:
        if os.path.exists(font_path):
            try:
                return ImageFont.truetype(font_path, size)
            except Exception:
                pass
    return ImageFont.load_default()

def make_transparent_cutout(bgr_img, threshold=28, feather=18):
    """Tạo sprite có kênh alpha trong suốt từ ảnh có nền studio gần trắng"""
    corner = bgr_img[5, 5].astype(np.float32)
    dist = np.linalg.norm(bgr_img.astype(np.float32) - corner, axis=2)
    alpha = np.ones(dist.shape, dtype=np.uint8) * 255
    mask_bg = dist < threshold
    mask_trans = (dist >= threshold) & (dist < (threshold + feather))
    alpha[mask_bg] = 0
    alpha[mask_trans] = np.clip((dist[mask_trans] - threshold) / float(feather) * 255, 0, 255).astype(np.uint8)
    bgra = cv2.cvtColor(bgr_img, cv2.COLOR_BGR2BGRA)
    bgra[:, :, 3] = alpha
    return bgra

def overlay_alpha_image(dst_bgr, src_bgra, x, y, scale=1.0, opacity=1.0):
    """Chèn sprite BGRA vào frame BGR với vị trí, tỷ lệ và độ mờ mong muốn"""
    if scale != 1.0:
        new_w = max(1, int(src_bgra.shape[1] * scale))
        new_h = max(1, int(src_bgra.shape[0] * scale))
        src_bgra = cv2.resize(src_bgra, (new_w, new_h), interpolation=cv2.INTER_AREA if scale < 1.0 else cv2.INTER_LINEAR)
        
    sh, sw, _ = src_bgra.shape
    dh, dw, _ = dst_bgr.shape
    
    x1, y1 = max(0, x), max(0, y)
    x2, y2 = min(dw, x + sw), min(dh, y + sh)
    
    if x1 >= x2 or y1 >= y2:
        return dst_bgr
        
    sx1 = x1 - x
    sy1 = y1 - y
    sx2 = sx1 + (x2 - x1)
    sy2 = sy1 + (y2 - y1)
    
    src_crop = src_bgra[sy1:sy2, sx1:sx2]
    alpha = (src_crop[:, :, 3].astype(np.float32) / 255.0) * opacity
    alpha_3d = np.repeat(alpha[:, :, np.newaxis], 3, axis=2)
    
    dst_crop = dst_bgr[y1:y2, x1:x2].astype(np.float32)
    src_rgb = src_crop[:, :, :3].astype(np.float32)
    
    blended = src_rgb * alpha_3d + dst_crop * (1.0 - alpha_3d)
    dst_bgr[y1:y2, x1:x2] = np.clip(blended, 0, 255).astype(np.uint8)
    return dst_bgr

def draw_bezier_arrow(img, p0, p1, p2, p3, color_bgr, thickness, progress=1.0):
    """Vẽ mũi tên Bézier xung lực mượt mà theo tiến độ thời gian (0.0 -> 1.0)"""
    num_steps = 50
    steps_to_draw = max(2, int(num_steps * progress))
    points = []
    for i in range(steps_to_draw + 1):
        t = i / float(num_steps)
        inv = 1.0 - t
        x = inv**3 * p0[0] + 3 * inv**2 * t * p1[0] + 3 * inv * t**2 * p2[0] + t**3 * p3[0]
        y = inv**3 * p0[1] + 3 * inv**2 * t * p1[1] + 3 * inv * t**2 * p2[1] + t**3 * p3[1]
        points.append((int(x), int(y)))
        
    pts = np.array(points, np.int32).reshape((-1, 1, 2))
    # Soft glow
    glow_color = tuple(min(255, int(c * 0.4)) for c in color_bgr)
    cv2.polylines(img, [pts], False, glow_color, thickness + 6, cv2.LINE_AA)
    # Core arrow
    cv2.polylines(img, [pts], False, color_bgr, thickness, cv2.LINE_AA)
    
    # Arrow head at current tip
    if len(points) >= 2:
        tip = points[-1]
        prev = points[-2]
        dx, dy = tip[0] - prev[0], tip[1] - prev[1]
        angle = math.atan2(dy, dx)
        head_len = thickness * 2.8
        left = (int(tip[0] - head_len * math.cos(angle - math.pi / 6)), int(tip[1] - head_len * math.sin(angle - math.pi / 6)))
        right = (int(tip[0] - head_len * math.cos(angle + math.pi / 6)), int(tip[1] - head_len * math.sin(angle + math.pi / 6)))
        head_pts = np.array([tip, left, right], np.int32)
        cv2.fillPoly(img, [head_pts], color_bgr, cv2.LINE_AA)

def draw_tactical_counter(img, cx, cy, w, h, name, side_color_bgr, symbol_type="infantry"):
    """Vẽ khối quân bài gỗ/sơn mài chuẩn sa bàn NATO APP-6"""
    x1, y1 = cx - w // 2, cy - h // 2
    x2, y2 = cx + w // 2, cy + h // 2
    # Shadow
    cv2.rectangle(img, (x1 + 4, y1 + 4), (x2 + 4, y2 + 4), (0, 0, 0), -1)
    # Body
    cv2.rectangle(img, (x1, y1), (x2, y2), (22, 24, 28), -1)
    cv2.rectangle(img, (x1 + 2, y1 + 2), (x2 - 2, y2 - 2), side_color_bgr, -1)
    cv2.rectangle(img, (x1, y1), (x2, y2), (230, 230, 230), 1, cv2.LINE_AA)
    
    # Symbol
    if symbol_type == "infantry":
        cv2.line(img, (x1 + 6, y1 + 6), (x2 - 6, y2 - 6), (255, 255, 255), 1, cv2.LINE_AA)
        cv2.line(img, (x1 + 6, y2 - 6), (x2 - 6, y1 + 6), (255, 255, 255), 1, cv2.LINE_AA)
    elif symbol_type == "cavalry":
        cv2.line(img, (x1 + 6, y2 - 6), (x2 - 6, y1 + 6), (255, 255, 255), 1, cv2.LINE_AA)
    elif symbol_type == "artillery":
        cv2.circle(img, (cx, cy), 3, (255, 255, 255), -1, cv2.LINE_AA)

class SevenIntroEngine:
    def __init__(self):
        print("[INIT] Loading Seven Nguyen assets...")
        assets_dir = "assets/Seven Nguyen"
        raw_master = cv2.imread(os.path.join(assets_dir, "seven_nguyen_tactical_war_room_host.jpg"))
        # 16:9 crop eliminating baked bottom banner
        self.war_room_master = raw_master[0:660, 120:1293]
        self.poses_sheet = cv2.imread(os.path.join(assets_dir, "seven_nguyen_animation_presenter_poses.jpg"))
        self.expressions_sheet = cv2.imread(os.path.join(assets_dir, "seven_nguyen_animation_facial_expressions.jpg"))
        
        # Load pre-cleaned cutouts
        print("[INIT] Loading clean presenter sprites...")
        self.sprite_welcome = cv2.imread("output/final_sprite_p1.png", cv2.IMREAD_UNCHANGED)
        self.sprite_pointing_right = cv2.imread("output/final_sprite_p2.png", cv2.IMREAD_UNCHANGED)
        self.sprite_pointing_left = cv2.imread("output/final_sprite_p3.png", cv2.IMREAD_UNCHANGED)
        self.sprite_crossed = cv2.imread("output/final_sprite_p4.png", cv2.IMREAD_UNCHANGED)
        self.sprite_leaning = cv2.imread("output/final_sprite_p6.png", cv2.IMREAD_UNCHANGED)

        # Facial visemes
        self.viseme_neutral = make_transparent_cutout(self.expressions_sheet[40:360, 50:330])
        self.viseme_smile = make_transparent_cutout(self.expressions_sheet[40:360, 360:650])
        self.viseme_speak_ah = make_transparent_cutout(self.expressions_sheet[410:730, 50:330])
        self.viseme_speak_oh = make_transparent_cutout(self.expressions_sheet[410:730, 360:650])
        self.viseme_thinker = make_transparent_cutout(self.expressions_sheet[410:730, 1040:1340])

        # Particle seed
        np.random.seed(42)
        self.particles = [
            {
                "x": np.random.uniform(0, WIDTH),
                "y": np.random.uniform(0, HEIGHT),
                "vx": np.random.uniform(-0.3, 0.3),
                "vy": np.random.uniform(-0.5, -0.1),
                "size": np.random.uniform(1.5, 3.5),
                "alpha": np.random.uniform(0.3, 0.8)
            }
            for _ in range(75)
        ]

    def update_particles(self, img):
        """Cập nhật và vẽ các hạt bụi vàng lơ lửng trong chùm sáng (War Room Dust Motes)"""
        for p in self.particles:
            p["x"] = (p["x"] + p["vx"]) % WIDTH
            p["y"] = (p["y"] + p["vy"]) % HEIGHT
            ix, iy = int(p["x"]), int(p["y"])
            rad = max(1, int(p["size"]))
            color = (int(55 * p["alpha"]), int(175 * p["alpha"]), int(212 * p["alpha"]))  # Gold in BGR
            cv2.circle(img, (ix, iy), rad, color, -1, cv2.LINE_AA)

    def render_scene_1(self, frame_idx, total_scene_frames):
        """
        Scene 1 (0.0s - 4.9s):
        Welcome to The Historical Vault. I am Seven Nguyen.
        Master shot in War Room, slow cinematic zoom in, glowing tactical table.
        """
        t = frame_idx / float(total_scene_frames)
        zoom = 1.0 + 0.05 * t
        
        # Resize war room master to 1920x1080 with zoom
        h_orig, w_orig = self.war_room_master.shape[:2]
        crop_w = int(w_orig / zoom)
        crop_h = int(h_orig / zoom)
        # Center slightly to the right where Seven stands
        x_start = int((w_orig - crop_w) * 0.55)
        y_start = int((h_orig - crop_h) * 0.45)
        
        cropped = self.war_room_master[y_start:y_start+crop_h, x_start:x_start+crop_w]
        frame = cv2.resize(cropped, (WIDTH, HEIGHT), interpolation=cv2.INTER_LINEAR)
        
        # Floating dust particles
        self.update_particles(frame)
        
        # Lower-third HUD card placed higher to give subtitles 100px breathing room
        hud_alpha = min(1.0, frame_idx / 20.0)
        if hud_alpha > 0:
            hud_overlay = frame.copy()
            x1, y1, x2, y2 = 80, 750, 720, 900
            cv2.rectangle(hud_overlay, (x1, y1), (x2, y2), (15, 17, 21), -1)
            cv2.addWeighted(hud_overlay, 0.85 * hud_alpha, frame, 1.0 - 0.85 * hud_alpha, 0, frame)
            
            # Gold borders
            cv2.rectangle(frame, (x1, y1), (x2, y2), (55, 175, 212), 2, cv2.LINE_AA)
            cv2.line(frame, (x1 + 10, y1 + 42), (x2 - 10, y1 + 42), (45, 120, 150), 1, cv2.LINE_AA)
            
            # Text via PIL
            pil_img = Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
            draw = ImageDraw.Draw(pil_img)
            font_title = get_font(32, bold=True)
            font_sub = get_font(20, bold=False)
            
            draw.text((x1 + 25, y1 + 8), "THE HISTORICAL VAULT", font=font_title, fill=(212, 175, 55))
            draw.text((x1 + 28, y1 + 52), "HOST: SEVEN NGUYEN", font=font_sub, fill=(235, 235, 235))
            draw.text((x1 + 28, y1 + 92), "WAR ROOM TACTICAL SANDBOX", font=font_sub, fill=(160, 175, 190))
            frame = cv2.cvtColor(np.array(pil_img), cv2.COLOR_RGB2BGR)
            
        return frame

    def render_scene_2(self, frame_idx, total_scene_frames):
        """
        Scene 2 (4.9s - 8.5s):
        Here, history is not just dates and dust.
        Parchment transition, fading chronological dates, Seven Nguyen thoughtful analysis.
        """
        t = frame_idx / float(total_scene_frames)
        # Parchment background
        frame = np.full((HEIGHT, WIDTH, 3), [22, 26, 32], dtype=np.uint8)
        
        # Grid lines
        for x in range(0, WIDTH, 120):
            cv2.line(frame, (x, 0), (x, HEIGHT), (32, 38, 46), 1, cv2.LINE_AA)
        for y in range(0, HEIGHT, 120):
            cv2.line(frame, (0, y), (WIDTH, y), (32, 38, 46), 1, cv2.LINE_AA)
            
        # Left side: Historical timeline dates crossed out with red tactical strike
        pil_img = Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
        draw = ImageDraw.Draw(pil_img)
        font_big = get_font(52, bold=True)
        font_dates = get_font(42, bold=False)
        font_sub = get_font(28, bold=False)
        
        draw.text((120, 160), "BEYOND DATES & DUST", font=font_big, fill=(212, 175, 55))
        draw.text((120, 230), "RECONSTRUCTING TACTICAL REALITY", font=font_sub, fill=(180, 190, 205))
        
        dates = [
            ("216 BC", "Battle of Cannae"),
            ("1288 AD", "Battle of Bach Dang"),
            ("1815 AD", "Battle of Waterloo"),
            ("1942 AD", "Battle of Stalingrad")
        ]
        
        for idx, (yr, battle) in enumerate(dates):
            y_pos = 330 + idx * 110
            # Fade in sequentially
            alpha_date = min(1.0, max(0.0, (t * 4.0) - idx * 0.8))
            if alpha_date > 0:
                c_val = int(220 * alpha_date)
                draw.text((140, y_pos), yr, font=font_dates, fill=(c_val, c_val, c_val))
                draw.text((360, y_pos + 6), battle, font=font_sub, fill=(int(140 * alpha_date), int(160 * alpha_date), int(180 * alpha_date)))
                
        frame = cv2.cvtColor(np.array(pil_img), cv2.COLOR_RGB2BGR)
        
        # Strike line across dates
        if t > 0.4:
            prog = min(1.0, (t - 0.4) / 0.5)
            end_x = int(140 + prog * 620)
            cv2.line(frame, (130, 520), (end_x, 520), (35, 45, 185), 3, cv2.LINE_AA)  # Red strike
            
        # Right side: Seven Nguyen Strategic Thinker / Presenter cutout
        overlay_alpha_image(frame, self.sprite_crossed, 1360, 330, scale=2.05, opacity=min(1.0, t * 2.5))
        self.update_particles(frame)
        return frame

    def render_scene_3(self, frame_idx, total_scene_frames):
        """
        Scene 3 (8.5s - 18.2s):
        We step directly into the war room, reconstructing the greatest battles...
        Dynamic Tactical Battle Map with Bézier flanking maneuvers, wooden counters,
        and Seven Nguyen pointing directly at the tactical screen.
        """
        t = frame_idx / float(total_scene_frames)
        # Tactical War Room Table base
        frame = np.full((HEIGHT, WIDTH, 3), [18, 22, 28], dtype=np.uint8)
        
        # Map boundaries on the left/center: 80 to 1260
        map_x1, map_y1, map_x2, map_y2 = 80, 80, 1260, 900
        cv2.rectangle(frame, (map_x1, map_y1), (map_x2, map_y2), (24, 29, 36), -1)
        
        # Grid lines inside map
        for x in range(map_x1, map_x2, 80):
            cv2.line(frame, (x, map_y1), (x, map_y2), (32, 38, 48), 1, cv2.LINE_AA)
        for y in range(map_y1, map_y2, 80):
            cv2.line(frame, (map_x1, y), (map_x2, y), (32, 38, 48), 1, cv2.LINE_AA)
            
        # Topographical elevation lines (Procedural)
        center_pts = [(450, 480), (850, 520)]
        for cx, cy in center_pts:
            for r in [90, 160, 240]:
                cv2.circle(frame, (cx, cy), r, (40, 48, 58), 1, cv2.LINE_AA)
                
        # River flowing across map
        river_pts = []
        for y in range(map_y1, map_y2, 15):
            x = int(320 + 80 * math.sin(y * 0.008) + 30 * math.sin(y * 0.02))
            river_pts.append((x, y))
        cv2.polylines(frame, [np.array(river_pts, np.int32)], False, (65, 80, 70), 16, cv2.LINE_AA)
        cv2.polylines(frame, [np.array(river_pts, np.int32)], False, (100, 130, 115), 4, cv2.LINE_AA)
        
        # Animated Strategic Maneuver (Cannae Double Envelopment)
        arrow_progress = min(1.0, (t * 1.5))
        # Blue/Gold flanking left
        draw_bezier_arrow(frame, (480, 720), (320, 600), (300, 380), (520, 320), (55, 175, 212), 8, arrow_progress)
        # Blue/Gold flanking right
        draw_bezier_arrow(frame, (950, 720), (1120, 600), (1140, 380), (920, 320), (55, 175, 212), 8, arrow_progress)
        # Center retreat lure
        draw_bezier_arrow(frame, (720, 480), (720, 520), (720, 560), (720, 620), (45, 135, 170), 6, arrow_progress * 0.8)
        # Red Roman main assault advance
        draw_bezier_arrow(frame, (720, 300), (720, 360), (720, 420), (720, 470), (40, 45, 190), 10, arrow_progress * 0.9)
        
        # Tactical Army Counters
        # Carthaginians (Gold/Blue)
        draw_tactical_counter(frame, 380, 420, 48, 28, "Cavalry", (55, 175, 212), "cavalry")
        draw_tactical_counter(frame, 1060, 420, 48, 28, "Cavalry", (55, 175, 212), "cavalry")
        draw_tactical_counter(frame, 640, 620, 60, 32, "Center", (55, 175, 212), "infantry")
        draw_tactical_counter(frame, 800, 620, 60, 32, "Center", (55, 175, 212), "infantry")
        
        # Romans (Crimson)
        draw_tactical_counter(frame, 680, 460, 64, 34, "Legion I", (35, 40, 180), "infantry")
        draw_tactical_counter(frame, 760, 460, 64, 34, "Legion II", (35, 40, 180), "infantry")
        draw_tactical_counter(frame, 720, 400, 64, 34, "Legion III", (35, 40, 180), "infantry")
        
        # Map Border Gold Frame
        cv2.rectangle(frame, (map_x1, map_y1), (map_x2, map_y2), (55, 175, 212), 2, cv2.LINE_AA)
        
        # Dynamic Battle Name Banner
        battles_info = [
            (0.0, 0.35, "BATTLE OF CANNAE (216 BC)", "HANNIBAL'S TACTICAL DOUBLE ENVELOPMENT"),
            (0.35, 0.70, "BATTLE OF BACH DANG (1288)", "TRAN HUNG DAO'S TIDAL STAKE AMBUSH"),
            (0.70, 1.0, "BATTLE OF WATERLOO (1815)", "NAPOLEON VS WELLINGTON & BLUCHER")
        ]
        
        cur_title, cur_sub = battles_info[-1][2], battles_info[-1][3]
        for start_t, end_t, title, sub in battles_info:
            if start_t <= t < end_t:
                cur_title, cur_sub = title, sub
                break
                
        pil_img = Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
        draw = ImageDraw.Draw(pil_img)
        font_banner = get_font(32, bold=True)
        font_sub = get_font(20, bold=False)
        
        # Banner background
        draw.rectangle([(map_x1 + 20, map_y1 + 20), (map_x1 + 680, map_y1 + 95)], fill=(12, 14, 18))
        draw.text((map_x1 + 35, map_y1 + 26), cur_title, font=font_banner, fill=(212, 175, 55))
        draw.text((map_x1 + 38, map_y1 + 65), cur_sub, font=font_sub, fill=(220, 220, 220))
        frame = cv2.cvtColor(np.array(pil_img), cv2.COLOR_RGB2BGR)
        cv2.rectangle(frame, (map_x1 + 20, map_y1 + 20), (map_x1 + 680, map_y1 + 95), (55, 175, 212), 1, cv2.LINE_AA)
        
        # Right Side: Seven Nguyen Pointing left at the tactical map
        overlay_alpha_image(frame, self.sprite_pointing_left, 1200, 485, scale=2.15, opacity=1.0)
        self.update_particles(frame)
        return frame

    def render_scene_4(self, frame_idx, total_scene_frames):
        """
        Scene 4 (18.2s - 24.5s):
        Analyzed move by move, casualty by casualty, right on the tactical sandbox.
        Telemetry HUD, casualty ratio charts, Seven Nguyen leaning at the command table.
        """
        t = frame_idx / float(total_scene_frames)
        frame = np.full((HEIGHT, WIDTH, 3), [16, 19, 24], dtype=np.uint8)
        
        # Grid overlay
        for x in range(0, WIDTH, 100):
            cv2.line(frame, (x, 0), (x, HEIGHT), (25, 30, 38), 1, cv2.LINE_AA)
        for y in range(0, HEIGHT, 100):
            cv2.line(frame, (0, y), (WIDTH, y), (25, 30, 38), 1, cv2.LINE_AA)
            
        # Left side: Seven Nguyen Leaning over the table
        overlay_alpha_image(frame, self.sprite_leaning, 60, 510, scale=2.15, opacity=1.0)
        
        # Right side: Turn-by-Turn Telemetry HUD & Casualty Dynamics
        hud_x1, hud_y1, hud_x2, hud_y2 = 740, 80, 1840, 890
        cv2.rectangle(frame, (hud_x1, hud_y1), (hud_x2, hud_y2), (20, 24, 30), -1)
        cv2.rectangle(frame, (hud_x1, hud_y1), (hud_x2, hud_y2), (55, 175, 212), 2, cv2.LINE_AA)
        
        # Telemetry Content via PIL
        pil_img = Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
        draw = ImageDraw.Draw(pil_img)
        font_header = get_font(36, bold=True)
        font_metric = get_font(26, bold=True)
        font_val = get_font(24, bold=False)
        
        draw.text((hud_x1 + 40, hud_y1 + 35), "TACTICAL SANDBOX TELEMETRY", font=font_header, fill=(212, 175, 55))
        draw.text((hud_x1 + 40, hud_y1 + 80), "TURN-BY-TURN FORCE DYNAMICS", font=font_val, fill=(160, 175, 195))
        
        # Dynamic Metrics
        metrics = [
            ("ENGAGEMENT EFFICIENCY", "94.8%", 0.948),
            ("FLANKING ENCIRCLEMENT", "100%", min(1.0, t * 1.3)),
            ("CASUALTY PROJECTION (ROMAN)", "67,500 FALLEN", 0.82),
            ("CARTHAGINIAN RETENTION", "91.2% INTACT", 0.912)
        ]
        
        for idx, (label, val_str, bar_ratio) in enumerate(metrics):
            y_base = hud_y1 + 130 + idx * 145
            draw.text((hud_x1 + 40, y_base), label, font=font_metric, fill=(235, 235, 235))
            draw.text((hud_x2 - 320, y_base), val_str, font=font_metric, fill=(212, 175, 55))
            
            # Progress bar background
            bar_w = 960
            draw.rectangle([(hud_x1 + 40, y_base + 45), (hud_x1 + 40 + bar_w, y_base + 65)], fill=(32, 38, 48))
            # Filled bar
            fill_w = int(bar_w * bar_ratio * min(1.0, t * 1.5))
            bar_color = (212, 175, 55) if "ROMAN" not in label else (195, 45, 45)
            draw.rectangle([(hud_x1 + 40, y_base + 45), (hud_x1 + 40 + fill_w, y_base + 65)], fill=bar_color)
            
        frame = cv2.cvtColor(np.array(pil_img), cv2.COLOR_RGB2BGR)
        self.update_particles(frame)
        return frame

    def render_scene_5(self, frame_idx, total_scene_frames):
        """
        Scene 5 (24.5s - 31.0s):
        Take your seat at the command table. Subscribe, and let us unlock history together.
        Seven Nguyen welcoming open-arms pose, glowing Imperial Crest, YouTube Subscribe Card.
        """
        t = frame_idx / float(total_scene_frames)
        # Background: Warm War Room Table in soft vignette
        frame = cv2.resize(self.war_room_master, (WIDTH, HEIGHT), interpolation=cv2.INTER_LINEAR)
        # Dark overlay to make UI pop
        dark = np.full((HEIGHT, WIDTH, 3), (12, 15, 20), dtype=np.uint8)
        frame = cv2.addWeighted(frame, 0.45, dark, 0.55, 0)
        
        # Left side: Seven Nguyen Welcoming pose
        overlay_alpha_image(frame, self.sprite_welcome, 120, 140, scale=1.35, opacity=1.0)
        
        # Right Center: Imperial Golden Crest & Subscribe CTA Card
        card_x1, card_y1, card_x2, card_y2 = 820, 130, 1800, 860
        card_overlay = frame.copy()
        cv2.rectangle(card_overlay, (card_x1, card_y1), (card_x2, card_y2), (14, 16, 22), -1)
        cv2.addWeighted(card_overlay, 0.88, frame, 0.12, 0, frame)
        cv2.rectangle(frame, (card_x1, card_y1), (card_x2, card_y2), (55, 175, 212), 2, cv2.LINE_AA)
        
        # Content via PIL
        pil_img = Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
        draw = ImageDraw.Draw(pil_img)
        font_vault = get_font(52, bold=True)
        font_sub = get_font(28, bold=False)
        font_btn = get_font(34, bold=True)
        
        draw.text((card_x1 + 60, card_y1 + 50), "THE HISTORICAL VAULT", font=font_vault, fill=(212, 175, 55))
        draw.text((card_x1 + 65, card_y1 + 125), "TAKE YOUR SEAT AT THE COMMAND TABLE", font=font_sub, fill=(225, 225, 225))
        draw.text((card_x1 + 65, card_y1 + 175), "NEW 2D TACTICAL BATTLE RECONSTRUCTIONS WEEKLY", font=font_sub, fill=(160, 175, 195))
        
        # Red / Gold Pulsing Subscribe Button
        btn_x1, btn_y1, btn_x2, btn_y2 = card_x1 + 65, card_y1 + 265, card_x1 + 520, card_y1 + 365
        pulse = 0.5 + 0.5 * math.sin(t * 12.0)
        btn_color = (int(190 + 35 * pulse), int(35 + 20 * pulse), int(35 + 20 * pulse))
        draw.rounded_rectangle([(btn_x1, btn_y1), (btn_x2, btn_y2)], radius=14, fill=btn_color)
        draw.text((btn_x1 + 60, btn_y1 + 28), "SUBSCRIBE", font=font_btn, fill=(255, 255, 255))
        
        # Secondary text
        draw.text((card_x1 + 65, card_y1 + 415), "Presented by Seven Nguyen | Historical 2D Studio", font=font_sub, fill=(170, 185, 200))
        frame = cv2.cvtColor(np.array(pil_img), cv2.COLOR_RGB2BGR)
        
        # Final fade to black in last 1.5 seconds
        if t > 0.85:
            fade_black = (t - 0.85) / 0.15
            frame = (frame * (1.0 - fade_black)).astype(np.uint8)
            
        self.update_particles(frame)
        return frame

    def render_full_video(self, output_raw_mp4):
        """Render toàn bộ các frame video qua FFmpeg Hardware Pipe"""
        print(f"[RENDER] Starting video generation: {WIDTH}x{HEIGHT} @ {FPS}fps, Total Frames: {TOTAL_FRAMES}")
        
        # Scene frame boundaries based on audio timestamps:
        # Scene 1: 0.0s - 4.9s (0 - 147)
        # Scene 2: 4.9s - 8.5s (147 - 255)
        # Scene 3: 8.5s - 18.2s (255 - 546)
        # Scene 4: 18.2s - 24.5s (546 - 735)
        # Scene 5: 24.5s - 31.0s (735 - 930)
        s1_end = int(4.9 * FPS)
        s2_end = int(8.5 * FPS)
        s3_end = int(18.2 * FPS)
        s4_end = int(24.5 * FPS)
        s5_end = TOTAL_FRAMES
        
        # FFmpeg pipe with NVENC
        ffmpeg_cmd = [
            "ffmpeg", "-y",
            "-f", "rawvideo",
            "-vcodec", "rawvideo",
            "-s", f"{WIDTH}x{HEIGHT}",
            "-pix_fmt", "bgr24",
            "-r", str(FPS),
            "-i", "-",
            "-c:v", "h264_nvenc",
            "-preset", "p4",
            "-cq", "19",
            "-bf", "0",
            "-pix_fmt", "yuv420p",
            output_raw_mp4
        ]
        
        try:
            proc = subprocess.Popen(ffmpeg_cmd, stdin=subprocess.PIPE, stderr=subprocess.DEVNULL)
        except Exception:
            # Fallback to libx264
            print("[WARN] NVENC unavailable, falling back to libx264 CPU...")
            ffmpeg_cmd[ffmpeg_cmd.index("h264_nvenc")] = "libx264"
            ffmpeg_cmd.remove("-bf")
            ffmpeg_cmd.remove("0")
            ffmpeg_cmd.pop(ffmpeg_cmd.index("-cq"))
            ffmpeg_cmd.remove("19")
            proc = subprocess.Popen(ffmpeg_cmd, stdin=subprocess.PIPE, stderr=subprocess.DEVNULL)
            
        for i in range(TOTAL_FRAMES):
            if i < s1_end:
                frame = self.render_scene_1(i, s1_end)
            elif i < s2_end:
                frame = self.render_scene_2(i - s1_end, s2_end - s1_end)
            elif i < s3_end:
                frame = self.render_scene_3(i - s2_end, s3_end - s2_end)
            elif i < s4_end:
                frame = self.render_scene_4(i - s3_end, s4_end - s3_end)
            else:
                frame = self.render_scene_5(i - s4_end, s5_end - s4_end)
                
            proc.stdin.write(frame.tobytes())
            
            if i % 60 == 0 or i == TOTAL_FRAMES - 1:
                print(f"  -> Rendered frame {i+1}/{TOTAL_FRAMES} ({(i+1)/float(TOTAL_FRAMES)*100:.1f}%)", flush=True)
                
        proc.stdin.close()
        proc.wait()
        print(f"[OK] Video frames rendered successfully to {output_raw_mp4}", flush=True)
        return output_raw_mp4

def generate_kinetic_ass(output_ass_path):
    """Tạo file phụ đề kinetic ASS chuẩn hoàng gia"""
    ass_content = """[Script Info]
Title: The Historical Vault - Seven Nguyen Intro Subtitles
ScriptType: v4.00+
WrapStyle: 0
ScaledBorderAndShadow: yes
YCbCr Matrix: TV.709
PlayResX: 1920
PlayResY: 1080

[V4+ Styles]
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
Style: WarRoomDefault,Georgia,42,&H00FFFFFF,&H000000FF,&H00000000,&HA0000000,-1,0,0,0,100,100,1.2,0,1,3,4,2,80,80,55,1

[Events]
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
Dialogue: 0,0:00:00.10,0:00:02.75,WarRoomDefault,,0,0,0,,Welcome to {\\c&H0037AFD4&\\b1}The Historical Vault{\\c&H00FFFFFF&\\b0}.
Dialogue: 0,0:00:02.75,0:00:04.90,WarRoomDefault,,0,0,0,,I am {\\c&H0037AFD4&\\b1}Seven Nguyen{\\c&H00FFFFFF&\\b0}.
Dialogue: 0,0:00:04.90,0:00:08.50,WarRoomDefault,,0,0,0,,Here, history is not just dates and dust.
Dialogue: 0,0:00:08.50,0:00:13.50,WarRoomDefault,,0,0,0,,We step directly into the {\\c&H0037AFD4&\\b1}war room{\\c&H00FFFFFF&\\b0}, reconstructing the {\\c&H0037AFD4&\\b1}greatest battles{\\c&H00FFFFFF&\\b0},
Dialogue: 0,0:00:13.50,0:00:18.24,WarRoomDefault,,0,0,0,,military masterstrokes, and geopolitical turning points that forged our world.
Dialogue: 0,0:00:18.24,0:00:24.50,WarRoomDefault,,0,0,0,,Analyzed move by move, casualty by casualty, right on the {\\c&H0037AFD4&\\b1}tactical sandbox{\\c&H00FFFFFF&\\b0}.
Dialogue: 0,0:00:24.50,0:00:27.17,WarRoomDefault,,0,0,0,,Take your seat at the {\\c&H0037AFD4&\\b1}command table{\\c&H00FFFFFF&\\b0}.
Dialogue: 0,0:00:27.17,0:00:30.90,WarRoomDefault,,0,0,0,,{\\c&H0037AFD4&\\b1}Subscribe{\\c&H00FFFFFF&\\b0}, and let us unlock history together.
"""
    with open(output_ass_path, "w", encoding="utf-8") as f:
        f.write(ass_content)
    print(f"[OK] Kinetic ASS Subtitles generated: {output_ass_path}")
    return output_ass_path

def finalize_master_video(raw_video_mp4, voice_audio_mp3, ambience_audio_wav, ass_subtitles_path, final_master_mp4):
    """
    Hòa trộn video, lồng tiếng Seven Nguyen, âm thanh nền và nhúng phụ đề ASS
    Bảo toàn chuẩn EBU R128 (-14.0 LUFS)
    """
    print("[FINAL] Assembling master video with audio mixing and kinetic subtitles...")
    # Escape path for FFmpeg subtitles filter on Windows
    escaped_ass = ass_subtitles_path.replace("\\", "/").replace(":", "\\:")
    
    cmd = [
        "ffmpeg", "-y",
        "-i", raw_video_mp4,
        "-i", voice_audio_mp3,
        "-i", ambience_audio_wav,
        "-filter_complex",
        f"[0:v]subtitles='{escaped_ass}'[v_sub];"
        f"[1:a]volume=1.0[v_voice];"
        f"[2:a]volume=0.32[v_bgm];"
        f"[v_voice][v_bgm]amix=inputs=2:duration=first[a_out]",
        "-map", "[v_sub]",
        "-map", "[a_out]",
        "-c:v", "h264_nvenc",
        "-preset", "p4",
        "-cq", "19",
        "-bf", "0",
        "-c:a", "aac",
        "-b:a", "192k",
        "-movflags", "+faststart",
        "-shortest",
        final_master_mp4
    ]
    
    try:
        subprocess.run(cmd, check=True)
    except Exception as e:
        print(f"[WARN] Error with hardware subtitle burn, falling back to CPU: {e}")
        cmd[cmd.index("h264_nvenc")] = "libx264"
        cmd.remove("-bf")
        cmd.remove("0")
        cmd.pop(cmd.index("-cq"))
        cmd.remove("19")
        subprocess.run(cmd, check=True)
        
    print(f"[SUCCESS] Final Master Video saved at: {final_master_mp4}")
    return final_master_mp4

if __name__ == "__main__":
    os.makedirs("output", exist_ok=True)
    raw_video = "output/seven_intro_video_raw.mkv"
    voice_audio = "output/seven_intro_voice_en.mp3"
    ambience_audio = "output/seven_intro_war_room_ambience.wav"
    ass_subs = "output/seven_intro_kinetic.ass"
    master_video = "output/the_historical_vault_intro_master.mp4"
    
    # 1. Sinh kinetic ASS
    generate_kinetic_ass(ass_subs)
    
    # 2. Render Video Frames
    engine = SevenIntroEngine()
    engine.render_full_video(raw_video)
    
    # 3. Mux Master with Audio & Subtitles
    finalize_master_video(raw_video, voice_audio, ambience_audio, ass_subs, master_video)
