"""AV-Live TouchOSC layout generator.

Builds a three-page PAGER layout (LIVE / FX+HARM+CONCERT / SEQ) for the
AV-Live SuperCollider engine on macm1 (control port 57121).

All OSC addresses and arg types follow the authoritative mapping in:
  docs/superpowers/plans/2026-06-28-touchosc-control-surface.md (brief override).
"""
import os

from gen import schema as s

# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------

CANVAS = (1194, 834)

PATTERNS = [
    "kick", "hats", "clap", "perc",
    "sub",  "acid", "arp",  "lead",
    "stab", "pad",  "ride", "rim",
    "tom",  "reese", "bells", "sweep",
]

SCALES = ["minor", "dorian", "phrygian", "penta"]

_LUA_DIR = os.path.join(os.path.dirname(__file__), "lua")


def _lua(name):
    """Load a Lua script by filename from the lua/ directory adjacent to this module."""
    with open(os.path.join(_LUA_DIR, name), encoding="utf-8") as fh:
        return fh.read()


# ---------------------------------------------------------------------------
# Primitive widget builders
# ---------------------------------------------------------------------------

def label(text, x, y, w, h):
    """A read-only LABEL node."""
    return s.node("LABEL", [s.prop("name", text, "s"), s.frame(x, y, w, h)])


def button(lbl, address, x, y, w, h, args=None):
    """A momentary/toggle BUTTON.  ``args`` is a list of Partial objects."""
    return s.node(
        "BUTTON",
        [s.prop("name", lbl, "s"), s.frame(x, y, w, h)],
        messages=[s.osc(address, args or [])],
    )


def vfader(address, args, x, y, w, h, recv_address=None, recv_args=None):
    """A vertical FADER.  Optional receive-only message for feedback."""
    msgs = [s.osc(address, args)]
    if recv_address is not None:
        msgs.append(s.osc(recv_address, recv_args or [s.val()],
                          send=0, receive=1, feedback=1))
    return s.node(
        "FADER",
        [s.prop("name", "", "s"), s.frame(x, y, w, h)],
        messages=msgs,
    )


# ---------------------------------------------------------------------------
# Pad cell (page 1)
# ---------------------------------------------------------------------------

def _pad_cell(name, x, y, w, h):
    """One pad cell: arm BUTTON + vol FADER + name LABEL, inside a GROUP.

    The GROUP carries pad.lua which reflects /armed/<name> back as color.
    The arm BUTTON sends /launch [const(name), vali()].
    The vol FADER sends /launch/vol [const(name), valf(0,1.5)].
    """
    btn_h = h - 26
    arm_btn = s.node(
        "BUTTON",
        [s.prop("name", name, "s"), s.frame(0, 0, w - 20, btn_h)],
        messages=[
            s.osc("/launch", [s.const(name), s.vali()]),
        ],
    )
    vol_fader = s.node(
        "FADER",
        [s.prop("name", f"vol_{name}", "s"), s.frame(w - 18, 0, 16, btn_h)],
        messages=[s.osc("/launch/vol", [s.const(name), s.valf(0, 1.5)])],
    )
    name_label = label(name, 0, btn_h + 2, w, 22)
    return s.node(
        "GROUP",
        [
            s.prop("name", f"pad_{name}", "s"),
            s.frame(x, y, w, h),
            s.script(_lua("pad.lua")),
        ],
        children=[arm_btn, vol_fader, name_label],
        messages=[s.osc(f"/armed/{name}", [s.val()], send=0, receive=1, feedback=1)],
    )


# ---------------------------------------------------------------------------
# Page 1 — LIVE
# ---------------------------------------------------------------------------

def live_page():
    """Page 1: top bar (tempo, clear, VU, beat) + 4x4 pad grid."""
    page = s.node("GROUP", [s.prop("name", "LIVE", "s"), s.frame(0, 0, *CANVAS)])
    ch = page.find("children")

    # Top bar ----------------------------------------------------------------
    # Tempo fader: value 0..1 -> BPM 60..200
    ch.append(s.node(
        "FADER",
        [s.prop("name", "tempo", "s"), s.frame(10, 8, 300, 50)],
        messages=[s.osc("/launch/tempo", [s.valf(60, 200)])],
    ))
    ch.append(label("TEMPO", 10, 60, 100, 20))

    # Clear all
    ch.append(button("CLEAR ALL", "/launch/clear", 322, 8, 150, 50, args=[]))

    # Master VU fader (receive-only /sync/rms)
    ch.append(s.node(
        "FADER",
        [s.prop("name", "rms", "s"), s.frame(490, 8, 40, 72)],
        messages=[s.osc("/sync/rms", [s.val()], send=0, receive=1, feedback=1)],
    ))

    # Beat flash box (receive-only /sync/beat)
    ch.append(s.node(
        "BOX",
        [s.prop("name", "beat", "s"), s.frame(542, 8, 72, 72),
         s.script(_lua("feedback.lua"))],
        messages=[s.osc("/sync/beat", [s.val()], send=0, receive=1, feedback=1)],
    ))

    # Quantize selector row (send-only momentary): 1/2 beat / 1 beat / 1 bar / 2 bars
    quant_defs = [("1/2", 0.5), ("1", 1), ("1 BAR", 4), ("2 BAR", 8)]
    qx_start, qy, qw, qh, qgap = 625, 8, 130, 50, 8
    for i, (lbl, beats) in enumerate(quant_defs):
        qx = qx_start + i * (qw + qgap)
        ch.append(button(lbl, "/launch/quant", qx, qy, qw, qh, args=[s.constf(beats)]))
    ch.append(label("QUANT", qx_start, 62, 120, 20))

    # 4x4 pad grid -----------------------------------------------------------
    gx, gy, pw, ph, gap = 10, 96, 283, 178, 10
    for i, name in enumerate(PATTERNS):
        col, row = i % 4, i // 4
        x = gx + col * (pw + gap)
        y = gy + row * (ph + gap)
        ch.append(_pad_cell(name, x, y, pw, ph))

    return page


# ---------------------------------------------------------------------------
# Page 2 — FX / HARMONIE / CONCERT
# ---------------------------------------------------------------------------

def fx_page():
    """Page 2: FX column, harmony column, concert column."""
    page = s.node(
        "GROUP",
        [s.prop("name", "FX-HARM-CONCERT", "s"), s.frame(0, 0, *CANVAS)],
    )
    ch = page.find("children")

    # FX column (x=10..440) --------------------------------------------------
    ch.append(label("FILTER", 10, 8, 80, 22))
    ch.append(s.node(
        "FADER",
        [s.prop("name", "filter", "s"), s.frame(10, 36, 80, 350)],
        messages=[s.osc("/control/fx/filter", [s.valf(0, 1)])],
    ))

    # One-shot buttons (vali() — engine ignores arg, just triggers)
    one_shots = [("STUTTER", "stutter"), ("CRASH", "crash"),
                 ("KICK",    "kick"),    ("SWELL",  "swell")]
    for i, (lbl, key) in enumerate(one_shots):
        ch.append(button(lbl, f"/control/fx/{key}",
                         100, 36 + i * 70, 160, 56, args=[s.vali()]))

    ch.append(button("BREAKDOWN IN",  "/control/fx/breakdown",
                     100, 316, 160, 56, args=[s.consti(1)]))
    ch.append(button("BREAKDOWN OUT", "/control/fx/breakdown",
                     100, 382, 160, 56, args=[s.consti(0)]))

    # Harmony column (x=290..780) --------------------------------------------
    ch.append(label("HARMONIE", 290, 8, 200, 22))

    ch.append(button("ROOT -", "/control/harmony/root",
                     290, 36, 145, 52, args=[s.consti(-2)]))
    ch.append(button("ROOT +", "/control/harmony/root",
                     445, 36, 145, 52, args=[s.consti(2)]))

    # Scale buttons: 2x2, each sends the string name (NOT index)
    scale_xy = [(290, 100), (445, 100), (290, 162), (445, 162)]
    for (sx, sy), scale_name in zip(scale_xy, SCALES):
        ch.append(button(scale_name.upper(), "/control/harmony/scale",
                         sx, sy, 145, 52, args=[s.const(scale_name)]))

    # Octave: three absolute buttons (0, 1, 2)
    for n in range(3):
        ch.append(button(f"OCT {n}", "/control/harmony/octave",
                         290 + n * 100, 234, 90, 52, args=[s.consti(n)]))

    # Phrase: four absolute buttons (0..3)
    for p in range(4):
        ch.append(button(f"PH {p}", "/control/harmony/phrase",
                         290 + p * 75, 306, 65, 52, args=[s.consti(p)]))

    # Concert column (x=620..900) --------------------------------------------
    ch.append(label("CONCERT", 620, 8, 200, 22))

    ch.append(button("MORCEAU >", "/control/concertNext",
                     620, 36, 260, 64, args=[]))
    ch.append(button("CONCERT", "/control/doScene",
                     620, 110, 260, 64, args=[s.const("concert")]))
    # body-play is /control/doScene "play" — NOT /control/bodyPlay
    ch.append(button("BODY-PLAY", "/control/doScene",
                     620, 184, 260, 64, args=[s.const("play")]))

    return page


# ---------------------------------------------------------------------------
# Page 3 — SEQUENCEURS
# ---------------------------------------------------------------------------

def seq_page():
    """Page 3: rhythm grid + melody faders + preset selectors + arm buttons.

    Per-voice VU strip deliberately omitted: the engine sums all voices to
    bus 0; per-voice live levels are not producible.  Master RMS VU on page 1
    is the only meter.  Do not add /sync/amp receivers here.
    """
    page = s.node(
        "GROUP",
        [
            s.prop("name", "SEQ", "s"),
            s.frame(0, 0, *CANVAS),
            s.script(_lua("preset_select.lua")),
        ],
        messages=[
            s.osc("/seq/rhythm/state", [s.val()], send=0, receive=1, feedback=1),
            s.osc("/seq/melody/state", [s.val()], send=0, receive=1, feedback=1),
        ],
    )
    ch = page.find("children")

    # Rhythm section (x=10..595) ---------------------------------------------
    ch.append(label("RYTHME", 10, 8, 120, 22))

    step = 74  # button width + gap
    for i in range(8):
        bx = 10 + i * step
        ch.append(button(f"R{i + 1}", "/seq/rhythm",
                         bx, 36, 66, 44, args=[s.consti(i)]))
        # Name label updated by preset_select.lua via /seq/rhythm/state
        nl = s.node(
            "LABEL",
            [s.prop("name", f"R{i + 1}", "s"), s.frame(bx, 86, 66, 22)],
        )
        ch.append(nl)

    # 3x16 rhythm grid
    rhythm_grid = s.node(
        "GRID",
        [
            s.prop("name", "rhythmgrid", "s"),
            s.frame(10, 114, 580, 162),
            s.prop("gridX", "16", "s"),
            s.prop("gridY", "3", "s"),
            s.script(_lua("rhythm_grid.lua")),
        ],
        messages=[
            # Nominal address for testability; Lua emits the real OSC.
            s.osc("/seq/rhythm/set", [], send=0, receive=0),
            s.osc("/seq/rhythm/state", [s.val()], send=0, receive=1, feedback=1),
        ],
    )
    ch.append(rhythm_grid)

    # Melody section (x=600..1185) -------------------------------------------
    ch.append(label("MELODIE", 600, 8, 120, 22))

    mstep = 74  # fader width + gap
    for i in range(8):
        fx = 600 + i * mstep
        ch.append(button(f"M{i + 1}", "/seq/melody",
                         fx, 36, 66, 44, args=[s.consti(i)]))
        # Melody step fader (melody_faders.lua emits /seq/melody/set)
        mel_fader = s.node(
            "FADER",
            [
                s.prop("name", f"m{i}", "s"),
                s.frame(fx, 86, 66, 200),
                s.script(_lua("melody_faders.lua")),
            ],
            messages=[
                # Nominal address so tests can find these 8 faders by address.
                s.osc("/seq/melody/set", [], send=0, receive=0),
            ],
        )
        ch.append(mel_fader)

    # ARM melody + rhythm buttons (below faders, y=296)
    ch.append(button("ARM MEL", "/launch",
                     600, 296, 130, 44, args=[s.const("melseq"), s.vali()]))
    ch.append(button("ARM RHY", "/launch",
                     738, 296, 130, 44, args=[s.const("rhythmseq"), s.vali()]))

    return page


# ---------------------------------------------------------------------------
# Page 4 — SECTIONS
# ---------------------------------------------------------------------------

def sections_page():
    """Page 4: 8 section slots (launch + save), prev/next nav, SC scene feedback.

    The GROUP carries sections.lua and a /scene/state receive message so that
    onReceiveOSC fires on the GROUP and can color the slot buttons.
    """
    page = s.node(
        "GROUP",
        [
            s.prop("name", "SECTIONS", "s"),
            s.frame(0, 0, *CANVAS),
            s.script(_lua("sections.lua")),
        ],
        messages=[
            s.osc("/scene/state", [s.val()], send=0, receive=1, feedback=1),
        ],
    )
    ch = page.find("children")

    ch.append(label("SECTIONS", 10, 8, 200, 30))

    gx = 10
    bw = 138
    gap = 10
    step = bw + gap          # 148 px per column

    launch_y, launch_h = 46, 140
    save_y, save_h = 196, 60

    for i in range(8):
        x = gx + i * step
        # Launch button: name=slot{i} so sections.lua can findByName("slot"..i)
        ch.append(s.node(
            "BUTTON",
            [s.prop("name", f"slot{i}", "s"), s.frame(x, launch_y, bw, launch_h)],
            messages=[s.osc("/scene/launch", [s.consti(i)])],
        ))
        # Save button
        ch.append(s.node(
            "BUTTON",
            [s.prop("name", f"S{i + 1}", "s"), s.frame(x, save_y, bw, save_h)],
            messages=[s.osc("/scene/save", [s.consti(i)])],
        ))

    # Nav buttons
    nav_y, nav_h = 272, 80
    ch.append(button("< PREV", "/scene/prev", 10, nav_y, 200, nav_h, args=[s.vali()]))
    ch.append(button("NEXT >", "/scene/next", 220, nav_y, 200, nav_h, args=[s.vali()]))

    return page


# ---------------------------------------------------------------------------
# Pager + root
# ---------------------------------------------------------------------------

def pager(pages):
    """Wrap a list of page GROUP nodes in a PAGER."""
    return s.node(
        "PAGER",
        [s.prop("name", "pager", "s"), s.frame(0, 0, *CANVAS)],
        children=pages,
    )


def build():
    """Assemble and return the full root node (called by build_layout.py)."""
    root = s.node(
        "GROUP",
        [s.prop("name", "av-live-control", "s"), s.frame(0, 0, *CANVAS)],
        children=[pager([live_page(), fx_page(), seq_page(), sections_page()])],
    )
    return root
