"""Construye un workflow ComfyUI de NORMAL MAP (o DEPTH/HEIGHT) de un sprite, en API format.

Para iluminacion dinamica 2.5D: un sprite plano reacciona a luces del motor si se le
acompana de su mapa de normales (Godot CanvasItem `normal_map`, Unity Sprite secondary
texture "Normal map"). El normal map se ve azul/violeta (XY en R/G, Z en B) — el tono
plano dominante es ~(128,128,255).

Pipeline minimo (sin difusion, ~0 VRAM extra):

    LoadImage ─IMAGE─► <preprocesador normal/depth> ─IMAGE─► SaveImage

El preprocesador lo elige `method`, y todos son nodos del pack controlnet_aux
(ya instalado). class_types verificados EXACTOS contra /object_info del servidor:

  - method="normal"        -> 'BAE-NormalMapPreprocessor'   inputs: image, resolution
        DEFAULT. Produce el normal map CANONICO azul/violeta (fondo plano ~(128,128,255),
        convencion tangent-space que Godot/Unity consumen directamente). Sin parametro de
        intensidad -> `strength` se ignora. Es el resultado usable out-of-the-box.
  - method="normal_midas"  -> 'MiDaS-NormalMapPreprocessor'
        inputs: image(IMAGE), a(FLOAT, default 2*pi), bg_threshold(FLOAT), resolution(INT)
        UNICO normal-preprocessor con control de intensidad nativo (`a`), por eso es el que
        mapea `strength` (a = clamp(strength*2*pi, 0, 5*pi)). OJO: su paleta de color NO es
        el azul/violeta canonico (domina verde/rosa); usalo solo si necesitas variar el
        relieve via strength, no como normal map directo de motor.
  - method="normal_dsine"  -> 'DSINE-NormalMapPreprocessor' inputs: image, fov, iterations, resolution
        Normal alternativo (DSINE); `strength` se ignora (usa fov/iterations por defecto).
  - method="depth"         -> 'DepthAnythingV2Preprocessor' inputs: image, ckpt_name, resolution
        Produce un DEPTH/HEIGHT map (escala de grises), NO un normal. Util para
        parallax/height; `strength` se ignora (DepthAnything no tiene intensidad ajustable).

Nota de diseno: el default es "normal" (BAE, no "depth") porque el objetivo de la funcion
es producir el normal map azul/violeta usable directamente. Hay nodos de normal NATIVOS en
el servidor, asi que NO se necesita el camino depth -> Sobel -> normal como post (esa
funcion auxiliar `comfyui_depth_to_normal` queda como posible futura solo si se quisiera
derivar el normal de un depth de mayor calidad; hoy es innecesaria).

`image` es el nombre del archivo dentro del directorio `input/` de ComfyUI (lo que espera
LoadImage). Para usar un sprite que ya esta en `output/`, hay que copiarlo a `input/`
antes de enviar el workflow (eso es I/O del runner, no de esta funcion pura).

Funcion pura: sin red, sin I/O, sin mutar entradas. Devuelve un dict nuevo.
"""
from __future__ import annotations

_TWO_PI = 6.283185307179586
_MAX_A = 15.707963267948966  # 5*pi, el max que admite MiDaS-NormalMapPreprocessor.a

# method -> class_type EXACTO del nodo preprocesador (verificado contra /object_info).
_METHODS = {
    "normal": "BAE-NormalMapPreprocessor",
    "normal_midas": "MiDaS-NormalMapPreprocessor",
    "normal_dsine": "DSINE-NormalMapPreprocessor",
    "depth": "DepthAnythingV2Preprocessor",
}


def comfyui_build_normal_map_workflow(
    image: str,
    *,
    method: str = "normal",
    strength: float = 1.0,
    resolution: int = 512,
    bg_threshold: float = 0.1,
    filename_prefix: str = "normal_map",
) -> dict:
    """Construye el dict (API format) de un workflow de normal/depth map de un sprite.

    Args:
        image: nombre del archivo de entrada dentro del directorio `input/` de ComfyUI
            (lo que consume LoadImage). No puede estar vacio. Si el sprite vive en
            `output/`, copialo antes a `input/` (I/O del runner).
        method: preprocesador a usar. keyword-only.
            'normal' (default, BAE, normal map canonico azul/violeta usable en motor),
            'normal_midas' (MiDaS, con `strength`, paleta no canonica), 'normal_dsine'
            (DSINE), 'depth' (DepthAnythingV2, height map en gris).
        strength: intensidad del relieve del normal map. Solo tiene efecto con
            method='normal_midas' (MiDaS): se mapea a `a` = clamp(strength*2*pi, 0, 5*pi);
            strength=1.0 -> a=2*pi (relieve estandar). Debe ser >= 0. Para los demas
            metodos se ignora (el nodo no expone intensidad). keyword-only.
        resolution: lado en px al que el preprocesador re-escala antes de inferir
            (64..16384). Conviene matchear el lado del sprite. keyword-only.
        bg_threshold: umbral de fondo de MiDaS (0..1). Solo aplica a method='normal_midas'.
            keyword-only.
        filename_prefix: prefijo del PNG resultante en output/. keyword-only.

    Returns:
        dict en API format con 3 nodos (LoadImage -> preprocesador -> SaveImage), listo
        para comfyui_submit_workflow.

    Raises:
        ValueError: si image esta vacio, method no es valido, strength < 0, o
            resolution esta fuera de 64..16384.
    """
    if not image or not image.strip():
        raise ValueError("comfyui_build_normal_map_workflow: 'image' no puede estar vacio")
    if method not in _METHODS:
        raise ValueError(
            f"comfyui_build_normal_map_workflow: method debe ser uno de "
            f"{tuple(_METHODS)}, no {method!r}"
        )
    if strength < 0:
        raise ValueError(
            f"comfyui_build_normal_map_workflow: strength debe ser >= 0, no {strength!r}"
        )
    if not (64 <= resolution <= 16384):
        raise ValueError(
            f"comfyui_build_normal_map_workflow: resolution debe estar en 64..16384, "
            f"no {resolution!r}"
        )

    class_type = _METHODS[method]

    preproc_inputs = {"image": ["1", 0], "resolution": resolution}
    if method == "normal_midas":
        # MiDaS expone `a` (intensidad) y `bg_threshold`; ahi se refleja `strength`.
        preproc_inputs["a"] = min(max(strength * _TWO_PI, 0.0), _MAX_A)
        preproc_inputs["bg_threshold"] = bg_threshold

    return {
        "1": {"class_type": "LoadImage", "inputs": {"image": image}},
        "2": {"class_type": class_type, "inputs": preproc_inputs},
        "3": {
            "class_type": "SaveImage",
            "inputs": {"images": ["2", 0], "filename_prefix": filename_prefix},
        },
    }


if __name__ == "__main__":
    import json

    wf = comfyui_build_normal_map_workflow(
        "item_icon_potion_00001_.png", method="normal"
    )
    print(json.dumps(wf, indent=2))
