#!/usr/bin/env python3
"""Boilerplate generators for Fusion Fuse plugins.

Each generator returns a complete, syntactically-valid .fuse source string ready
to be written to the Fuses install directory. Generated sources include a
machine-readable header comment so list() can identify MCP-installed Fuses.

Reference: Fusion Fuse SDK (June 2023), Blackmagic Design.
"""

from typing import Any, Dict, Optional

# Maps the public `type` argument to Fusion's internal ClassType enum.
CLASS_TYPES = {
    "tool": "CT_Tool",
    "modifier": "CT_Modifier",
    "viewlut": "CT_ViewLUTPlugin",
}

# Linear color ops the color_matrix template knows how to compose.
COLOR_MATRIX_OPS = ("brightness", "contrast", "gain", "saturation", "invert")


def header(name: str, kind: str, fuse_type: str) -> str:
    """MCP marker comment placed at the top of generated Fuses."""
    return (
        f"-- @mcp-fuse name={name} kind={kind} type={fuse_type}\n"
        "-- Generated by davinci-resolve-mcp. Edit freely; the marker above\n"
        "-- only affects how the MCP `fuse_plugin list` action reports this file.\n\n"
    )


def color_matrix(name: str, options: Optional[Dict[str, Any]] = None) -> str:
    """Linear color tool composed from a sequence of matrix operations.

    options:
        ops: list of operation names from COLOR_MATRIX_OPS.
             Defaults to ['brightness', 'contrast', 'saturation'].

    The generated Fuse exposes one slider per op and applies them as a single
    ColorMatrixFull, which is faster than chaining separate image operations.
    See Fuse SDK pp. 24 and 102–105.
    """
    options = options or {}
    ops = options.get("ops") or ["brightness", "contrast", "saturation"]
    invalid = [o for o in ops if o not in COLOR_MATRIX_OPS]
    if invalid:
        raise ValueError(f"Unknown color_matrix ops: {invalid}. "
                         f"Valid: {list(COLOR_MATRIX_OPS)}")

    inputs = []
    matrix_steps = []

    for op in ops:
        if op == "brightness":
            inputs.append(_slider("InBright", "Brightness", default=0.0,
                                  min_=-1.0, max_=1.0))
            matrix_steps.append(
                "    local b = InBright:GetValue(req).Value\n"
                "    m:Offset(b, b, b, 0)"
            )
        elif op == "contrast":
            inputs.append(_slider("InContrast", "Contrast", default=1.0,
                                  min_=0.0, max_=2.0))
            matrix_steps.append(
                "    local c = InContrast:GetValue(req).Value\n"
                "    m:Offset(-0.5, -0.5, -0.5, 0)\n"
                "    m:Scale(c, c, c, 1)\n"
                "    m:Offset(0.5, 0.5, 0.5, 0)"
            )
        elif op == "gain":
            inputs.append(_slider("InGain", "Gain", default=1.0,
                                  min_=0.0, max_=4.0))
            matrix_steps.append(
                "    local g = InGain:GetValue(req).Value\n"
                "    m:Scale(g, g, g, 1)"
            )
        elif op == "saturation":
            inputs.append(_slider("InSat", "Saturation", default=1.0,
                                  min_=0.0, max_=4.0))
            matrix_steps.append(
                "    local s = InSat:GetValue(req).Value\n"
                "    m:RGBtoYUV()\n"
                "    m:Scale(1, s, s, 1)\n"
                "    m:YUVtoRGB()"
            )
        elif op == "invert":
            inputs.append(_checkbox("InInvert", "Invert"))
            matrix_steps.append(
                "    if InInvert:GetValue(req).Value > 0.5 then\n"
                "        m:Scale(-1, -1, -1, 1)\n"
                "        m:Offset(1, 1, 1, 0)\n"
                "    end"
            )

    inputs_block = "\n".join(inputs)
    process_block = "\n".join(matrix_steps)
    op_list = ", ".join(ops)

    return header(name, "color_matrix", "tool") + f'''FuRegisterClass("{name}", CT_Tool, {{
    REGS_Category = "Fuses\\\\MCP",
    REGS_OpIconString = "{name[:4]}",
    REGS_OpDescription = "Color matrix tool ({op_list})",
    REG_NoBlendCtrls = false,
    REG_NoMotionBlurCtrls = true,
}})

function Create()
{inputs_block}

    InImage = self:AddInput("Input", "Input", {{
        LINKID_DataType = "Image",
        LINK_Main = 1,
    }})

    OutImage = self:AddOutput("Output", "Output", {{
        LINKID_DataType = "Image",
        LINK_Main = 1,
    }})
end

function Process(req)
    local img = InImage:GetValue(req)
    local m = ColorMatrixFull()

{process_block}

    local out = img:ApplyMatrixOf(m, {{}})
    OutImage:Set(req, out)
end
'''


def per_pixel(name: str, options: Optional[Dict[str, Any]] = None) -> str:
    """Per-pixel processing tool using MultiProcessPixels (multithreaded).

    options:
        inputs:     1 or 2 image inputs (default 1)
        expression: Lua body executed per pixel. Receives (x, y, p1) or
                    (x, y, p1, p2). Must mutate and return p1.
                    Default: copies p1 unchanged.
        amount:     bool, whether to expose an Amount slider as `amt` in the
                    expression (default True)

    See Fuse SDK pp. 29-30, 88-89.
    """
    options = options or {}
    n_inputs = int(options.get("inputs", 1))
    if n_inputs not in (1, 2):
        raise ValueError("per_pixel: inputs must be 1 or 2")
    expression = options.get("expression", "    return p1")
    expose_amount = bool(options.get("amount", True))

    pixel_args = "x, y, p1, p2" if n_inputs == 2 else "x, y, p1"

    extra_input = ""
    fetch_extra = ""
    pass_extra = "img1, img1"  # MultiProcessPixels needs 2 images even with 1 input
    if n_inputs == 2:
        extra_input = '''
    InImage2 = self:AddInput("Input 2", "Input2", {
        LINKID_DataType = "Image",
        LINK_Main = 2,
        INP_Required = false,
    })
'''
        fetch_extra = "    local img2 = InImage2:GetValue(req)\n    if img2 == nil then img2 = img1 end"
        pass_extra = "img1, img2"

    amount_input = ""
    amount_pass = ""
    if expose_amount:
        amount_input = _slider("InAmount", "Amount", default=1.0, min_=0.0, max_=2.0)
        amount_pass = "{ amt = InAmount:GetValue(req).Value }"
    else:
        amount_pass = "{}"

    return header(name, "per_pixel", "tool") + f'''FuRegisterClass("{name}", CT_Tool, {{
    REGS_Category = "Fuses\\\\MCP",
    REGS_OpIconString = "{name[:4]}",
    REGS_OpDescription = "Per-pixel custom op",
}})

-- Pixel function: must be defined before Process() (no header files in Lua).
-- p1 and p2 are Pixel structs with members R, G, B, A and aux channels.
local function pixel_op({pixel_args})
{expression}
end

function Create()
{amount_input}{extra_input}
    InImage = self:AddInput("Input", "Input", {{
        LINKID_DataType = "Image",
        LINK_Main = 1,
    }})

    OutImage = self:AddOutput("Output", "Output", {{
        LINKID_DataType = "Image",
        LINK_Main = 1,
    }})
end

function Process(req)
    local img1 = InImage:GetValue(req)
{fetch_extra}

    local out = Image({{ IMG_Like = img1 }})
    out:MultiProcessPixels(nil, {amount_pass},
                           0, 0, img1.Width, img1.Height,
                           {pass_extra}, pixel_op)
    OutImage:Set(req, out)
end
'''


def view_lut(name: str, options: Optional[Dict[str, Any]] = None) -> str:
    """View LUT plugin: GLSL shader applied during display only.

    options:
        shader: GLSL body for `void ShadePixel(inout FuPixel f)`.
                Default: a simple gamma adjustment driven by InGamma.
        params: list of {name, type='float', default=1.0, min=0.0, max=5.0}
                Each becomes a slider in the Inspector AND a uniform in the
                shader. Default: one InGamma slider.

    FuPixel members available in the shader:
        Color      vec4 — source pixel color
        TexCoord0  vec4 — pixel coordinates [0..w-1, 0..h-1]
        TexCoord1  vec4 — normalized coordinates [0..1, 0..1]
        TexCoord2  vec4 — destination screen coordinates

    See Fuse SDK pp. 118–124.
    """
    options = options or {}
    params = options.get("params") or [
        {"name": "Gamma", "type": "float", "default": 1.0, "min": 0.0, "max": 5.0}
    ]
    shader_body = options.get("shader") or (
        "    EvalShadePixel(f);\n"
        "    f.Color.rgb = sign(f.Color.rgb) * pow(abs(f.Color.rgb), vec3(Gamma));"
    )

    valid_types = ("float", "vec2", "vec3_rgb", "vec4_rgba")
    inputs = []
    setparams = []
    shader_params = []
    for i, prm in enumerate(params, start=1):
        pname = prm["name"]
        ptype = prm.get("type", "float")
        if ptype not in valid_types:
            raise ValueError(f"Invalid view_lut param type '{ptype}'. "
                             f"Valid: {list(valid_types)}")
        if ptype == "float":
            default = prm.get("default", 1.0)
            pmin = prm.get("min", 0.0)
            pmax = prm.get("max", 5.0)
            inputs.append(_slider(f"In{pname}", pname, default=default,
                                  min_=pmin, max_=pmax))
            setparams.append(
                f"    vsnode:Set({i}, In{pname}:GetValue(req).Value)"
            )
            shader_params.append(f"float {pname}")
        elif ptype == "vec2":
            inputs.append(
                f'    In{pname} = self:AddInput("{pname}", "{pname}", {{\n'
                f'        LINKID_DataType = "Point",\n'
                f'        INPID_InputControl = "OffsetControl",\n'
                f'        INPID_PreviewControl = "CrosshairControl",\n'
                f'        INP_DefaultX = 0.5, INP_DefaultY = 0.5,\n'
                f'    }})\n'
            )
            setparams.append(
                f"    local p_{pname} = In{pname}:GetValue(req)\n"
                f"    vsnode:Set({i}, p_{pname}.X, p_{pname}.Y)"
            )
            shader_params.append(f"vec2 {pname}")
        elif ptype in ("vec3_rgb", "vec4_rgba"):
            count = 3 if ptype == "vec3_rgb" else 4
            channel_names = ["Red", "Green", "Blue", "Alpha"][:count]
            for j, chan in enumerate(channel_names):
                inputs.append(
                    f'    In{pname}{chan} = self:AddInput('
                    f'"{pname} {chan}", "{pname}{chan}", {{\n'
                    f'        LINKID_DataType = "Number",\n'
                    f'        INPID_InputControl = "ColorControl",\n'
                    f'        IC_ControlGroup = {100 + i},\n'
                    f'        IC_ControlID = {j},\n'
                    f'        INP_Default = 1.0,\n'
                    f'    }})\n'
                )
            getters = ", ".join(
                f"In{pname}{chan}:GetValue(req).Value"
                for chan in channel_names
            )
            setparams.append(f"    vsnode:Set({i}, {getters})")
            glsl_type = "vec3" if ptype == "vec3_rgb" else "vec4"
            shader_params.append(f"{glsl_type} {pname}")

    inputs_block = "\n".join(inputs)
    setparams_block = "\n".join(setparams)
    params_string = "\\n".join(shader_params)

    return header(name, "view_lut", "viewlut") + f'''FuRegisterClass("{name}", CT_ViewLUTPlugin, {{
    REGS_Name = "{name}",
    REGS_Category = "ViewShaders",
}})

function Create()
{inputs_block}
end

local params = [[
{chr(10).join(shader_params)}
]]

local shader = [[
void ShadePixel(inout FuPixel f)
{{
{shader_body}
}}
]]

function SetupShadeNode(group, req, img)
    return ViewShadeNode(group, "{name}", params, shader)
end

function SetupParams(req, vsnode, img)
{setparams_block}
    return true
end
'''


def transform(name: str, options: Optional[Dict[str, Any]] = None) -> str:
    """Geometric warp tool with on-screen Center crosshair, Angle, and Size.

    options:
        edge_mode: 'Black' | 'Canvas' | 'Wrap' | 'Duplicate' (default 'Black')

    The generated Fuse uses Image:Transform under the hood, with the OffsetControl
    Y-coordinate squashed conversion baked in so the on-screen crosshair lands
    where the user clicks regardless of image aspect ratio. See Fuse SDK p. 115.
    """
    options = options or {}
    edge_mode = options.get("edge_mode", "Black")
    if edge_mode not in ("Black", "Canvas", "Wrap", "Duplicate"):
        raise ValueError(f"Invalid edge_mode '{edge_mode}'. "
                         "Valid: Black, Canvas, Wrap, Duplicate")

    return header(name, "transform", "tool") + f'''FuRegisterClass("{name}", CT_Tool, {{
    REGS_Category = "Fuses\\\\MCP",
    REGS_OpIconString = "{name[:4]}",
    REGS_OpDescription = "Transform tool (warp/rotate/scale)",
}})

function Create()
    InCenter = self:AddInput("Center", "Center", {{
        LINKID_DataType = "Point",
        INPID_InputControl = "OffsetControl",
        INPID_PreviewControl = "CrosshairControl",
        INP_DefaultX = 0.5,
        INP_DefaultY = 0.5,
    }})

    InAngle = self:AddInput("Angle", "Angle", {{
        LINKID_DataType = "Number",
        INPID_InputControl = "ScrewControl",
        INP_Default = 0.0,
        INP_MinScale = -180.0,
        INP_MaxScale = 180.0,
    }})

    InSize = self:AddInput("Size", "Size", {{
        LINKID_DataType = "Number",
        INPID_InputControl = "SliderControl",
        INP_Default = 1.0,
        INP_MinScale = 0.0,
        INP_MaxScale = 4.0,
    }})

    InImage = self:AddInput("Input", "Input", {{
        LINKID_DataType = "Image",
        LINK_Main = 1,
    }})

    OutImage = self:AddOutput("Output", "Output", {{
        LINKID_DataType = "Image",
        LINK_Main = 1,
    }})
end

function Process(req)
    local img = InImage:GetValue(req)
    local center = InCenter:GetValue(req)
    local angle = InAngle:GetValue(req).Value
    local size = InSize:GetValue(req).Value

    local out = img:Transform(nil, {{
        XF_XOffset = center.X,
        XF_YOffset = center.Y,
        XF_XAxis = 0.5,
        XF_YAxis = 0.5,
        XF_XSize = size,
        XF_YSize = size,
        XF_Angle = angle,
        XF_EdgeMode = "{edge_mode}",
    }})

    OutImage:Set(req, out)
end
'''


def text_overlay(name: str, options: Optional[Dict[str, Any]] = None) -> str:
    """Renders styled text into the input image using Fusion's font shape system.

    options:
        default_text: str (default '')
        justify: 'left' | 'center' | 'right' (default 'center')

    The generated Fuse exposes Styled Text, Font, Style, Size, Position, Color,
    and a justify dropdown. Internally uses Fontmetrics + ImageChannel + Shape +
    PutToImage with CM_Merge so text overlays the input image.
    See Fuse SDK pp. 33-36 (Example6_Text reference implementation).
    """
    options = options or {}
    default_text = options.get("default_text", "")
    justify_label = options.get("justify", "center")
    justify_index = {"left": 0, "center": 1, "right": 2}.get(justify_label)
    if justify_index is None:
        raise ValueError(f"Invalid justify '{justify_label}'. "
                         "Valid: left, center, right")

    return header(name, "text_overlay", "tool") + f'''FuRegisterClass("{name}", CT_Tool, {{
    REGS_Category = "Fuses\\\\MCP",
    REGS_OpIconString = "{name[:4]}",
    REGS_OpDescription = "Text overlay",
}})

function Create()
    InText = self:AddInput("Styled Text", "StyledText", {{
        LINKID_DataType = "Text",
        INPID_InputControl = "TextEditControl",
        TEC_Lines = 3,
        INPS_DefaultText = "{default_text}",
    }})

    self:BeginControlNest("Font", "FontNest", true, {{}})
    InFont = self:AddInput("Font", "Font", {{
        LINKID_DataType = "Text",
        INPID_InputControl = "FontFileControl",
        IC_ControlGroup = 1,
        IC_ControlID = 0,
        INP_DoNotifyChanged = true,
    }})

    InStyle = self:AddInput("Style", "Style", {{
        LINKID_DataType = "Text",
        INPID_InputControl = "FontFileControl",
        IC_ControlGroup = 1,
        IC_ControlID = 1,
        INP_DoNotifyChanged = true,
    }})

    InSize = self:AddInput("Size", "Size", {{
        LINKID_DataType = "Number",
        INPID_InputControl = "SliderControl",
        INP_Default = 0.08,
        INP_MinScale = 0.0,
        INP_MaxScale = 1.0,
    }})
    self:EndControlNest()

    self:BeginControlNest("Layout", "LayoutNest", true, {{}})
    InPosition = self:AddInput("Position", "Position", {{
        LINKID_DataType = "Point",
        INPID_InputControl = "OffsetControl",
        INPID_PreviewControl = "CrosshairControl",
        INP_DefaultX = 0.5,
        INP_DefaultY = 0.5,
    }})

    InJustify = self:AddInput("Justify", "Justify", {{
        LINKID_DataType = "Number",
        INPID_InputControl = "ComboControl",
        INP_Default = {justify_index},
        INP_Integer = true,
        {{ CCS_AddString = "Left", }},
        {{ CCS_AddString = "Center", }},
        {{ CCS_AddString = "Right", }},
    }})
    self:EndControlNest()

    self:BeginControlNest("Color", "ColorNest", false, {{}})
    InR = self:AddInput("Red", "Red", {{
        LINKID_DataType = "Number", INPID_InputControl = "ColorControl",
        IC_ControlGroup = 2, IC_ControlID = 0, INP_Default = 1.0,
    }})
    InG = self:AddInput("Green", "Green", {{
        LINKID_DataType = "Number", INPID_InputControl = "ColorControl",
        IC_ControlGroup = 2, IC_ControlID = 1, INP_Default = 1.0,
    }})
    InB = self:AddInput("Blue", "Blue", {{
        LINKID_DataType = "Number", INPID_InputControl = "ColorControl",
        IC_ControlGroup = 2, IC_ControlID = 2, INP_Default = 1.0,
    }})
    InA = self:AddInput("Alpha", "Alpha", {{
        LINKID_DataType = "Number", INPID_InputControl = "ColorControl",
        IC_ControlGroup = 2, IC_ControlID = 3, INP_Default = 1.0,
    }})
    self:EndControlNest()

    InImage = self:AddInput("Input", "Input", {{
        LINKID_DataType = "Image",
        LINK_Main = 1,
    }})

    OutImage = self:AddOutput("Output", "Output", {{
        LINKID_DataType = "Image",
        LINK_Main = 1,
    }})
end

-- drawstring renders one or more lines of text into ImageChannel `ic`.
-- Adapted from the SDK's Example6_Text Fuse.
local function drawstring(img, font_name, style, size, justify, quality,
                          x, y, colour, text)
    local ic = ImageChannel(img, quality)
    local fs = FillStyle()
    local cs = ChannelStyle()
    cs.Color = colour
    ic:SetStyleFill(fs)

    local font = TextStyleFont(font_name, style)
    local tfm = TextStyleFontMetrics(font)
    local line_height = (tfm.TextAscent + tfm.TextDescent +
                         tfm.TextExternalLeading) * 10 * size

    local mat = Matrix4()
    mat:Scale(1.0 / tfm.Scale, 1.0 / tfm.Scale, 1.0)
    mat:Scale(size, size, 1)
    mat:Move(x, y, 0)

    local shape = Shape()

    for line in string.gmatch(text, "%C+") do
        local line_width = 0
        for i = 1, #line do
            local ch = line:sub(i, i):byte()
            line_width = line_width + tfm:CharacterWidth(ch) * 10 * size
        end

        if justify == 1 then
            mat:Move(-line_width / 2, 0, 0)
        elseif justify == 2 then
            mat:Move(-line_width, 0, 0)
        end

        local prevch
        local x_move = 0
        for i = 1, #line do
            local ch = line:sub(i, i):byte()
            local cw = tfm:CharacterWidth(ch) * 10 * size

            if prevch then
                local x_offset = tfm:CharacterKerning(prevch, ch) * 10 * size
                x_move = x_move + x_offset
                mat:Move(x_offset, 0, 0)
            end

            mat:Move(cw / 2, 0, 0)
            local sh = tfm:GetCharacterShape(ch, false)
            sh = sh:TransformOfShape(mat)
            mat:Move(cw / 2, 0, 0)
            x_move = x_move + cw

            shape:AddShape(sh)
            prevch = ch
        end

        if justify == 0 then
            mat:Move(-x_move, -line_height, 0)
        elseif justify == 1 then
            mat:Move(-x_move / 2, -line_height, 0)
        else
            mat:Move(0, -line_height, 0)
        end
    end

    ic:ShapeFill(shape)
    ic:PutToImage("CM_Merge", cs)
end

function Process(req)
    local img = InImage:GetValue(req)
    local out = img:CopyOf()

    local text = InText:GetValue(req).Value or ""
    if text == "" then
        OutImage:Set(req, out)
        return
    end

    local font = InFont:GetValue(req).Value or "Open Sans"
    local style = InStyle:GetValue(req).Value or "Regular"
    local size = InSize:GetValue(req).Value
    local pos = InPosition:GetValue(req)
    local justify = InJustify:GetValue(req).Value
    local r = InR:GetValue(req).Value
    local g = InG:GetValue(req).Value
    local b = InB:GetValue(req).Value
    local a = InA:GetValue(req).Value

    -- Squash-correction so the on-screen point lands where the user clicks.
    local cx = pos.X
    local cy = pos.Y * (out.Height * out.YScale) / (out.Width * out.XScale)

    if FontManager and not next(FontManager:GetFontList()) then
        FontManager:ScanDir()
    end

    local quality = 32
    if req:IsQuick() then quality = 1 end

    drawstring(out, font, style, size, justify, quality, cx, cy,
               Pixel({{R = r, G = g, B = b, A = a}}), text)

    OutImage:Set(req, out)
end
'''


def modifier(name: str, options: Optional[Dict[str, Any]] = None) -> str:
    """Modifier plugin: drives a Number input on another tool with a function of time.

    options:
        kind: 'sine' | 'noise' | 'ramp' (default 'sine')

    Modifiers have no image inputs; they expose a Number output. To use the
    generated modifier, right-click any slider in the Inspector → Modify with →
    [name]. See Fuse SDK p. 9 (Modifier description) and p. 40 (CT_Modifier).
    """
    options = options or {}
    kind = options.get("kind", "sine")
    if kind not in ("sine", "noise", "ramp"):
        raise ValueError(f"Invalid modifier kind '{kind}'. "
                         "Valid: sine, noise, ramp")

    if kind == "sine":
        formula = "math.sin(2 * math.pi * (t * freq + phase)) * amp + offset"
    elif kind == "ramp":
        formula = "((t * freq + phase) % 1.0) * amp + offset"
    else:  # noise
        formula = ("amp * (math.random() - 0.5) * 2 + offset")

    return header(name, "modifier", "modifier") + f'''FuRegisterClass("{name}", CT_Modifier, {{
    REGS_Category = "Fuses\\\\MCP",
    REGS_OpIconString = "{name[:4]}",
    REGS_OpDescription = "Number modifier ({kind})",
}})

function Create()
    InFreq = self:AddInput("Frequency", "Frequency", {{
        LINKID_DataType = "Number",
        INPID_InputControl = "SliderControl",
        INP_Default = 1.0,
        INP_MinScale = 0.0,
        INP_MaxScale = 10.0,
    }})

    InAmp = self:AddInput("Amplitude", "Amplitude", {{
        LINKID_DataType = "Number",
        INPID_InputControl = "SliderControl",
        INP_Default = 1.0,
        INP_MinScale = 0.0,
        INP_MaxScale = 10.0,
    }})

    InPhase = self:AddInput("Phase", "Phase", {{
        LINKID_DataType = "Number",
        INPID_InputControl = "SliderControl",
        INP_Default = 0.0,
        INP_MinScale = -1.0,
        INP_MaxScale = 1.0,
    }})

    InOffset = self:AddInput("Offset", "Offset", {{
        LINKID_DataType = "Number",
        INPID_InputControl = "SliderControl",
        INP_Default = 0.0,
        INP_MinScale = -10.0,
        INP_MaxScale = 10.0,
    }})

    OutValue = self:AddOutput("Value", "Value", {{
        LINKID_DataType = "Number",
        LINK_Main = 1,
    }})
end

function Process(req)
    local t = req.Time
    local freq = InFreq:GetValue(req).Value
    local amp = InAmp:GetValue(req).Value
    local phase = InPhase:GetValue(req).Value
    local offset = InOffset:GetValue(req).Value

    local v = {formula}
    OutValue:Set(req, Number(v))
end
'''


def dctl_kernel(name: str, options: Optional[Dict[str, Any]] = None) -> str:
    """Fuse that wraps a DCTL kernel via DVIPComputeNode (GPU compute).

    options:
        kernel: GPU kernel body. Default is a per-pixel gain shader.
        params: list of {name, type='float', default=1.0, min=0.0, max=4.0}
                Each becomes a Lua slider AND a kernel parameter.

    The generated Fuse runs on Metal (macOS), CUDA (NVIDIA), or OpenCL (AMD)
    via Resolve's built-in DCTL compiler. See Fuse SDK pp. 130-143 (Circle/
    Gradient example).
    """
    options = options or {}
    params = options.get("params") or [
        {"name": "Gain", "type": "float", "default": 1.0, "min": 0.0, "max": 4.0}
    ]
    kernel = options.get("kernel") or (
        "        float4 c = _tex2DVecN(src, x, y, 15);\n"
        "        c.x *= params->Gain;\n"
        "        c.y *= params->Gain;\n"
        "        c.z *= params->Gain;\n"
        "        _tex2DVec4Write(dst, x, y, c);"
    )

    inputs = []
    set_lines = []
    param_struct = []
    for prm in params:
        pname = prm["name"]
        ptype = prm.get("type", "float")
        default = prm.get("default", 1.0)
        pmin = prm.get("min", 0.0)
        pmax = prm.get("max", 4.0)
        inputs.append(_slider(f"In{pname}", pname, default=default,
                              min_=pmin, max_=pmax))
        set_lines.append(f"    paramblock.{pname} = In{pname}:GetValue(req).Value")
        param_struct.append(f"    {ptype} {pname};")

    inputs_block = "\n".join(inputs)
    set_block = "\n".join(set_lines)
    struct_body = "\n".join(param_struct)

    return header(name, "dctl_kernel", "tool") + f'''FuRegisterClass("{name}", CT_Tool, {{
    REGS_Category = "Fuses\\\\MCP",
    REGS_OpIconString = "{name[:4]}",
    REGS_OpDescription = "DCTL GPU kernel",
}})

KernelParams = [[
{struct_body}
    int srcsize[2];
]]

KernelSource = [[
__KERNEL__ void {name}Kernel(__CONSTANTREF__ KernelParams *params,
                              __TEXTURE2D__ src,
                              __TEXTURE2D_WRITE__ dst)
{{
    DEFINE_KERNEL_ITERATORS_XY(x, y);
    if (x < params->srcsize[0] && y < params->srcsize[1])
    {{
{kernel}
    }}
}}
]]

function Create()
{inputs_block}

    InImage = self:AddInput("Input", "Input", {{
        LINKID_DataType = "Image",
        LINK_Main = 1,
    }})

    OutImage = self:AddOutput("Output", "Output", {{
        LINKID_DataType = "Image",
        LINK_Main = 1,
    }})
end

function Process(req)
    local img = InImage:GetValue(req)
    local out = Image({{ IMG_Like = img }})

    local node = DVIPComputeNode(req, "{name}Kernel", KernelSource,
                                 "KernelParams", KernelParams)
    local paramblock = node:GetParamBlock(KernelParams)
{set_block}
    paramblock.srcsize[0] = img.DataWindow:Width()
    paramblock.srcsize[1] = img.DataWindow:Height()
    node:SetParamBlock(paramblock)

    node:AddInput("src", img)
    node:AddOutput("dst", out)
    node:AddSampler("RowSampler", TEX_FILTER_MODE_LINEAR,
                    TEX_ADDRESS_MODE_CLAMP, TEX_NORMALIZED_COORDS_FALSE)

    local ok = node:RunSession(req)
    if not ok then
        print("DCTL kernel run failed for {name}")
    end

    OutImage:Set(req, out)
end
'''


def source_generator(name: str, options: Optional[Dict[str, Any]] = None) -> str:
    """Source-only Fuse with no image input. Outputs procedurally-generated content.

    options:
        kind: 'noise' | 'gradient' | 'checkerboard' | 'solid' (default 'noise')
        width:  int (default 1920)
        height: int (default 1080)

    Uses CT_Tool with REG_Source_*Ctrls registration so Fusion treats this as
    a source generator (no image input slot, sized by the project). See the
    SDK's GPUSampleFuse example (p. 140).
    """
    options = options or {}
    kind = options.get("kind", "noise")
    if kind not in ("noise", "gradient", "checkerboard", "solid"):
        raise ValueError(f"Invalid source_generator kind '{kind}'. "
                         "Valid: noise, gradient, checkerboard, solid")
    width = int(options.get("width", 1920))
    height = int(options.get("height", 1080))

    if kind == "noise":
        body = '''    local p = Pixel({A = 1})
    for y = 0, out.Height - 1 do
        for x = 0, out.Width - 1 do
            p.R = math.random()
            p.G = math.random()
            p.B = math.random()
            out:SetPixel(x, y, p)
        end
    end'''
    elif kind == "gradient":
        body = '''    local p = Pixel({A = 1})
    for y = 0, out.Height - 1 do
        local v = y / (out.Height - 1)
        for x = 0, out.Width - 1 do
            local u = x / (out.Width - 1)
            p.R = u
            p.G = v
            p.B = 1 - u
            out:SetPixel(x, y, p)
        end
    end'''
    elif kind == "checkerboard":
        body = '''    local p = Pixel({A = 1})
    local cell = 64
    for y = 0, out.Height - 1 do
        for x = 0, out.Width - 1 do
            local on = ((math.floor(x / cell) + math.floor(y / cell)) % 2) == 0
            local v = on and 1 or 0
            p.R, p.G, p.B = v, v, v
            out:SetPixel(x, y, p)
        end
    end'''
    else:  # solid
        body = '''    local r = InColorR:GetValue(req).Value
    local g = InColorG:GetValue(req).Value
    local b = InColorB:GetValue(req).Value
    out:Fill(Pixel({R = r, G = g, B = b, A = 1}))'''

    extra_inputs = ""
    if kind == "solid":
        extra_inputs = (
            _slider("InColorR", "Red", default=1.0, min_=0.0, max_=1.0)
            + _slider("InColorG", "Green", default=1.0, min_=0.0, max_=1.0)
            + _slider("InColorB", "Blue", default=1.0, min_=0.0, max_=1.0)
        )

    return header(name, "source_generator", "tool") + f'''FuRegisterClass("{name}", CT_Tool, {{
    REGS_Category = "Fuses\\\\MCP",
    REGS_OpIconString = "{name[:4]}",
    REGS_OpDescription = "Source generator ({kind})",
    REG_NoBlendCtrls = true,
    REG_NoMotionBlurCtrls = true,
    REG_NoObjMatCtrls = true,
    REG_OpNoMask = true,
    REG_Source_GlobalCtrls = true,
    REG_Source_SizeCtrls = true,
    REG_Source_AspectCtrls = true,
    REG_Source_DepthCtrls = true,
}})

function Create()
{extra_inputs}
    OutImage = self:AddOutput("Output", "Output", {{
        LINKID_DataType = "Image",
        LINK_Main = 1,
    }})
end

function Process(req)
    local realwidth = Width
    local realheight = Height
    Width = Width / Scale
    Height = Height / Scale
    Scale = 1

    local imgattrs = {{
        IMG_Document = self.Comp,
        {{ IMG_Channel = "Red" }},
        {{ IMG_Channel = "Green" }},
        {{ IMG_Channel = "Blue" }},
        {{ IMG_Channel = "Alpha" }},
        IMG_Width = Width,
        IMG_Height = Height,
        IMG_XScale = XAspect,
        IMG_YScale = YAspect,
        IMAT_OriginalWidth = realwidth,
        IMAT_OriginalHeight = realheight,
        IMG_Quality = not req:IsQuick(),
        IMG_MotionBlurQuality = not req:IsNoMotionBlur(),
    }}
    if not req:IsStampOnly() then
        imgattrs.IMG_ProxyScale = 1
    end
    if SourceDepth ~= 0 then
        imgattrs.IMG_Depth = SourceDepth
    end

    local out = Image(imgattrs)
{body}

    OutImage:Set(req, out)
end
'''


def time_displace(name: str, options: Optional[Dict[str, Any]] = None) -> str:
    """Temporal effect Fuse: outputs the input at a time-shifted frame.

    options:
        default_offset: integer frame offset (default -1, i.e. previous frame)

    Uses InImage:GetSource(req.Time + offset) to read a different frame than
    the current request time. Foundation for motion blur, frame averaging,
    deja-vu, and time-displacement effects. See SDK p. 45.
    """
    options = options or {}
    default_offset = int(options.get("default_offset", -1))

    return header(name, "time_displace", "tool") + f'''FuRegisterClass("{name}", CT_Tool, {{
    REGS_Category = "Fuses\\\\MCP",
    REGS_OpIconString = "{name[:4]}",
    REGS_OpDescription = "Time displace (temporal frame shift)",
}})

function Create()
    InOffset = self:AddInput("Frame Offset", "FrameOffset", {{
        LINKID_DataType = "Number",
        INPID_InputControl = "SliderControl",
        INP_Integer = true,
        INP_Default = {default_offset},
        INP_MinScale = -30,
        INP_MaxScale = 30,
    }})

    InImage = self:AddInput("Input", "Input", {{
        LINKID_DataType = "Image",
        LINK_Main = 1,
        INP_SendRequest = false,
    }})

    OutImage = self:AddOutput("Output", "Output", {{
        LINKID_DataType = "Image",
        LINK_Main = 1,
    }})
end

function Process(req)
    local offset = InOffset:GetValue(req).Value
    -- GetSource fetches the input at a frame other than the current request time.
    local img = InImage:GetSource(req.Time + offset)
    if img == nil then
        -- No frame available at that time (e.g. start/end of timeline);
        -- fall back to current frame.
        img = InImage:GetValue(req)
    end
    OutImage:Set(req, img)
end
'''


def channel_op(name: str, options: Optional[Dict[str, Any]] = None) -> str:
    """Channel operation Fuse with full aux-channel exposure.

    options:
        operation: 'Copy' | 'Add' | 'Subtract' | 'Multiply' | 'Divide' |
                   'Max' | 'Min' | 'Invert' | 'Difference' | 'SignedAdd' |
                   'Threshold' (default 'Copy')
        rgba_only: bool — if true, only RGBA controls are exposed
                   (default true). Set false to also expose Z/Object/Position
                   channel mappings.

    Wraps Image:ChannelOpOf (SDK pp. 71-72) which gives fine-grained control
    over how individual channels combine across two images. Real workflow:
    multipass / VFX deep-comp where you need to remap aux channels.
    """
    options = options or {}
    operation = options.get("operation", "Copy")
    valid_ops = ("Copy", "Add", "Subtract", "Multiply", "Divide", "Max", "Min",
                 "Invert", "Difference", "SignedAdd", "Threshold")
    if operation not in valid_ops:
        raise ValueError(f"Invalid channel_op operation '{operation}'. "
                         f"Valid: {list(valid_ops)}")
    rgba_only = bool(options.get("rgba_only", True))

    aux_channels = ""
    aux_doc = ""
    if not rgba_only:
        aux_doc = (
            "\n-- Pixel struct also exposes Z, U/V/W, Coverage, ObjectID,\n"
            "-- MaterialID, NX/NY/NZ, BgR/G/B/A, VectorX/Y, BackVectorX/Y,\n"
            "-- DisparityX/Y, PositionX/Y/Z. Reference any of these in the\n"
            "-- options table values (e.g. R = 'Fg.Z' to map Z buffer into Red).\n"
        )

    return header(name, "channel_op", "tool") + f'''FuRegisterClass("{name}", CT_Tool, {{
    REGS_Category = "Fuses\\\\MCP",
    REGS_OpIconString = "{name[:4]}",
    REGS_OpDescription = "Channel operation ({operation})",
}})
{aux_doc}
function Create()
    InOperation = self:AddInput("Operation", "Operation", {{
        LINKID_DataType = "Number",
        INPID_InputControl = "ComboControl",
        INP_Integer = true,
        INP_Default = 0,
        {{ CCS_AddString = "Copy" }},
        {{ CCS_AddString = "Add" }},
        {{ CCS_AddString = "Subtract" }},
        {{ CCS_AddString = "Multiply" }},
        {{ CCS_AddString = "Divide" }},
        {{ CCS_AddString = "Max" }},
        {{ CCS_AddString = "Min" }},
        {{ CCS_AddString = "Invert" }},
        {{ CCS_AddString = "Difference" }},
        {{ CCS_AddString = "SignedAdd" }},
        {{ CCS_AddString = "Threshold" }},
    }})

    InBackground = self:AddInput("Background", "Background", {{
        LINKID_DataType = "Image",
        LINK_Main = 1,
    }})

    InForeground = self:AddInput("Foreground", "Foreground", {{
        LINKID_DataType = "Image",
        LINK_Main = 2,
        INP_Required = false,
    }})

    OutImage = self:AddOutput("Output", "Output", {{
        LINKID_DataType = "Image",
        LINK_Main = 1,
    }})
end

local op_table = {{
    [0] = "Copy", "Add", "Subtract", "Multiply", "Divide",
    "Max", "Min", "Invert", "Difference", "SignedAdd", "Threshold",
}}

function Process(req)
    local bg = InBackground:GetValue(req)
    local fg = InForeground:GetValue(req)
    if fg == nil then
        fg = bg
    end
    local op = op_table[InOperation:GetValue(req).Value]

    -- Default mapping: copy each channel from foreground.
    local out = bg:ChannelOpOf(op, fg, {{
        R = "Fg.R", G = "Fg.G", B = "Fg.B", A = "Fg.A",
    }})

    OutImage:Set(req, out)
end
'''


def spatial_warp(name: str, options: Optional[Dict[str, Any]] = None) -> str:
    """Per-pixel spatial warp Fuse. Each output pixel samples a different
    source location.

    options:
        edge_mode: 'wrap' | 'clamp' | 'black' (default 'wrap')
                   Selects SamplePixelW / SamplePixelD / SamplePixelB.
        warp:      'sine' | 'scatter' | 'pinch' (default 'sine')

    Reference implementation: SDK Example7_Sampling.fuse (pp. 37–38).
    """
    options = options or {}
    edge_mode = options.get("edge_mode", "wrap")
    if edge_mode not in ("wrap", "clamp", "black"):
        raise ValueError(f"Invalid edge_mode '{edge_mode}'. "
                         "Valid: wrap, clamp, black")
    warp = options.get("warp", "sine")
    if warp not in ("sine", "scatter", "pinch"):
        raise ValueError(f"Invalid warp '{warp}'. "
                         "Valid: sine, scatter, pinch")
    sample_fn = {"wrap": "SamplePixelW", "clamp": "SamplePixelD",
                 "black": "SamplePixelB"}[edge_mode]

    if warp == "sine":
        warp_body = '''            local xt = x - amp * math.sin(y * freq + phase)
            local yt = y - amp * math.sin(x * freq + phase)
            img:{sample_fn}(xt, yt, sp)'''
    elif warp == "scatter":
        warp_body = '''            img:GetPixel(x, y, sp)
            local xt = x - amp * 5 * (sp.R - 0.5)
            local yt = y - amp * 5 * (sp.B - 0.5)
            img:{sample_fn}(xt, yt, sp)'''
    else:  # pinch
        warp_body = '''            local dx = x - cx
            local dy = y - cy
            local r = math.sqrt(dx*dx + dy*dy)
            local k = 1 + amp * math.exp(-r / (freq * 100))
            img:{sample_fn}(cx + dx / k, cy + dy / k, sp)'''
    warp_body = warp_body.replace("{sample_fn}", sample_fn)

    return header(name, "spatial_warp", "tool") + f'''FuRegisterClass("{name}", CT_Tool, {{
    REGS_Category = "Fuses\\\\MCP",
    REGS_OpIconString = "{name[:4]}",
    REGS_OpDescription = "Spatial warp ({warp}, {edge_mode})",
}})

function Create()
    InAmp = self:AddInput("Amplitude", "Amplitude", {{
        LINKID_DataType = "Number",
        INPID_InputControl = "SliderControl",
        INP_Default = 5.0,
        INP_MinScale = 0.0,
        INP_MaxScale = 50.0,
    }})

    InFreq = self:AddInput("Frequency", "Frequency", {{
        LINKID_DataType = "Number",
        INPID_InputControl = "SliderControl",
        INP_Default = 0.05,
        INP_MinScale = 0.0,
        INP_MaxScale = 1.0,
    }})

    InPhase = self:AddInput("Phase", "Phase", {{
        LINKID_DataType = "Number",
        INPID_InputControl = "ScrewControl",
        INP_Default = 0.0,
        INP_MinScale = 0.0,
        INP_MaxScale = 6.283,
    }})

    InImage = self:AddInput("Input", "Input", {{
        LINKID_DataType = "Image",
        LINK_Main = 1,
    }})

    OutImage = self:AddOutput("Output", "Output", {{
        LINKID_DataType = "Image",
        LINK_Main = 1,
    }})
end

function Process(req)
    local img = InImage:GetValue(req)
    local amp = InAmp:GetValue(req).Value
    local freq = InFreq:GetValue(req).Value
    local phase = InPhase:GetValue(req).Value

    local out = Image({{ IMG_Like = img }})
    local sp = Pixel()
    local cx = img.Width / 2
    local cy = img.Height / 2

    for y = 0, img.Height - 1 do
        for x = 0, img.Width - 1 do
{warp_body}
            out:SetPixel(x, y, sp)
        end
    end

    OutImage:Set(req, out)
end
'''


def builtin_blur(name: str, options: Optional[Dict[str, Any]] = None) -> str:
    """Blur Fuse with full BLUR_Type / per-channel-mask exposure.

    options:
        default_type: 'Box' | 'Soften' | 'Bartlett' | 'Sharpen' | 'Gaussian'
                      | 'Hilight' | 'Blend' | 'Solarise' (default 'Gaussian')

    Wraps Image:Blur with all 8 documented BLUR_Type values + per-channel
    enable/disable. Stock Blur tool is great but hides MultiBox/Solarise/
    FastGaussian. SDK pp. 69-70.
    """
    options = options or {}
    valid_types = ("Box", "Soften", "Bartlett", "Sharpen", "Gaussian",
                   "Hilight", "Blend", "Solarise")
    default_type = options.get("default_type", "Gaussian")
    if default_type not in valid_types:
        raise ValueError(f"Invalid default_type '{default_type}'. "
                         f"Valid: {list(valid_types)}")
    default_idx = valid_types.index(default_type)

    return header(name, "builtin_blur", "tool") + f'''FuRegisterClass("{name}", CT_Tool, {{
    REGS_Category = "Fuses\\\\MCP",
    REGS_OpIconString = "{name[:4]}",
    REGS_OpDescription = "Configurable blur",
}})

function Create()
    InType = self:AddInput("Filter", "Filter", {{
        LINKID_DataType = "Number",
        INPID_InputControl = "ComboControl",
        INP_Integer = true,
        INP_Default = {default_idx},
        {{ CCS_AddString = "Box" }},
        {{ CCS_AddString = "Soften" }},
        {{ CCS_AddString = "Bartlett" }},
        {{ CCS_AddString = "Sharpen" }},
        {{ CCS_AddString = "Gaussian" }},
        {{ CCS_AddString = "Hilight" }},
        {{ CCS_AddString = "Blend" }},
        {{ CCS_AddString = "Solarise" }},
    }})

    InSize = self:AddInput("Size", "Size", {{
        LINKID_DataType = "Number",
        INPID_InputControl = "SliderControl",
        INP_Default = 5.0,
        INP_MinScale = 0.0,
        INP_MaxScale = 100.0,
    }})

    InRed = self:AddInput("Red", "Red", {{
        LINKID_DataType = "Number", INPID_InputControl = "CheckboxControl",
        INP_Integer = true, INP_Default = 1.0, ICD_Width = 0.25,
    }})
    InGreen = self:AddInput("Green", "Green", {{
        LINKID_DataType = "Number", INPID_InputControl = "CheckboxControl",
        INP_Integer = true, INP_Default = 1.0, ICD_Width = 0.25,
    }})
    InBlue = self:AddInput("Blue", "Blue", {{
        LINKID_DataType = "Number", INPID_InputControl = "CheckboxControl",
        INP_Integer = true, INP_Default = 1.0, ICD_Width = 0.25,
    }})
    InAlpha = self:AddInput("Alpha", "Alpha", {{
        LINKID_DataType = "Number", INPID_InputControl = "CheckboxControl",
        INP_Integer = true, INP_Default = 0.0, ICD_Width = 0.25,
    }})

    InImage = self:AddInput("Input", "Input", {{
        LINKID_DataType = "Image",
        LINK_Main = 1,
    }})

    OutImage = self:AddOutput("Output", "Output", {{
        LINKID_DataType = "Image",
        LINK_Main = 1,
    }})
end

local type_table = {{
    [0] = "Box", "Soften", "Bartlett", "Sharpen", "Gaussian",
    "Hilight", "Blend", "Solarise",
}}

function Process(req)
    local img = InImage:GetValue(req)
    local size = InSize:GetValue(req).Value
    local out = Image({{ IMG_Like = img }})

    img:Blur(out, {{
        BLUR_Type = type_table[InType:GetValue(req).Value],
        BLUR_Red = InRed:GetValue(req).Value > 0.5,
        BLUR_Green = InGreen:GetValue(req).Value > 0.5,
        BLUR_Blue = InBlue:GetValue(req).Value > 0.5,
        BLUR_Alpha = InAlpha:GetValue(req).Value > 0.5,
        BLUR_XSize = size / img.OriginalWidth,
        BLUR_YSize = size / img.OriginalWidth,
    }})

    OutImage:Set(req, out)
end
'''


def builtin_resize(name: str, options: Optional[Dict[str, Any]] = None) -> str:
    """Resize Fuse with all 12 documented filter methods + Lanczos windowing.

    Stock Resize tool exposes a subset; this gives full control including
    Lanczos+window combinations for production scaling. SDK p. 91.
    """
    options = options or {}
    return header(name, "builtin_resize", "tool") + f'''FuRegisterClass("{name}", CT_Tool, {{
    REGS_Category = "Fuses\\\\MCP",
    REGS_OpIconString = "{name[:4]}",
    REGS_OpDescription = "Configurable resize",
}})

function Create()
    InFilter = self:AddInput("Filter", "Filter", {{
        LINKID_DataType = "Number",
        INPID_InputControl = "ComboControl",
        INP_Integer = true,
        INP_Default = 11,  -- Lanczos
        {{ CCS_AddString = "TopLeft" }},
        {{ CCS_AddString = "Nearest" }},
        {{ CCS_AddString = "Box" }},
        {{ CCS_AddString = "Linear" }},
        {{ CCS_AddString = "BiLinear" }},
        {{ CCS_AddString = "Quadratic" }},
        {{ CCS_AddString = "BiCubic" }},
        {{ CCS_AddString = "Cubic" }},
        {{ CCS_AddString = "BSpline" }},
        {{ CCS_AddString = "CatmullRom" }},
        {{ CCS_AddString = "Gaussian" }},
        {{ CCS_AddString = "Mitchell" }},
        {{ CCS_AddString = "Lanczos" }},
        {{ CCS_AddString = "Sinc" }},
        {{ CCS_AddString = "Bessel" }},
    }})

    InWindow = self:AddInput("Window", "Window", {{
        LINKID_DataType = "Number",
        INPID_InputControl = "ComboControl",
        INP_Integer = true,
        INP_Default = 0,
        {{ CCS_AddString = "Hanning" }},
        {{ CCS_AddString = "Hamming" }},
        {{ CCS_AddString = "Blackman" }},
        {{ CCS_AddString = "Kaiser" }},
    }})

    InWidth = self:AddInput("Width", "Width", {{
        LINKID_DataType = "Number",
        INPID_InputControl = "SliderControl",
        INP_Integer = true,
        INP_Default = 1920,
        INP_MinScale = 1, INP_MaxScale = 7680,
    }})

    InHeight = self:AddInput("Height", "Height", {{
        LINKID_DataType = "Number",
        INPID_InputControl = "SliderControl",
        INP_Integer = true,
        INP_Default = 1080,
        INP_MinScale = 1, INP_MaxScale = 4320,
    }})

    InImage = self:AddInput("Input", "Input", {{
        LINKID_DataType = "Image",
        LINK_Main = 1,
    }})

    OutImage = self:AddOutput("Output", "Output", {{
        LINKID_DataType = "Image",
        LINK_Main = 1,
    }})
end

local filter_table = {{
    [0] = "TopLeft", "Nearest", "Box", "Linear", "BiLinear",
    "Quadratic", "BiCubic", "Cubic", "BSpline", "CatmullRom",
    "Gaussian", "Mitchell", "Lanczos", "Sinc", "Bessel",
}}
local window_table = {{ [0] = "Hanning", "Hamming", "Blackman", "Kaiser" }}

function Process(req)
    local img = InImage:GetValue(req)
    local filter = filter_table[InFilter:GetValue(req).Value]
    local w = InWidth:GetValue(req).Value
    local h = InHeight:GetValue(req).Value

    local out = Image({{ IMG_Like = img, IMG_Width = w, IMG_Height = h }})
    img:Resize(out, {{
        RSZ_Filter = filter,
        RSZ_Window = window_table[InWindow:GetValue(req).Value],
        RSZ_Width = w,
        RSZ_Height = h,
    }})

    OutImage:Set(req, out)
end
'''


def controls_demo(name: str, options: Optional[Dict[str, Any]] = None) -> str:
    """Reference Fuse demonstrating ALL UI control types in nested groups.

    Educational template — when an LLM (or human) wants a worked example of
    every available UI control, generate this and read the source. Mirrors
    SDK Example2_UIControls.fuse.
    """
    return header(name, "controls_demo", "tool") + f'''FuRegisterClass("{name}", CT_Tool, {{
    REGS_Category = "Fuses\\\\MCP",
    REGS_OpIconString = "{name[:4]}",
    REGS_OpDescription = "Reference: every UI control type",
}})

function Create()
    self:AddControlPage("Controls")

    self:BeginControlNest("Sliders", "Sliders", true, {{}})
    InSliderFloat = self:AddInput("Slider Float", "SliderFloat", {{
        LINKID_DataType = "Number", INPID_InputControl = "SliderControl",
        INP_Default = 0.5, INP_MinScale = 0.0, INP_MaxScale = 1.0,
    }})
    InSliderInt = self:AddInput("Slider Int", "SliderInt", {{
        LINKID_DataType = "Number", INPID_InputControl = "SliderControl",
        INP_Integer = true, INP_Default = 1, INP_MinScale = 0, INP_MaxScale = 10,
    }})
    InScrew = self:AddInput("Thumbwheel", "Thumbwheel", {{
        LINKID_DataType = "Number", INPID_InputControl = "ScrewControl",
        INP_Default = 0,
    }})
    InRangeLow = self:AddInput("Range Low", "RangeLow", {{
        LINKID_DataType = "Number", INPID_InputControl = "RangeControl",
        IC_ControlGroup = 1, IC_ControlID = 0, INP_Default = 0.1,
    }})
    InRangeHigh = self:AddInput("Range High", "RangeHigh", {{
        LINKID_DataType = "Number", INPID_InputControl = "RangeControl",
        IC_ControlGroup = 1, IC_ControlID = 1, INP_Default = 0.9,
    }})
    self:EndControlNest()

    self:BeginControlNest("Toggles & Buttons", "Toggles", false, {{}})
    InCheck = self:AddInput("Checkbox", "Checkbox", {{
        LINKID_DataType = "Number", INPID_InputControl = "CheckboxControl",
        INP_Integer = true, INP_Default = 1.0,
    }})
    InCombo = self:AddInput("Combo", "Combo", {{
        LINKID_DataType = "Number", INPID_InputControl = "ComboControl",
        INP_Integer = true, INP_Default = 0,
        {{ CCS_AddString = "Option A" }},
        {{ CCS_AddString = "Option B" }},
        {{ CCS_AddString = "Option C" }},
    }})
    InMulti = self:AddInput("Multi-button", "Multi", {{
        LINKID_DataType = "Number", INPID_InputControl = "MultiButtonControl",
        INP_Default = 0, INP_Integer = true,
        {{ MBTNC_AddButton = "X", MBTNCD_ButtonWidth = 0.33 }},
        {{ MBTNC_AddButton = "Y", MBTNCD_ButtonWidth = 0.33 }},
        {{ MBTNC_AddButton = "Z", MBTNCD_ButtonWidth = 0.33 }},
    }})
    InButton = self:AddInput("Button", "Button", {{
        LINKID_DataType = "Text", INPID_InputControl = "ButtonControl",
        INP_DoNotifyChanged = true, INP_External = false,
    }})
    self:EndControlNest()

    self:BeginControlNest("Color & Gradient", "Colors", false, {{}})
    InGrad = self:AddInput("Gradient", "Gradient", {{
        LINKID_DataType = "Gradient", INPID_InputControl = "GradientControl",
        INP_DelayDefault = true,
    }})
    InR = self:AddInput("Red", "Red", {{
        LINKID_DataType = "Number", INPID_InputControl = "ColorControl",
        IC_ControlGroup = 2, IC_ControlID = 0, INP_Default = 1.0,
    }})
    InG = self:AddInput("Green", "Green", {{
        LINKID_DataType = "Number", INPID_InputControl = "ColorControl",
        IC_ControlGroup = 2, IC_ControlID = 1, INP_Default = 1.0,
    }})
    InB = self:AddInput("Blue", "Blue", {{
        LINKID_DataType = "Number", INPID_InputControl = "ColorControl",
        IC_ControlGroup = 2, IC_ControlID = 2, INP_Default = 1.0,
    }})
    self:EndControlNest()

    self:BeginControlNest("Text & File", "Text", false, {{}})
    InText = self:AddInput("Text", "Text", {{
        LINKID_DataType = "Text", INPID_InputControl = "TextEditControl",
        TEC_Lines = 2,
    }})
    InFile = self:AddInput("File", "File", {{
        LINKID_DataType = "Text", INPID_InputControl = "FileControl",
        FC_ClipBrowse = true,
    }})
    self:EndControlNest()

    self:BeginControlNest("Onscreen", "Onscreen", false, {{}})
    InCenter = self:AddInput("Center", "Center", {{
        LINKID_DataType = "Point", INPID_InputControl = "OffsetControl",
        INPID_PreviewControl = "CrosshairControl",
        INP_DefaultX = 0.5, INP_DefaultY = 0.5,
    }})
    self:EndControlNest()

    InImage = self:AddInput("Input", "Input", {{
        LINKID_DataType = "Image", LINK_Main = 1, INP_Required = false,
    }})

    OutImage = self:AddOutput("Output", "Output", {{
        LINKID_DataType = "Image", LINK_Main = 1,
    }})
end

function NotifyChanged(inp, param, time)
    if inp == InButton and param.Value == 1 then
        print("Button pressed at frame " .. tostring(time))
    end
end

function Process(req)
    local img = InImage:GetValue(req)
    if img == nil then
        -- Stub output when no image connected. Caller should normally connect one.
        local stub = Image({{
            IMG_Document = self.Comp,
            {{IMG_Channel = "Red"}}, {{IMG_Channel = "Green"}},
            {{IMG_Channel = "Blue"}}, {{IMG_Channel = "Alpha"}},
            IMG_Width = 64, IMG_Height = 64,
            IMG_XScale = 1.0, IMG_YScale = 1.0,
        }})
        stub:Fill(Pixel({{R = 0, G = 0, B = 0, A = 0}}))
        OutImage:Set(req, stub)
        return
    end
    OutImage:Set(req, img)
end
'''


def shape_generator(name: str, options: Optional[Dict[str, Any]] = None) -> str:
    """Vector shape Fuse: draws a parametric shape (circle/rect/star) as a
    filled image overlay.

    options:
        shape: 'circle' | 'rect' | 'star' (default 'circle')

    Demonstrates Shape + AddRectangle / MoveTo / LineTo / OutlineOfShape +
    ChannelStyle + ImageChannel + PutToImage. See SDK Example5_Shapes.
    """
    options = options or {}
    shape_kind = options.get("shape", "circle")
    if shape_kind not in ("circle", "rect", "star"):
        raise ValueError(f"Invalid shape '{shape_kind}'. "
                         "Valid: circle, rect, star")

    if shape_kind == "circle":
        # Approximate circle using bezier segments.
        shape_body = '''    -- Circle approximated with 4 cubic beziers (k = 0.5522847498).
    local k = 0.5522847498 * radius
    sh:MoveTo(radius, 0)
    sh:BezierTo(0, radius, k, 0, radius, k)
    sh:BezierTo(-radius, 0, 0, radius, -k, 0)
    sh:BezierTo(0, -radius, -k, 0, -radius, -k)
    sh:BezierTo(radius, 0, 0, -radius, k, 0)
    sh:Close()'''
    elif shape_kind == "rect":
        shape_body = '''    sh:AddRectangle(-radius, -radius, radius, radius, 0.0, 8)'''
    else:  # star
        shape_body = '''    -- 5-point star.
    local n = 5
    for i = 0, 2 * n - 1 do
        local a = (i / (2 * n)) * 2 * math.pi - math.pi / 2
        local r = (i % 2 == 0) and radius or radius * 0.4
        local x, y = math.cos(a) * r, math.sin(a) * r
        if i == 0 then sh:MoveTo(x, y) else sh:LineTo(x, y) end
    end
    sh:Close()'''

    return header(name, "shape_generator", "tool") + f'''FuRegisterClass("{name}", CT_Tool, {{
    REGS_Category = "Fuses\\\\MCP",
    REGS_OpIconString = "{name[:4]}",
    REGS_OpDescription = "Vector shape ({shape_kind})",
}})

function Create()
    InCenter = self:AddInput("Center", "Center", {{
        LINKID_DataType = "Point",
        INPID_InputControl = "OffsetControl",
        INPID_PreviewControl = "CrosshairControl",
        INP_DefaultX = 0.5, INP_DefaultY = 0.5,
    }})

    InRadius = self:AddInput("Radius", "Radius", {{
        LINKID_DataType = "Number",
        INPID_InputControl = "SliderControl",
        INP_Default = 0.2,
        INP_MinScale = 0.0, INP_MaxScale = 1.0,
    }})

    InR = self:AddInput("Red", "Red", {{
        LINKID_DataType = "Number", INPID_InputControl = "ColorControl",
        IC_ControlGroup = 1, IC_ControlID = 0, INP_Default = 1.0,
    }})
    InG = self:AddInput("Green", "Green", {{
        LINKID_DataType = "Number", INPID_InputControl = "ColorControl",
        IC_ControlGroup = 1, IC_ControlID = 1, INP_Default = 1.0,
    }})
    InB = self:AddInput("Blue", "Blue", {{
        LINKID_DataType = "Number", INPID_InputControl = "ColorControl",
        IC_ControlGroup = 1, IC_ControlID = 2, INP_Default = 1.0,
    }})
    InA = self:AddInput("Alpha", "Alpha", {{
        LINKID_DataType = "Number", INPID_InputControl = "ColorControl",
        IC_ControlGroup = 1, IC_ControlID = 3, INP_Default = 1.0,
    }})

    InImage = self:AddInput("Input", "Input", {{
        LINKID_DataType = "Image",
        LINK_Main = 1,
        INP_Required = false,
    }})

    OutImage = self:AddOutput("Output", "Output", {{
        LINKID_DataType = "Image",
        LINK_Main = 1,
    }})
end

function Process(req)
    local img = InImage:GetValue(req)
    if img == nil then
        img = Image({{
            IMG_Document = self.Comp,
            {{ IMG_Channel = "Red" }}, {{ IMG_Channel = "Green" }},
            {{ IMG_Channel = "Blue" }}, {{ IMG_Channel = "Alpha" }},
            IMG_Width = 1920, IMG_Height = 1080,
            IMG_XScale = 1.0, IMG_YScale = 1.0,
        }})
        img:Fill(Pixel({{R = 0, G = 0, B = 0, A = 0}}))
    end

    local out = img:CopyOf()
    local center = InCenter:GetValue(req)
    local radius = InRadius:GetValue(req).Value
    local r = InR:GetValue(req).Value
    local g = InG:GetValue(req).Value
    local b = InB:GetValue(req).Value
    local a = InA:GetValue(req).Value

    local ic = ImageChannel(out, 8)
    local fs = FillStyle()
    local cs = ChannelStyle()
    cs.Color = Pixel({{R = r, G = g, B = b, A = a}})
    ic:SetStyleFill(fs)

    local mat = Matrix4()
    -- Y-squash conversion so on-screen Center lands correctly.
    local cy = center.Y * (out.Height * out.YScale) / (out.Width * out.XScale)
    mat:Move(center.X, cy, 0)

    local sh = Shape()
{shape_body}
    sh = sh:TransformOfShape(mat)

    ic:ShapeFill(sh)
    ic:PutToImage("CM_Merge", cs)

    OutImage:Set(req, out)
end
'''


def notifychanged_demo(name: str, options: Optional[Dict[str, Any]] = None) -> str:
    """Demonstrates dynamic UI: NotifyChanged hides/shows controls based on a
    checkbox state.

    Reference pattern from SDK Example2_UIControls.fuse (pp. 23-24). When the
    `Show Details` checkbox is toggled, the detail sliders appear or hide.
    """
    return header(name, "notifychanged_demo", "tool") + f'''FuRegisterClass("{name}", CT_Tool, {{
    REGS_Category = "Fuses\\\\MCP",
    REGS_OpIconString = "{name[:4]}",
    REGS_OpDescription = "NotifyChanged dynamic-UI demo",
}})

function Create()
    InToggle = self:AddInput("Show Details", "ShowDetails", {{
        LINKID_DataType = "Number",
        INPID_InputControl = "CheckboxControl",
        INP_Integer = true,
        INP_Default = 0.0,
        INP_DoNotifyChanged = true,
    }})

    InDetailA = self:AddInput("Detail A", "DetailA", {{
        LINKID_DataType = "Number",
        INPID_InputControl = "SliderControl",
        INP_Default = 0.5,
        IC_Visible = false,
    }})

    InDetailB = self:AddInput("Detail B", "DetailB", {{
        LINKID_DataType = "Number",
        INPID_InputControl = "SliderControl",
        INP_Default = 0.5,
        IC_Visible = false,
    }})

    InImage = self:AddInput("Input", "Input", {{
        LINKID_DataType = "Image",
        LINK_Main = 1,
    }})

    OutImage = self:AddOutput("Output", "Output", {{
        LINKID_DataType = "Image",
        LINK_Main = 1,
    }})
end

function NotifyChanged(inp, param, time)
    if inp == InToggle then
        local visible = (param.Value > 0.5)
        InDetailA:SetAttrs({{ IC_Visible = visible }})
        InDetailB:SetAttrs({{ IC_Visible = visible }})
    end
end

function Process(req)
    OutImage:Set(req, InImage:GetValue(req))
end
'''


def point_modifier(name: str, options: Optional[Dict[str, Any]] = None) -> str:
    """Modifier producing a Point (X,Y) value over time. Use to drive
    on-screen positions like a Merge tool's Center input.

    options:
        kind: 'orbit' | 'figure_eight' | 'spring' (default 'orbit')

    Wires up via Inspector → right-click any Point input → Modify with → [name].
    Higher-risk than the Number modifier because CT_Modifier with Point output
    isn't covered by an SDK example.
    """
    options = options or {}
    kind = options.get("kind", "orbit")
    if kind not in ("orbit", "figure_eight", "spring"):
        raise ValueError(f"Invalid point_modifier kind '{kind}'. "
                         "Valid: orbit, figure_eight, spring")

    if kind == "orbit":
        formula = '''    local x = cx + r * math.cos(2 * math.pi * t * freq + phase)
    local y = cy + r * math.sin(2 * math.pi * t * freq + phase)'''
    elif kind == "figure_eight":
        formula = '''    local angle = 2 * math.pi * t * freq + phase
    local x = cx + r * math.sin(angle)
    local y = cy + r * math.sin(angle * 2) / 2'''
    else:  # spring
        formula = '''    local angle = 2 * math.pi * t * freq + phase
    local damping = math.exp(-t * 0.1)
    local x = cx + r * math.cos(angle) * damping
    local y = cy + r * math.sin(angle) * damping'''

    return header(name, "point_modifier", "modifier") + f'''FuRegisterClass("{name}", CT_Modifier, {{
    REGS_Category = "Fuses\\\\MCP",
    REGS_OpIconString = "{name[:4]}",
    REGS_OpDescription = "Point modifier ({kind})",
}})

function Create()
    InCenterX = self:AddInput("Center X", "CenterX", {{
        LINKID_DataType = "Number",
        INPID_InputControl = "SliderControl",
        INP_Default = 0.5, INP_MinScale = 0.0, INP_MaxScale = 1.0,
    }})

    InCenterY = self:AddInput("Center Y", "CenterY", {{
        LINKID_DataType = "Number",
        INPID_InputControl = "SliderControl",
        INP_Default = 0.5, INP_MinScale = 0.0, INP_MaxScale = 1.0,
    }})

    InRadius = self:AddInput("Radius", "Radius", {{
        LINKID_DataType = "Number",
        INPID_InputControl = "SliderControl",
        INP_Default = 0.2, INP_MinScale = 0.0, INP_MaxScale = 1.0,
    }})

    InFreq = self:AddInput("Frequency", "Frequency", {{
        LINKID_DataType = "Number",
        INPID_InputControl = "SliderControl",
        INP_Default = 0.05, INP_MinScale = 0.0, INP_MaxScale = 1.0,
    }})

    InPhase = self:AddInput("Phase", "Phase", {{
        LINKID_DataType = "Number",
        INPID_InputControl = "ScrewControl",
        INP_Default = 0.0, INP_MinScale = 0.0, INP_MaxScale = 6.283,
    }})

    OutPoint = self:AddOutput("Value", "Value", {{
        LINKID_DataType = "Point",
        LINK_Main = 1,
    }})
end

function Process(req)
    local t = req.Time
    local cx = InCenterX:GetValue(req).Value
    local cy = InCenterY:GetValue(req).Value
    local r = InRadius:GetValue(req).Value
    local freq = InFreq:GetValue(req).Value
    local phase = InPhase:GetValue(req).Value

{formula}

    OutPoint:Set(req, Point(x, y))
end
'''


def variable_blur(name: str, options: Optional[Dict[str, Any]] = None) -> str:
    """Variable-radius box blur using Summed Area Tables (SAT). Each pixel
    can have a different blur radius.

    options:
        radius_source: 'slider' | 'red' | 'alpha' (default 'slider')
                       'slider' = uniform radius from a slider input
                       'red'    = per-pixel radius driven by red channel
                       'alpha'  = per-pixel radius driven by alpha channel

    Demonstrates UseSAT() + SampleAreaW() + RecycleSAT() pattern (SDK pp. 92-95,
    99). Full per-pixel implementation — slower than Image:Blur for fixed
    radii but the right tool for radius-mapped effects (e.g. depth-of-field
    from a Z buffer).
    """
    options = options or {}
    radius_source = options.get("radius_source", "slider")
    if radius_source not in ("slider", "red", "alpha"):
        raise ValueError(f"Invalid radius_source '{radius_source}'. "
                         "Valid: slider, red, alpha")

    if radius_source == "slider":
        sample_radius = "    local r = max_radius"
    elif radius_source == "red":
        sample_radius = '''    img:GetPixel(x, y, ref_pixel)
    local r = max_radius * ref_pixel.R'''
    else:  # alpha
        sample_radius = '''    img:GetPixel(x, y, ref_pixel)
    local r = max_radius * ref_pixel.A'''

    return header(name, "variable_blur", "tool") + f'''FuRegisterClass("{name}", CT_Tool, {{
    REGS_Category = "Fuses\\\\MCP",
    REGS_OpIconString = "{name[:4]}",
    REGS_OpDescription = "Variable-radius blur (SAT-based, {radius_source})",
}})

function Create()
    InMaxRadius = self:AddInput("Max Radius", "MaxRadius", {{
        LINKID_DataType = "Number",
        INPID_InputControl = "SliderControl",
        INP_Integer = true,
        INP_Default = 16, INP_MinScale = 1, INP_MaxScale = 100,
    }})

    InImage = self:AddInput("Input", "Input", {{
        LINKID_DataType = "Image",
        LINK_Main = 1,
    }})

    OutImage = self:AddOutput("Output", "Output", {{
        LINKID_DataType = "Image",
        LINK_Main = 1,
    }})
end

function Process(req)
    local img = InImage:GetValue(req)
    local max_radius = InMaxRadius:GetValue(req).Value
    local out = Image({{ IMG_Like = img }})

    -- Build the Summed Area Table once. SampleArea functions need this.
    img:UseSAT()

    local sp = Pixel()
    local ref_pixel = Pixel()

    for y = 0, img.Height - 1 do
        if self.Status ~= "OK" then break end
        for x = 0, img.Width - 1 do
{sample_radius}
            r = math.max(1, math.floor(r))
            -- SampleAreaW expects 4 corner points (rectangle vertices).
            img:SampleAreaW(x - r, y - r, x + r, y - r,
                            x + r, y + r, x - r, y + r, sp)
            out:SetPixel(x, y, sp)
        end
    end

    -- Release the SAT once we're done with it.
    img:RecycleSAT()

    OutImage:Set(req, out)
end
'''


# ─── Internal helpers ─────────────────────────────────────────────────────────

def _slider(varname: str, label: str, default: float = 0.0,
            min_: float = -1.0, max_: float = 1.0) -> str:
    return (
        f'    {varname} = self:AddInput("{label}", "{label}", {{\n'
        f'        LINKID_DataType = "Number",\n'
        f'        INPID_InputControl = "SliderControl",\n'
        f'        INP_Default = {default},\n'
        f'        INP_MinScale = {min_},\n'
        f'        INP_MaxScale = {max_},\n'
        f'    }})\n'
    )


def _checkbox(varname: str, label: str, default: int = 0) -> str:
    return (
        f'    {varname} = self:AddInput("{label}", "{label}", {{\n'
        f'        LINKID_DataType = "Number",\n'
        f'        INPID_InputControl = "CheckboxControl",\n'
        f'        INP_Integer = true,\n'
        f'        INP_Default = {default},\n'
        f'    }})\n'
    )


# Public registry of generators.
TEMPLATES = {
    # Color & per-pixel
    "color_matrix": color_matrix,
    "per_pixel": per_pixel,
    "channel_op": channel_op,
    # Geometric / spatial
    "transform": transform,
    "spatial_warp": spatial_warp,
    # Text & shapes
    "text_overlay": text_overlay,
    "shape_generator": shape_generator,
    # Source / temporal
    "source_generator": source_generator,
    "time_displace": time_displace,
    # Filters
    "builtin_blur": builtin_blur,
    "builtin_resize": builtin_resize,
    "variable_blur": variable_blur,
    # Modifiers
    "modifier": modifier,
    "point_modifier": point_modifier,
    # Display / shaders
    "view_lut": view_lut,
    "dctl_kernel": dctl_kernel,
    # Reference / educational
    "controls_demo": controls_demo,
    "notifychanged_demo": notifychanged_demo,
}
