from __future__ import annotations

import argparse
import math
from pathlib import Path

from PIL import Image


PROJECT = Path(__file__).resolve().parents[1]
POSES = PROJECT / "bibi" / "poses"
KEY_POSES = PROJECT / "bibi" / "key_poses"
SOURCE_POSES = PROJECT / "bibi" / "source_poses"


def load_pose(name: str) -> Image.Image:
    return Image.open(POSES / f"{name}.png").convert("RGBA")


def load_source_pose(name: str) -> Image.Image:
    path = SOURCE_POSES / f"{name}.png"
    if path.exists():
        return Image.open(path).convert("RGBA")
    return load_pose(name)


def load_key_pose(name: str) -> Image.Image:
    path = KEY_POSES / f"{name}_v2.png"
    if path.exists():
        image = Image.open(path).convert("RGBA")
        bbox = image.getchannel("A").getbbox()
        if bbox:
            image = image.crop(bbox)
        if image.height > 760:
            width = max(1, round(image.width * 760 / image.height))
            image = image.resize((width, 760), Image.Resampling.LANCZOS)
        return image
    return load_pose(name)


def save_pose(name: str, image: Image.Image) -> Path:
    path = POSES / name
    image.save(path)
    return path


def lengthen_legs(
    image: Image.Image,
    *,
    split_ratio: float = 0.46,
    lower_scale: float = 1.24,
) -> Image.Image:
    """Raise the apparent waistline while preserving the face and foot anchor."""

    image = image.convert("RGBA")
    width, height = image.size
    split_y = max(1, min(height - 1, round(height * split_ratio)))
    lower_height = max(1, round((height - split_y) * lower_scale))
    output_height = split_y + lower_height
    mesh = [
        (
            (0, 0, width, split_y),
            (0, 0, 0, split_y, width, split_y, width, 0),
        ),
        (
            (0, split_y, width, output_height),
            (0, split_y, 0, height, width, height, width, split_y),
        ),
    ]
    return image.convert("RGBa").transform(
        (width, output_height),
        Image.Transform.MESH,
        mesh,
        resample=Image.Resampling.BICUBIC,
    ).convert("RGBA")


def build_long_base_poses() -> list[Path]:
    names = ("idle", "look", "blink", "happy", "wave", "sleep", "talk", "drag")
    return [save_pose(f"{name}.png", lengthen_legs(load_source_pose(name))) for name in names]


def anchored_transform(
    image: Image.Image,
    scale_x: float = 1.0,
    scale_y: float = 1.0,
    offset_x: float = 0.0,
    offset_y: float = 0.0,
    anchor_x_ratio: float = 0.5,
) -> Image.Image:
    width, height = image.size
    anchor_x_ratio = max(0.0, min(1.0, float(anchor_x_ratio)))
    scale_x = max(0.01, float(scale_x))
    scale_y = max(0.01, float(scale_y))
    anchor_x = width * anchor_x_ratio
    anchor_y = float(height)
    inverse = (
        1.0 / scale_x,
        0.0,
        anchor_x - (anchor_x + offset_x) / scale_x,
        0.0,
        1.0 / scale_y,
        anchor_y - (anchor_y + offset_y) / scale_y,
    )
    return image.transform(
        image.size,
        Image.Transform.AFFINE,
        inverse,
        resample=Image.Resampling.BICUBIC,
        fillcolor=(0, 0, 0, 0),
    )


def anchored_scale(image: Image.Image, scale_x: float = 1.0, scale_y: float = 1.0) -> Image.Image:
    return anchored_transform(image, scale_x=scale_x, scale_y=scale_y)


def blend_pose(base: Image.Image, target: Image.Image, alpha: float) -> Image.Image:
    if base.size != target.size:
        target = target.resize(base.size, Image.Resampling.BICUBIC)
    return Image.blend(base.convert("RGBA"), target.convert("RGBA"), alpha)


def smoothstep(start: float, end: float, value: float) -> float:
    if end <= start:
        return 0.0
    ratio = max(0.0, min(1.0, (value - start) / (end - start)))
    return ratio * ratio * (3.0 - 2.0 * ratio)


def weighted_upper_transform(
    image: Image.Image,
    *,
    head_x: float = 0.0,
    head_y: float = 0.0,
    head_tilt: float = 0.0,
    upper_x: float = 0.0,
    upper_y: float = 0.0,
) -> Image.Image:
    """Deform the upper body while keeping the feet and canvas fixed."""

    width, height = image.size
    columns, rows = 8, 16
    xs = [round(width * index / columns) for index in range(columns + 1)]
    ys = [round(height * index / rows) for index in range(rows + 1)]
    center_x = width * 0.50
    head_center_y = height * 0.21

    def mapped(x: float, y: float) -> tuple[float, float]:
        ny = y / max(1.0, height)
        head_weight = 1.0 - smoothstep(0.18, 0.44, ny)
        upper_weight = 1.0 - smoothstep(0.30, 0.74, ny)
        angle = head_tilt * head_weight
        dx = x - center_x
        dy = y - head_center_y
        cos_a = math.cos(angle)
        sin_a = math.sin(angle)
        rotated_x = center_x + cos_a * dx + sin_a * dy
        rotated_y = head_center_y - sin_a * dx + cos_a * dy
        source_x = rotated_x - head_x * head_weight - upper_x * upper_weight
        source_y = rotated_y - head_y * head_weight - upper_y * upper_weight
        return (
            max(0.0, min(width - 1.0, source_x)),
            max(0.0, min(height - 1.0, source_y)),
        )

    mesh = []
    for row in range(rows):
        for column in range(columns):
            x0, x1 = xs[column], xs[column + 1]
            y0, y1 = ys[row], ys[row + 1]
            nw = mapped(x0, y0)
            sw = mapped(x0, y1)
            se = mapped(x1, y1)
            ne = mapped(x1, y0)
            mesh.append(((x0, y0, x1, y1), (*nw, *sw, *se, *ne)))

    return image.convert("RGBa").transform(
        image.size,
        Image.Transform.MESH,
        mesh,
        resample=Image.Resampling.BICUBIC,
    ).convert("RGBA")


def build_upper_motion(
    source_name: str,
    output_prefix: str,
    *,
    steps: int = 15,
    head_x: float = 0.0,
    head_y: float = 0.0,
    head_tilt: float = 0.0,
    upper_x: float = 0.0,
    upper_y: float = 0.0,
) -> list[Path]:
    image = load_pose(source_name)
    written = [save_pose(f"{output_prefix}.png", image)]
    for index in range(1, steps + 1):
        progress = 0.5 - 0.5 * math.cos(math.pi * index / steps)
        frame = weighted_upper_transform(
            image,
            head_x=head_x * progress,
            head_y=head_y * progress,
            head_tilt=head_tilt * progress,
            upper_x=upper_x * progress,
            upper_y=upper_y * progress,
        )
        written.append(save_pose(f"{output_prefix}_{index}.png", frame))
    return written


def build_upper_cycle(
    source_name: str,
    output_prefix: str,
    sampler,
    *,
    frames: int = 24,
) -> list[Path]:
    image = load_pose(source_name)
    return build_upper_cycle_from_image(image, output_prefix, sampler, frames=frames)


def build_upper_cycle_from_image(
    image: Image.Image,
    output_prefix: str,
    sampler,
    *,
    frames: int = 24,
) -> list[Path]:
    written: list[Path] = []
    for index in range(frames):
        phase = math.tau * index / frames
        params = sampler(phase)
        frame = weighted_upper_transform(image, **params)
        filename = f"{output_prefix}.png" if index == 0 else f"{output_prefix}_{index}.png"
        written.append(save_pose(filename, frame))
    return written


def eased_half_cycle(
    steps: int,
    peak_scale_x: float,
    peak_scale_y: float,
) -> list[tuple[float, float]]:
    variants: list[tuple[float, float]] = []
    for index in range(1, steps + 1):
        progress = index / steps
        eased = 0.5 - 0.5 * math.cos(math.pi * progress)
        variants.append(
            (
                1.0 + (peak_scale_x - 1.0) * eased,
                1.0 + (peak_scale_y - 1.0) * eased,
            )
        )
    return variants


def build_variants(
    source_name: str,
    output_prefix: str,
    variants: list[tuple[float, float]],
    anchor_x_ratio: float = 0.5,
) -> list[Path]:
    image = load_pose(source_name)
    return [
        save_pose(
            f"{output_prefix}_{idx}.png",
            anchored_transform(
                image,
                scale_x=scale_x,
                scale_y=scale_y,
                anchor_x_ratio=anchor_x_ratio,
            ),
        )
        for idx, (scale_x, scale_y) in enumerate(variants, start=1)
    ]


def build_blink() -> list[Path]:
    open_eye = load_pose("idle")
    closed_eye = load_pose("blink")
    return [
        save_pose("blink_seq.png", open_eye),
        save_pose("blink_seq_1.png", blend_pose(open_eye, closed_eye, 0.45)),
        save_pose("blink_seq_2.png", closed_eye),
        save_pose("blink_seq_3.png", blend_pose(open_eye, closed_eye, 0.35)),
    ]


def build_wave() -> list[Path]:
    return build_variants(
        "wave",
        "wave",
        eased_half_cycle(24, peak_scale_x=1.0050, peak_scale_y=0.9910),
        anchor_x_ratio=0.5,
    )


def build_sleep() -> list[Path]:
    return build_variants(
        "sleep",
        "sleep",
        eased_half_cycle(24, peak_scale_x=1.0030, peak_scale_y=0.9920),
    )


def build_talk() -> list[Path]:
    return build_variants(
        "talk",
        "talk",
        eased_half_cycle(24, peak_scale_x=1.0050, peak_scale_y=0.9910),
    )


def build_happy() -> list[Path]:
    return build_variants(
        "happy",
        "happy",
        eased_half_cycle(24, peak_scale_x=1.0060, peak_scale_y=0.9880),
    )


def build_breathe() -> list[Path]:
    return build_upper_motion(
        "idle",
        "breathe",
        steps=24,
        head_y=-2.0,
        upper_y=-3.0,
    )


def build_nod() -> list[Path]:
    return build_upper_cycle(
        "idle",
        "nod",
        lambda phase: {
            "head_y": 9.0 * (0.5 - 0.5 * math.cos(phase)),
            "upper_y": 1.8 * (0.5 - 0.5 * math.cos(phase)),
        },
    )


def build_shake() -> list[Path]:
    return build_upper_cycle(
        "idle",
        "shake",
        lambda phase: {
            "head_x": 9.0 * math.sin(phase),
            "head_tilt": 0.026 * math.sin(phase),
            "upper_x": 1.2 * math.sin(phase),
        },
    )


def build_curious() -> list[Path]:
    return build_upper_motion("look", "curious", head_x=2.5, head_y=-1.0, head_tilt=-0.045, upper_x=0.8)


def build_listen() -> list[Path]:
    image = load_key_pose("listen")
    unit = image.height / 470.0
    return build_upper_cycle_from_image(
        image,
        "listen",
        lambda phase: {
            "head_x": 3.0 * unit * math.sin(phase),
            "head_y": -1.2 * unit * (0.5 - 0.5 * math.cos(phase)),
            "head_tilt": 0.006 * math.sin(phase),
        },
    )


def build_shy() -> list[Path]:
    image = load_key_pose("shy")
    unit = image.height / 470.0
    return build_upper_cycle_from_image(
        image,
        "shy",
        lambda phase: {
            "head_x": 1.5 * unit * math.sin(phase),
            "head_y": 2.5 * unit * (0.5 - 0.5 * math.cos(phase)),
            "head_tilt": 0.005 * math.sin(phase),
            "upper_y": 0.8 * unit * (0.5 - 0.5 * math.cos(phase)),
        },
    )


def build_stretch() -> list[Path]:
    image = load_key_pose("stretch")
    unit = image.height / 470.0
    return build_upper_cycle_from_image(
        image,
        "stretch",
        lambda phase: {
            "head_x": 1.0 * unit * math.sin(phase),
            "head_y": -4.0 * unit * (0.5 - 0.5 * math.cos(phase)),
            "head_tilt": 0.004 * math.sin(phase),
            "upper_y": -2.5 * unit * (0.5 - 0.5 * math.cos(phase)),
        },
    )


def build_walk() -> list[Path]:
    source = Image.open(PROJECT / "bibi" / "optimized_detail_no_black_edge_same_size.png").convert("RGBA")
    written: list[Path] = []
    for index in range(24):
        phase = math.tau * index / 24
        progress = 0.5 - 0.5 * math.cos(phase)
        frame = anchored_transform(
            source,
            scale_x=1.0 + 0.003 * progress,
            scale_y=1.0 - 0.008 * progress,
            offset_x=1.2 * math.sin(phase),
            anchor_x_ratio=0.47,
        )
        filename = "walk.png" if index == 0 else f"walk_{index}.png"
        written.append(save_pose(filename, frame))
    return written


def main() -> int:
    parser = argparse.ArgumentParser(description="生成逐帧桌宠动作素材")
    parser.add_argument("--all", action="store_true", help="同时重建原有呼吸、表情和说话帧")
    args = parser.parse_args()
    POSES.mkdir(parents=True, exist_ok=True)
    written: list[Path] = []
    if args.all:
        written.extend(build_long_base_poses())
        written.extend(build_blink())
        written.extend(build_happy())
        written.extend(build_wave())
        written.extend(build_breathe())
        written.extend(build_sleep())
        written.extend(build_talk())
    written.extend(build_nod())
    written.extend(build_shake())
    written.extend(build_curious())
    written.extend(build_listen())
    written.extend(build_shy())
    written.extend(build_stretch())
    written.extend(build_walk())
    for path in written:
        print(path)
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
