"""
VidPuls Cinema3D – 3D-Dialogvideos mit echtem Lip-Sync (Rhubarb).

Animierte 3D-Figuren: Mundformen, Mimik und Gestik pro Frame,
synchron zur Dialog-Audio-Spur.
"""

from __future__ import annotations

import math
import subprocess
import tempfile
from pathlib import Path

import numpy as np

ROOT = Path(__file__).resolve().parents[1]


def _find_ffmpeg() -> str | None:
    from jam.tiktok_remix import _find_ffmpeg

    return _find_ffmpeg()


def available() -> bool:
    try:
        import numpy as np  # noqa: F401
        from PIL import Image  # noqa: F401

        return _find_ffmpeg() is not None
    except ImportError:
        return False


def _active_key(speaker: str) -> str | None:
    sp = (speaker or "").lower()
    if sp in ("sarah", "female", "lisa"):
        return "sarah"
    if sp == "laura":
        return "laura"
    return None


def _expression_for_shape(shape: str, open_amount: float) -> float:
    shape = (shape or "X").upper()
    base = {
        "A": 0.85,
        "E": 0.75,
        "G": 0.65,
        "C": 0.55,
        "B": 0.45,
        "H": 0.40,
        "D": 0.35,
        "F": 0.30,
        "X": 0.12,
    }.get(shape, 0.35)
    return min(1.0, base + open_amount * 0.15)


def _character_poses(
    speaker: str,
    t: float,
    frame_idx: int,
    cues: list,
    *,
    has_cues: bool,
) -> tuple["CharacterPose", "CharacterPose"]:
    from jam.dialogue_scene import CharacterPose
    from jam.lipsync_rhubarb import mouth_open_at, shape_at

    active = _active_key(speaker)
    sarah = CharacterPose()
    laura = CharacterPose()
    breath = 0.06 + 0.04 * math.sin(t * 2.8)
    idle_gesture = t * 3.5 + frame_idx * 0.04

    if active is None:
        sarah = CharacterPose(
            mouth_open=breath,
            mouth_shape="X",
            expression=0.08,
            gesture=idle_gesture,
        )
        laura = CharacterPose(
            mouth_open=breath * 0.9,
            mouth_shape="X",
            expression=0.08,
            gesture=idle_gesture + 0.6,
        )
        return sarah, laura

    t_audio = t
    if has_cues:
        mouth = mouth_open_at(cues, t_audio)
        shape = shape_at(cues, t_audio)
    else:
        mouth = 0.35 + 0.25 * abs(math.sin(t * 11.0))
        shape = "C" if mouth > 0.45 else "B"

    talk_gesture = t * 7.5 + mouth * 2.0
    expr = _expression_for_shape(shape, mouth)

    if active == "sarah":
        sarah = CharacterPose(
            speaking=True,
            mouth_open=mouth,
            mouth_shape=shape,
            expression=expr,
            gesture=talk_gesture,
        )
        laura = CharacterPose(
            speaking=False,
            mouth_open=0.06 + 0.03 * math.sin(t * 2.2),
            mouth_shape="X",
            expression=0.22 + mouth * 0.12,
            gesture=idle_gesture,
            listen_tilt=0.18,
        )
    else:
        laura = CharacterPose(
            speaking=True,
            mouth_open=mouth,
            mouth_shape=shape,
            expression=expr,
            gesture=talk_gesture,
        )
        sarah = CharacterPose(
            speaking=False,
            mouth_open=0.06 + 0.03 * math.sin(t * 2.2),
            mouth_shape="X",
            expression=0.22 + mouth * 0.12,
            gesture=idle_gesture,
            listen_tilt=-0.18,
        )
    return sarah, laura


def render_dialogue_shot(
    speaker: str,
    out: Path,
    *,
    duration: float,
    width: int,
    height: int,
    audio_part: Path | None = None,
    fps: int = 24,
    visual_style: str = "3d",
    assets_dir: Path | None = None,
) -> bool:
    ff = _find_ffmpeg()
    if not ff or duration <= 0:
        return False

    from jam.dialogue_scene import _render_3d_frame_animated
    from jam.lipsync_rhubarb import analyze, available as rhubarb_ok

    compose_w = min(width, 1080)
    compose_h = int(height * (compose_w / width))
    if compose_h % 2:
        compose_h += 1

    cues: list = []
    if audio_part and audio_part.is_file() and rhubarb_ok():
        cues = analyze(audio_part, dialog=True)

    active = _active_key(speaker)
    frames = max(1, int(duration * fps))
    out.parent.mkdir(parents=True, exist_ok=True)

    with tempfile.TemporaryDirectory(prefix="cinema3d_") as tmp:
        tmp_p = Path(tmp)
        for fi in range(frames):
            t = fi / fps
            sarah_pose, laura_pose = _character_poses(
                speaker, t, fi, cues, has_cues=bool(cues),
            )
            img = _render_3d_frame_animated(
                (compose_w, compose_h),
                sarah=sarah_pose,
                laura=laura_pose,
            )

            ox = 0
            if active == "sarah":
                ox = int(-5 * math.sin(t * 0.9))
            elif active == "laura":
                ox = int(5 * math.sin(t * 0.9))
            if ox:
                from PIL import Image

                canvas = Image.new("RGB", (compose_w, compose_h))
                canvas.paste(img, (ox, 0))
                img = canvas

            img.save(tmp_p / f"f_{fi:05d}.png")

        scale = ""
        if width != compose_w or height != compose_h:
            scale = f",scale={width}:{height}:flags=lanczos"

        zoom_side = "0.46" if active == "sarah" else ("0.54" if active == "laura" else "0.5")
        vf = (
            f"zoompan=z='1.04+0.025*sin(2*PI*on/{max(frames, 1)})':"
            f"x='(iw-iw/zoom)*{zoom_side}':"
            f"y='(ih-ih/zoom)*0.36':"
            f"d={frames}:s={compose_w}x{compose_h},"
            f"eq=saturation=1.15:contrast=1.06:brightness=0.02,"
            f"format=yuv420p{scale}"
        )

        from jam.gpu_render import ffmpeg_encode_args

        cmd = [
            ff,
            "-y",
            "-framerate",
            str(fps),
            "-i",
            str(tmp_p / "f_%05d.png"),
            "-t",
            f"{duration:.3f}",
            "-vf",
            vf,
            *ffmpeg_encode_args(crf_fallback="16" if width >= 2160 else "18"),
            "-pix_fmt",
            "yuv420p",
            "-an",
            str(out),
        ]
        try:
            subprocess.run(
                cmd,
                capture_output=True,
                timeout=max(300, int(duration * 25)),
                check=True,
            )
        except (subprocess.CalledProcessError, subprocess.TimeoutExpired, OSError):
            return False
    return out.is_file() and out.stat().st_size > 5000


def render_full_dialogue(
    shot_plan: list[tuple[str, float, str, Path | None]],
    out_silent: Path,
    *,
    width: int,
    height: int,
    visual_style: str = "3d",
    work_dir: Path | None = None,
    fps: int = 24,
) -> tuple[bool, int]:
    ff = _find_ffmpeg()
    if not ff:
        return False, 0
    work = work_dir or out_silent.parent
    shots: list[Path] = []
    n = 0
    for i, (speaker, dur, _line, audio_part) in enumerate(shot_plan):
        seg = work / f"cinema3d_{i:03d}.mp4"
        if render_dialogue_shot(
            speaker,
            seg,
            duration=dur,
            width=width,
            height=height,
            audio_part=audio_part,
            fps=fps,
            visual_style=visual_style,
        ):
            shots.append(seg)
            n += 1
    if not shots:
        return False, 0
    lst = work / "cinema3d_concat.txt"
    lst.write_text("\n".join(f"file '{p.resolve().as_posix()}'" for p in shots), encoding="utf-8")
    try:
        subprocess.run(
            [ff, "-y", "-f", "concat", "-safe", "0", "-i", str(lst), "-c", "copy", str(out_silent)],
            capture_output=True,
            timeout=300,
            check=True,
        )
    except (subprocess.CalledProcessError, subprocess.TimeoutExpired, OSError):
        return False, n
    return out_silent.is_file(), n
