#!/usr/bin/env python3
"""Generate amy_api.generated.js from amy/__init__.py and amy/constants.py.

Reads the _KW_MAP_LIST and ctrl_coefs dict_fields from the Python AMY API,
plus all constants from amy/constants.py, and generates a self-contained
JS module with amy_message(), amy_send(), and an AMY constants object.
"""
import ast
import re
import textwrap
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
AMY_INIT = ROOT / "amy" / "__init__.py"
AMY_CONSTANTS = ROOT / "amy" / "constants.py"
OUTPUT = ROOT / "build" / "amy_api.generated.js"


def extract_kw_map_list(source: str):
    """Parse _KW_MAP_LIST from the Python source."""
    match = re.search(
        r"_KW_MAP_LIST\s*=\s*\[(.+?)\]\s*\n",
        source,
        re.DOTALL,
    )
    if not match:
        raise RuntimeError("Could not find _KW_MAP_LIST in AMY __init__.py")
    raw = "[" + match.group(1) + "]"
    raw = re.sub(r"#[^\n]*", "", raw)
    entries = ast.literal_eval(raw)
    return entries


def extract_coef_fields(source: str):
    """Parse the dict_fields list from parse_ctrl_coefs."""
    match = re.search(r"dict_fields\s*=\s*(\[[^\]]+\])", source)
    if not match:
        raise RuntimeError("Could not find dict_fields in AMY __init__.py")
    return ast.literal_eval(match.group(1))


def extract_constants(source: str):
    """Parse all NAME=VALUE constants from constants.py.

    Keeps only simple integer/float assignments. When a name is assigned
    multiple times (e.g. AMY_BLOCK_SIZE), the last value wins (matching
    Python's runtime behaviour with conditional re-assignments).
    """
    constants = {}
    for line in source.splitlines():
        line = line.strip()
        if not line or line.startswith('#'):
            continue
        m = re.match(r'^([A-Z][A-Z0-9_]*)\s*=\s*(.+)$', line)
        if not m:
            continue
        name, val_str = m.group(1), m.group(2).strip()
        # Skip true/false pseudo-constants
        if name in ('true', 'false'):
            continue
        try:
            val = ast.literal_eval(val_str)
        except Exception:
            continue
        if isinstance(val, (int, float)):
            constants[name] = val
    return constants


def generate_js(kw_map_list, coef_fields, constants=None):
    """Build the complete JS source."""
    map_entries = []
    priority_entries = []
    for i, (name, code) in enumerate(kw_map_list):
        wire = code[:-1]
        type_code = code[-1]
        map_entries.append(f'  {name}: {{wire: "{wire}", type: "{type_code}"}}')
        priority_entries.append(f"  {name}: {i}")

    kw_map_js = "{\n" + ",\n".join(map_entries) + "\n}"
    kw_priority_js = "{\n" + ",\n".join(priority_entries) + "\n}"
    coef_fields_js = "[" + ", ".join(f'"{f}"' for f in coef_fields) + "]"

    js = textwrap.dedent("""\
    // AUTO-GENERATED by scripts/gen_amy_js_api.py — do not edit by hand.
    // Source: amy/__init__.py
    (function() {
    "use strict";

    var AMY_KW_MAP = %KW_MAP%;

    var AMY_KW_PRIORITY = %KW_PRIORITY%;

    var AMY_COEF_FIELDS = %COEF_FIELDS%;

    // --- type handlers (mirror Python's str_of_int, trunc, parse_list_or_comma_string, parse_ctrl_coefs) ---

    function _str_of_int(arg) {
      return String(Math.trunc(Number(arg)));
    }

    function _trunc(number) {
      if (typeof number === "string") {
        if (number.trim() === "") return "";
        number = parseFloat(number);
      }
      if (typeof number === "number") {
        var s = number.toFixed(6);
        s = s.replace(/0+$/, "");
        s = s.replace(/\\.$/, "");
        return s;
      }
      return String(number);
    }

    function _parse_list(obj) {
      if (Array.isArray(obj)) {
        return obj.map(function(v) { return v == null ? "" : String(v); }).join(",");
      }
      return String(obj);
    }

    function _trim_trailing(arr) {
      var end = arr.length;
      while (end > 0 && arr[end - 1] == null) end--;
      return arr.slice(0, end);
    }

    function _parse_ctrl_coefs(coefs) {
      if (typeof coefs === "string") {
        return coefs.split(",").map(function(x) { return _trunc(x); }).join(",");
      }
      if (typeof coefs === "number") {
        return _trunc(coefs);
      }
      if (!Array.isArray(coefs) && typeof coefs === "object" && coefs !== null) {
        var list = new Array(AMY_COEF_FIELDS.length).fill(null);
        for (var key in coefs) {
          if (!coefs.hasOwnProperty(key)) continue;
          var idx = AMY_COEF_FIELDS.indexOf(key);
          if (idx < 0) throw new Error("Unknown ctrl_coef field: " + key + ". Valid: " + AMY_COEF_FIELDS.join(", "));
          list[idx] = coefs[key];
        }
        coefs = list;
      }
      if (!Array.isArray(coefs)) {
        return String(coefs);
      }
      coefs = _trim_trailing(coefs);
      return coefs.map(function(v) { return v == null ? "" : _trunc(v); }).join(",");
    }

    var _ARG_HANDLERS = {
      I: _str_of_int,
      F: _trunc,
      S: String,
      L: _parse_list,
      C: _parse_ctrl_coefs
    };

    /**
     * Build an AMY wire code string from named parameters.
     *
     *   amy_message({osc: 0, wave: 1, freq: 440})  =>  "v0w1f440Z"
     *   amy_message({synth: 1, patch: 257, num_voices: 6})  =>  "K257i1iv6Z"
     *
     * Control coefficient params (amp, freq, duty, pan, filter_freq) accept:
     *   - A number:  freq: 440
     *   - An array:  freq: [440, 1, null, null, null, null, 1]
     *   - An object: freq: {const: 440, bend: 1}
     */
    function amy_message(params) {
      if (!params || typeof params !== "object") {
        throw new Error("amy_message requires an object of parameters");
      }
      var keys = [];
      for (var key in params) {
        if (!params.hasOwnProperty(key)) continue;
        if (!(key in AMY_KW_MAP)) {
          throw new Error("Unknown AMY parameter: " + key);
        }
        var arg = params[key];
        if (arg == null) {
          if (key !== "time" && key !== "sequence") {
            throw new Error("Null value for AMY parameter: " + key);
          }
          continue;
        }
        keys.push(key);
      }
      keys.sort(function(a, b) {
        return (AMY_KW_PRIORITY[a] || 0) - (AMY_KW_PRIORITY[b] || 0);
      });
      var m = "";
      for (var i = 0; i < keys.length; i++) {
        var k = keys[i];
        var mapping = AMY_KW_MAP[k];
        var handler = _ARG_HANDLERS[mapping.type];
        m += mapping.wire + handler(params[k]);
      }
      return m + "Z";
    }

    /**
     * Build and send an AMY wire code message.
     * Equivalent to Python's amy.send().
     */
    function amy_send(params, log) {
      var msg = amy_message(params);
      if (log && typeof amy_add_log_message === "function") {
        amy_add_log_message(msg);
      } else if (typeof amy_add_message === "function") {
        amy_add_message(msg);
      } else {
        console.warn("amy_send: no AMY message handler found (is amy.js loaded?)");
      }
      return msg;
    }

    // Constants from amy/constants.py (mirrors amy.SINE, amy.FILTER_LPF, etc.)
    var AMY = %CONSTANTS%;

    if (typeof globalThis !== "undefined") {
      globalThis.amy_message = amy_message;
      globalThis.amy_send = amy_send;
      globalThis.AMY = AMY;
      globalThis.AMY_KW_MAP = AMY_KW_MAP;
      globalThis.AMY_COEF_FIELDS = AMY_COEF_FIELDS;
    }

    })();
    """)

    # Build constants object
    if constants:
        const_entries = []
        for name, val in constants.items():
            if isinstance(val, float):
                const_entries.append(f"  {name}: {val}")
            else:
                const_entries.append(f"  {name}: {val}")
        constants_js = "{\n" + ",\n".join(const_entries) + "\n}"
    else:
        constants_js = "{}"

    js = js.replace("%KW_MAP%", kw_map_js)
    js = js.replace("%KW_PRIORITY%", kw_priority_js)
    js = js.replace("%COEF_FIELDS%", coef_fields_js)
    js = js.replace("%CONSTANTS%", constants_js)
    return js


def main():
    source = AMY_INIT.read_text()
    kw_map_list = extract_kw_map_list(source)
    coef_fields = extract_coef_fields(source)
    constants = extract_constants(AMY_CONSTANTS.read_text())
    js = generate_js(kw_map_list, coef_fields, constants)
    OUTPUT.write_text(js)
    print(f"Generated {OUTPUT.relative_to(ROOT)} ({len(kw_map_list)} parameters, {len(coef_fields)} coef fields, {len(constants)} constants)")


if __name__ == "__main__":
    main()
