"""セグメンテーションの評価（`tias-seg eval`）を手元で通すための合成データを生成する.

実データ（TongueSegmentation の `input/`）と同じ構成を作る:

    <out>/input/train/{sample,label}/*.jpg
    <out>/input/test/cropped/{sample,label}/*.jpg
    <out>/input/test_extra/cropped/{sample,label}/*.jpg
    <out>/input/test/fullsize/{sample,label}/*.jpg

撮影画像（fullsize）は青みがかった灰色の背景に舌の代わりの赤い楕円を描いたもの，正解マスクは
楕円の内側を 255 にしたグレースケール JPEG（実データと同じく JPEG のため境界に中間値を含む）．
切り出し済み（cropped）は楕円の外接矩形に余白を付けて切り出したもの．一部の画像には楕円の外に
小さな赤い点を置き，小領域除去が働く場面を作る．ダミーの ONNX モデルは
`scripts/make_dummy_seg_models.py` で作る．

使い方:
    uv run python scripts/make_synthetic_segmentation.py --out data/synthetic_seg --per-set 8
"""

from __future__ import annotations

import argparse
import logging
import shutil
import sys
from pathlib import Path

import numpy as np
from PIL import Image, ImageDraw

logger = logging.getLogger(__name__)

# 出力先がこのスクリプトの生成物であることを示す目印（--overwrite で消すのは目印のある所だけ）．
MARKER_FILE = ".synthetic_segmentation"

# (評価セットのディレクトリ, 撮影画像か)．
SETS: tuple[tuple[str, bool], ...] = (
    ("train", False),
    ("test/cropped", False),
    ("test_extra/cropped", False),
    ("test/fullsize", True),
)

BACKGROUND_RGB = (110, 120, 150)  # 青みがかった灰色（ダミーの ResUNet が背景と判定する色）
TONGUE_RGB = (200, 95, 105)  # 赤みのある舌の色
COLOR_JITTER = 10
PIXEL_NOISE_STD = 5.0
MIN_SIZE = 200
MAX_SIZE = 360
SPOT_PROBABILITY = 0.5  # 楕円の外に小さな赤い点を置く確率
JPEG_QUALITY = 95


def make_fullsize(rng: np.random.Generator) -> tuple[np.ndarray, np.ndarray]:
    """撮影画像を模した画像と正解マスクを 1 組作る.

    Args:
        rng: 乱数生成器．

    Returns:
        （RGB 画像 `(H, W, 3)` uint8, 正解マスク `(H, W)` bool）．
    """
    width = int(rng.integers(MIN_SIZE, MAX_SIZE + 1))
    height = int(rng.integers(MIN_SIZE, MAX_SIZE + 1))
    ew = int(rng.integers(width // 4, width // 2))
    eh = int(rng.integers(height // 4, height // 2))
    cx = int(rng.integers(ew // 2 + 5, width - ew // 2 - 5))
    cy = int(rng.integers(eh // 2 + 5, height - eh // 2 - 5))
    mask_img = Image.new("L", (width, height), 0)
    ImageDraw.Draw(mask_img).ellipse(
        (cx - ew // 2, cy - eh // 2, cx + ew // 2, cy + eh // 2), fill=255
    )
    mask = np.asarray(mask_img) > 0  # (H, W)

    # 背景は横方向に少し明るさが変わる（一様な色だけにしない）．
    gradient = np.linspace(-15.0, 15.0, width)[None, :, None]
    background = np.array(BACKGROUND_RGB, dtype=np.float64) + gradient
    tongue = np.array(TONGUE_RGB, dtype=np.float64) + rng.integers(
        -COLOR_JITTER, COLOR_JITTER + 1, size=3
    )
    pixels = np.where(mask[..., None], tongue, background)
    pixels = pixels + rng.normal(0.0, PIXEL_NOISE_STD, size=(height, width, 3))

    if rng.random() < SPOT_PROBABILITY:
        # 舌と同じ色の小さな点（小領域除去で消えるべき誤検出の種）．正解マスクには含めない．
        spot = Image.new("L", (width, height), 0)
        sx = int(rng.integers(3, width - 8))
        sy = int(rng.integers(3, height - 8))
        ImageDraw.Draw(spot).ellipse((sx, sy, sx + 4, sy + 4), fill=255)
        spot_mask = (np.asarray(spot) > 0) & ~mask
        pixels[spot_mask] = tongue
    return np.clip(pixels, 0, 255).astype(np.uint8), mask


def crop_around(
    rgb: np.ndarray, mask: np.ndarray, rng: np.random.Generator
) -> tuple[np.ndarray, np.ndarray]:
    """正解マスクの外接矩形に余白を付けて切り出す（切り出し済みの画像を模す）.

    Args:
        rgb: RGB 画像（`(H, W, 3)`, uint8）．
        mask: 正解マスク（`(H, W)`, bool）．
        rng: 乱数生成器．

    Returns:
        （切り出した RGB 画像, 切り出した正解マスク）．
    """
    ys, xs = np.nonzero(mask)
    height, width = mask.shape
    mx = int(rng.integers(2, max(3, (xs.max() - xs.min()) // 8)))
    my = int(rng.integers(2, max(3, (ys.max() - ys.min()) // 8)))
    x0, x1 = max(0, xs.min() - mx), min(width, xs.max() + 1 + mx)
    y0, y1 = max(0, ys.min() - my), min(height, ys.max() + 1 + my)
    return rgb[y0:y1, x0:x1], mask[y0:y1, x0:x1]


def generate(out_dir: Path, per_set: int, seed: int) -> int:
    """合成データを生成する.

    Args:
        out_dir: 出力先（空または存在しないディレクトリ）．
        per_set: 評価セットあたりの枚数．
        seed: 乱数 seed．

    Returns:
        生成した組の数．
    """
    rng = np.random.default_rng(seed)
    out_dir.mkdir(parents=True, exist_ok=True)
    (out_dir / MARKER_FILE).write_text(f"seed={seed}\nper_set={per_set}\n", encoding="utf-8")
    serial = 0
    for set_dir, fullsize in SETS:
        sample_dir = out_dir / "input" / set_dir / "sample"
        label_dir = out_dir / "input" / set_dir / "label"
        sample_dir.mkdir(parents=True, exist_ok=True)
        label_dir.mkdir(parents=True, exist_ok=True)
        for _ in range(per_set):
            rgb, mask = make_fullsize(rng)
            if not fullsize:
                rgb, mask = crop_around(rgb, mask, rng)
            name = f"seg_{serial:06d}.jpg"
            Image.fromarray(rgb).save(sample_dir / name, quality=JPEG_QUALITY)
            Image.fromarray((mask * 255).astype(np.uint8)).save(
                label_dir / name, quality=JPEG_QUALITY
            )
            serial += 1
        logger.info("%s: %d 組", set_dir, per_set)
    return serial


def _prepare_out_dir(out_dir: Path, overwrite: bool) -> None:
    if not out_dir.exists():
        return
    if not out_dir.is_dir():
        raise ValueError(f"出力先がディレクトリではない: {out_dir}")
    if not any(out_dir.iterdir()):
        return
    if not overwrite:
        raise ValueError(f"出力先が空でない: {out_dir}（作り直すには --overwrite を付ける）")
    if not (out_dir / MARKER_FILE).is_file():
        raise ValueError(
            f"出力先に合成データの目印 {MARKER_FILE} が無いため削除しない: {out_dir}"
            "（合成データ以外を誤って消さないため）"
        )
    logger.info("既存の合成データを削除する: %s", out_dir)
    shutil.rmtree(out_dir)


def _parse_args(argv: list[str] | None) -> argparse.Namespace:
    parser = argparse.ArgumentParser(description="セグメンテーションの評価用の合成データを生成する")
    parser.add_argument(
        "--out",
        type=Path,
        default=Path("data/synthetic_seg"),
        help="出力先（既定: data/synthetic_seg）",
    )
    parser.add_argument("--per-set", type=int, default=8, help="評価セットあたりの枚数（既定: 8）")
    parser.add_argument("--seed", type=int, default=0, help="乱数 seed（既定: 0）")
    parser.add_argument(
        "--overwrite",
        action="store_true",
        help="既存の合成データを削除して作り直す（目印ファイルのある出力先のみ．models/ も消える）",
    )
    args = parser.parse_args(argv)
    if args.per_set < 1:
        parser.error("--per-set は 1 以上を指定する")
    return args


def main(argv: list[str] | None = None) -> int:
    """エントリポイント.

    Args:
        argv: コマンドライン引数（None なら `sys.argv[1:]`）．

    Returns:
        終了コード（成功 0・入力の問題 1）．
    """
    logging.basicConfig(
        level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s"
    )
    args = _parse_args(argv)
    try:
        _prepare_out_dir(args.out, args.overwrite)
    except ValueError as e:
        logger.error("%s", e)
        return 1
    count = generate(args.out, args.per_set, args.seed)
    logger.info("合成データを生成した: %d 組 -> %s", count, args.out / "input")
    return 0


if __name__ == "__main__":
    sys.exit(main())
