"""midi.py — the MUSIC surface projector: geometry -> symbolic music (MIDI).

The first NON-TEXT surface, and the proof of the general shape. "Music is
language and it is math" (Will): symbolic music is tractable and geometry-native,
so it is the natural efferent twin to try first after text.

CRUCIALLY this projector does NOT read the realized sentences. It reads the IR's
GEOMETRY facet — ``block.provenance`` — and DECODES each edge onto a musical
surface. That is the whole thesis of the multimodal projector: the same
geometry-carrying IR drives text AND music; a text projector reads the words, a
music projector reads the meaning-geometry. The mapping is deterministic and
faithful to the geometry's structure:

    relation lemma  -> scale degree      (same relation -> same pitch class;
                                          meaning has a consistent sonic form)
    polarity        -> mode              (aff = major third above; neg = minor
                                          third / lowered — SACRED polarity is
                                          audible, a negated edge sounds negated)
    confidence      -> note duration     (stronger grounding rings longer)
    importance      -> velocity          (more important source = louder)
    section         -> phrase + register shift (structure becomes musical form)

Own-the-core: a Standard MIDI File is a header chunk + a track chunk of
delta-timed events. We emit the raw bytes with ``struct`` — no external MIDI
library. Format 0, one track.
"""
from __future__ import annotations

import io
import os
import struct
import sys

sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from document_ir import DocumentIR, Provenance  # noqa: E402
from projectors.base import TwoStageProjector, register  # noqa: E402

_TICKS = 480                      # ticks per quarter note
_C_MAJOR = [0, 2, 4, 5, 7, 9, 11]  # semitone offsets of a diatonic scale


def _vlq(n: int) -> bytes:
    """MIDI variable-length quantity encoding of a delta time."""
    if n == 0:
        return b"\x00"
    out = bytearray()
    out.append(n & 0x7F)
    n >>= 7
    while n:
        out.insert(0, (n & 0x7F) | 0x80)
        n >>= 7
    return bytes(out)


def _degree_for(relation: str) -> int:
    """Stable scale degree for a relation lemma (same relation -> same pitch)."""
    if not relation:
        return 0
    return sum(ord(c) for c in relation.lower()) % len(_C_MAJOR)


def _note_for(p: Provenance, base: int) -> tuple[int, int, int]:
    """(pitch, velocity, duration_ticks) for one geometry edge."""
    root = base + _C_MAJOR[_degree_for(p.relation)]
    # polarity -> mode: affirmed edges take the bright major third, negated edges
    # take the darker minor third. The negation is AUDIBLE and never dropped.
    third = 4 if p.polarity == "aff" else 3
    pitch = max(24, min(96, root + (third if p.confidence >= 0.5 else 0)))
    velocity = int(56 + 60 * min(1.0, max(0.0, p.importance)))
    velocity = max(40, min(120, velocity))
    # confidence -> duration: quarter .. dotted-half
    dur = int(_TICKS * (0.5 + 1.5 * min(1.0, max(0.0, p.confidence))))
    return pitch, velocity, dur


# a mode-profile: the pluggable musical knob (the peer's mode_profile). Scale +
# tempo. Swapping this profile re-voices the SAME geometry — surface as parameter.
_DEFAULT_PROFILE = {"scale": _C_MAJOR, "tempo_us": 500000,
                    "registers": [60, 55, 64, 50, 67, 48], "program": 0}


class MidiProjector(TwoStageProjector):
    """geometry -> symbolic music, in the shared two-stage shape.

    ``plan(frame)``  -> a music_spec: an ordered list of note dicts derived
                        deterministically from the frame's provenance geometry
                        (the peer's ``plan(frame, profile) -> spec``).
    ``realize(spec)`` -> Standard MIDI File bytes (the peer's
                        ``realize(spec, profile) -> surface``; here the surface
                        is symbolic MIDI, the minimal audio proof — a richer
                        additive-synth audio projector conforms identically).
    """

    surface = "midi"
    media_type = "audio/midi"
    ext = "mid"
    modality = "audio"

    def __init__(self, profile: dict | None = None):
        self.profile = profile or _DEFAULT_PROFILE

    # -- stage 1: meaning-geometry -> music_spec (reads the GEOMETRY facet) -- #
    def plan(self, doc: DocumentIR) -> list[dict]:
        registers = self.profile["registers"]
        spec: list[dict] = []
        for si, sec in enumerate(doc.sections):
            base = registers[si % len(registers)]
            provs = [p for p in sec.all_provenance()
                     if p.kind in ("fact", "interpretation")]
            for i, p in enumerate(provs):
                pitch, vel, dur = _note_for(p, base)
                spec.append({"pitch": pitch, "velocity": vel, "dur": dur,
                             "rest_before": (_TICKS // 2) if (si > 0 and i == 0) else 0,
                             "relation": p.relation, "polarity": p.polarity})
        return spec

    # -- stage 2: music_spec -> MIDI bytes (own-core, no library) ------------ #
    def realize(self, spec: list[dict]) -> bytes:
        ev = bytearray()
        ev += _vlq(0) + b"\xFF\x51\x03" + struct.pack(">I", self.profile["tempo_us"])[1:]
        ev += _vlq(0) + bytes([0xC0, self.profile["program"] & 0x7F])
        for note in spec:
            ev += _vlq(note["rest_before"]) + bytes([0x90, note["pitch"], note["velocity"]])
            ev += _vlq(note["dur"]) + bytes([0x80, note["pitch"], 0])
        ev += _vlq(0) + b"\xFF\x2F\x00"
        track = bytes(ev)
        buf = io.BytesIO()
        buf.write(b"MThd" + struct.pack(">IHHH", 6, 0, 1, _TICKS))
        buf.write(b"MTrk" + struct.pack(">I", len(track)) + track)
        return buf.getvalue()


register(MidiProjector())
