"""セグメンテーションの評価を手元で通すための小さなダミーの ONNX モデルを作る.

本物の ResUNet・検出モデルと**入出力の名前・形状・型だけ**を合わせたもので，合成データ
（`scripts/make_synthetic_segmentation.py`）の赤い楕円を前景として返す．精度の評価には使えない．

- `ResUNet.onnx`: 入力 `input.1`（`(1, 3, 256, 256)` float32．BGR・/255），出力 `output`
  （`(1, 2, 256, 256)` float32 のロジット）．1×1 畳み込みで ch0 = k・(R − B − 0.25)，ch1 = −ch0
  （赤みの強い画素が前景．ch1 は出力チャンネルの取り違えを検出できるかの確認用）
- `Detection_Normal.onnx`: 入力 `image_tensor:0`（`(1, H, W, 3)` uint8．RGB），出力
  `detection_boxes:0`（`(1, 2, 4)`．正規化 `[ymin, xmin, ymax, xmax]`）・`detection_scores:0`
  （`(1, 2)`）．1 つ目は R − B が大きい画素の外接矩形（スコア 0.9），2 つ目は画像全体（スコア 0.3）

ONNX のグラフは `onnx`（開発用の依存）で直接組み立てる．

使い方:
    uv run python scripts/make_dummy_seg_models.py --out data/synthetic_seg/models
"""

from __future__ import annotations

import argparse
import logging
import sys
from pathlib import Path

import numpy as np
import onnx
from onnx import TensorProto, helper, numpy_helper

logger = logging.getLogger(__name__)

OPSET = 17
IR_VERSION = 9
RESUNET_SIZE = 256
RESUNET_GAIN = 30.0  # ロジットの傾き（シグモイド後に 0/1 に近くなるよう大きめ）
RESUNET_OFFSET = 0.25  # R − B がこれを超える画素を前景とする（値域 0〜1）
DETECTION_RED_MINUS_BLUE = 60.0  # 検出で前景とみなす R − B（値域 0〜255）
DETECTION_SCORES = (0.9, 0.3)


def build_resunet() -> onnx.ModelProto:
    """ダミーの ResUNet を作る.

    Returns:
        ONNX モデル（`input.1` `(1, 3, 256, 256)` → `output` `(1, 2, 256, 256)`）．
    """
    # 入力のチャンネルは B・G・R の順（DeepTIAS は OpenCV の BGR のまま渡す）．
    fg = np.array([-RESUNET_GAIN, 0.0, RESUNET_GAIN], dtype=np.float32)
    weight = np.stack([fg, -fg]).reshape(2, 3, 1, 1)
    bias = np.array([-RESUNET_GAIN * RESUNET_OFFSET, RESUNET_GAIN * RESUNET_OFFSET], np.float32)
    graph = helper.make_graph(
        [helper.make_node("Conv", ["input.1", "W", "B"], ["output"])],
        "dummy_resunet",
        [
            helper.make_tensor_value_info(
                "input.1", TensorProto.FLOAT, [1, 3, RESUNET_SIZE, RESUNET_SIZE]
            )
        ],
        [
            helper.make_tensor_value_info(
                "output", TensorProto.FLOAT, [1, 2, RESUNET_SIZE, RESUNET_SIZE]
            )
        ],
        initializer=[numpy_helper.from_array(weight, "W"), numpy_helper.from_array(bias, "B")],
    )
    return _finish(graph)


def build_detector() -> onnx.ModelProto:
    """ダミーの検出モデルを作る.

    Returns:
        ONNX モデル（`image_tensor:0` `(1, H, W, 3)` uint8 → `detection_boxes:0` `(1, 2, 4)`・
        `detection_scores:0` `(1, 2)`）．
    """

    def const(name: str, value: np.ndarray) -> onnx.NodeProto:
        return helper.make_node(
            "Constant", [], [name], value=numpy_helper.from_array(value, name + "_v")
        )

    nodes = [
        helper.make_node("Cast", ["image_tensor:0"], ["xf"], to=TensorProto.FLOAT),
        const("c0", np.array([0], np.int64)),
        const("c1", np.array([1], np.int64)),
        const("c2", np.array([2], np.int64)),
        const("c3", np.array([3], np.int64)),
        const("ax3", np.array([3], np.int64)),
        helper.make_node("Slice", ["xf", "c0", "c1", "ax3"], ["red"]),
        helper.make_node("Slice", ["xf", "c2", "c3", "ax3"], ["blue"]),
        helper.make_node("Sub", ["red", "blue"], ["rb"]),
        const("thr", np.array(DETECTION_RED_MINUS_BLUE, np.float32)),
        helper.make_node("Greater", ["rb", "thr"], ["fg_bool"]),
        helper.make_node("Cast", ["fg_bool"], ["fg"], to=TensorProto.FLOAT),
        # 行・列ごとに前景があるか（(1, H)・(1, W)）．
        helper.make_node("ReduceMax", ["fg"], ["rows"], axes=[2, 3], keepdims=0),
        helper.make_node("ReduceMax", ["fg"], ["cols"], axes=[1, 3], keepdims=0),
        helper.make_node("ArgMax", ["rows"], ["ymin_i"], axis=1, keepdims=0),
        helper.make_node("ArgMax", ["rows"], ["ymax_i"], axis=1, keepdims=0, select_last_index=1),
        helper.make_node("ArgMax", ["cols"], ["xmin_i"], axis=1, keepdims=0),
        helper.make_node("ArgMax", ["cols"], ["xmax_i"], axis=1, keepdims=0, select_last_index=1),
        helper.make_node("Shape", ["image_tensor:0"], ["shape"]),
        helper.make_node("Gather", ["shape", "c1"], ["h_i"], axis=0),
        helper.make_node("Gather", ["shape", "c2"], ["w_i"], axis=0),
        helper.make_node("Cast", ["h_i"], ["h"], to=TensorProto.FLOAT),
        helper.make_node("Cast", ["w_i"], ["w"], to=TensorProto.FLOAT),
        const("one", np.array(1.0, np.float32)),
    ]
    for name in ("ymin", "ymax", "xmin", "xmax"):
        nodes.append(helper.make_node("Cast", [f"{name}_i"], [f"{name}_f"], to=TensorProto.FLOAT))
    nodes += [
        helper.make_node("Add", ["ymax_f", "one"], ["ymax_e"]),
        helper.make_node("Add", ["xmax_f", "one"], ["xmax_e"]),
        helper.make_node("Div", ["ymin_f", "h"], ["ymin_n"]),
        helper.make_node("Div", ["ymax_e", "h"], ["ymax_n"]),
        helper.make_node("Div", ["xmin_f", "w"], ["xmin_n"]),
        helper.make_node("Div", ["xmax_e", "w"], ["xmax_n"]),
        helper.make_node("Concat", ["ymin_n", "xmin_n", "ymax_n", "xmax_n"], ["box1"], axis=0),
        const("box_shape", np.array([1, 1, 4], np.int64)),
        helper.make_node("Reshape", ["box1", "box_shape"], ["box1_r"]),
        const("box2", np.array([[[0.0, 0.0, 1.0, 1.0]]], np.float32)),
        helper.make_node("Concat", ["box1_r", "box2"], ["detection_boxes:0"], axis=1),
        const("detection_scores:0", np.array([DETECTION_SCORES], np.float32)),
    ]
    graph = helper.make_graph(
        nodes,
        "dummy_detection",
        [helper.make_tensor_value_info("image_tensor:0", TensorProto.UINT8, [1, None, None, 3])],
        [
            helper.make_tensor_value_info("detection_boxes:0", TensorProto.FLOAT, [1, 2, 4]),
            helper.make_tensor_value_info("detection_scores:0", TensorProto.FLOAT, [1, 2]),
        ],
    )
    return _finish(graph)


def _finish(graph: onnx.GraphProto) -> onnx.ModelProto:
    model = helper.make_model(graph, opset_imports=[helper.make_opsetid("", OPSET)])
    model.ir_version = IR_VERSION
    onnx.checker.check_model(model)
    return model


def _parse_args(argv: list[str] | None) -> argparse.Namespace:
    parser = argparse.ArgumentParser(description="ダミーの ResUNet・検出モデル（ONNX）を作る")
    parser.add_argument(
        "--out",
        type=Path,
        default=Path("data/synthetic_seg/models"),
        help="出力先（既定: data/synthetic_seg/models）",
    )
    return parser.parse_args(argv)


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

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

    Returns:
        終了コード（成功 0）．
    """
    logging.basicConfig(
        level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s"
    )
    args = _parse_args(argv)
    args.out.mkdir(parents=True, exist_ok=True)
    onnx.save(build_resunet(), args.out / "ResUNet.onnx")
    onnx.save(build_detector(), args.out / "Detection_Normal.onnx")
    logger.info("ダミーのモデルを書き出した: %s", args.out)
    return 0


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