'use babel';

// Tidal keyword sets used to resolve what each "^NN" CC reference binds to.
// Order in the union matters: longer/more-specific names should match first,
// so we scan effects-bus before plain effects.

export const EFFECTS_BUS = [
  'djfbus', 'hpfbus', 'lpfbus', 'bpfbus',
  'octerbus', 'roombus', 'shapebus', 'crushbus',
  'delaybus', 'delaytimebus', 'delayfbbus'
];

export const EFFECTS = [
  'gain', 'pan', 'lpf', 'hpf', 'bpf', 'crush', 'shape',
  'room', 'sz', 'dry', 'octer',
  'delay', 'delaytime', 'delayfb',
  'speed', 'cut', 'legato', 'attack', 'release', 'sustain'
];

export const PATTERN_MODS = [
  'ply', 'slow', 'fast', 'chop', 'slice', 'loopAt', 'iter',
  'off', 'every', 'whenmod', 'range', 'struct', 'mask',
  'rev', 'jux', 'juxBy', 'superimpose', 'stut', 'echo'
];

export const TRIGGERS = [
  'midiOn', 'midiOff', 'sometimesBy', 'someCyclesBy',
  'sometimes', 'rarely', 'often', 'always'
];

export const NOTES = ['note', 'n', 'up'];

// File-level let-bindings the user defines at the top of every track.
// When a "^NN" follows one of these directly inside a `let X = …` line,
// the binding belongs to the GROUPS section, not to any single orbit.
export const GROUPS = [
  'gMask', 'gMute1', 'gMute2', 'gMute3',
  'gM1', 'gM2', 'gM3', 'gF1', 'gF2', 'gF3'
];

// Generic identifier matcher — catches ANY word-like token so user-defined
// helpers (midiG, leslie, lsize, modIndex, gO, …) get a sensible label
// without us having to maintain an exhaustive whitelist.
const ID_RE = /\b([a-zA-Z_][a-zA-Z0-9_]*)\b/g;

// Identifiers that are Haskell/Tidal syntax, not Tidal effect names. When
// they appear as the "nearest" word before a "^NN", we skip them and use
// the next-nearest meaningful identifier instead.
const SKIP_IDS = new Set(['let', 'in', 'do', 'where', 'if', 'then', 'else', 'of', 'fix']);

// Triggers: identifiers where the CC IS the button (state toggle or
// probability source). The "^NN" is directly associated with this id —
// don't backtrack past it to the section head.
//   midiOn "^42" ...                 → keep midiOn (→ on)
//   midiOff "^57" (midiOn "^89" ...) → cc57 stays midiOff, cc89 stays midiOn
//   sometimesBy "^56" (...)          → keep sometimesBy (→ some)
const TRIGGER_IDS = new Set([
  'midiOn', 'midiOff',
  'sometimesBy', 'someCyclesBy',
  'sometimes', 'someCycles',
  'juxBy'
]);

// Given the substring of a line BEFORE the "^NN" occurrence, return the
// nearest enclosing identifier (last match that isn't a skip word), or null.
export function nearestKeywordBefore(prefix) {
  let last = null;
  let m;
  ID_RE.lastIndex = 0;
  while ((m = ID_RE.exec(prefix)) !== null) {
    if (!SKIP_IDS.has(m[1])) last = m[1];
  }
  return last;
}

// All identifier matches in order. Used by the range-backtrack rule.
function allKeywordsBefore(prefix) {
  const out = [];
  let m;
  ID_RE.lastIndex = 0;
  while ((m = ID_RE.exec(prefix)) !== null) {
    if (!SKIP_IDS.has(m[1])) out.push({ keyword: m[1], index: m.index });
  }
  return out;
}

// Resolve the effect label for a "^NN" found at ccIndex on `line`.
// Smarter than classifyBinding: handles the `# <effect>[bus N] (range … "^NN")`
// idiom by stepping back from `range` to the enclosing #/$ head.
// Resolve the effect label for a "^NN" found at ccIndex on `line`.
//
// The Tidal idiom is `# <effect>[bus N] (<wrapper> … "^NN")` or
// `$ <trigger> "^NN" (...)`. In both shapes, the meaningful effect is the
// FIRST identifier after the nearest #/$ section head — regardless of
// what wrapping the value passes through (range, slow, fast, a custom
// helper, etc.). That single rule subsumes the old range-only backtrack.
//
// Examples handled:
//   # crush 0.5 "^22"                → crush
//   # crush (range 16 4 "^53")       → crush
//   # octerbus 72 (range 0 1 "^35")  → octer
//   # lsize (range 0 8 "^16")        → lsize
//   # foo (someWrapper "^22")        → foo
//   # n "^29"                        → n
//   $ midiOn "^42" (...)             → on (or 'mute' if line has mask "f*N")
//   $ sometimesBy "^56" (...)        → some
export function classifyEffectForCC(line, ccIndex) {
  const prefix = line.slice(0, ccIndex);

  // Step 1: find the identifier IMMEDIATELY preceding the "^NN".
  const immediate = nearestKeywordBefore(prefix);

  // Step 2: if it's a trigger (the CC IS the button/probability source),
  // keep it as-is — don't backtrack past it.
  //   midiOff "^57" (midiOn "^89" ...) → cc89 stays midiOn, cc57 stays midiOff
  if (immediate && TRIGGER_IDS.has(immediate)) {
    return classifyBinding(immediate, line);
  }

  // Step 3: otherwise the CC is being shaped (range, slow, fast, lsize,
  // someUserHelper, …). The effect we care about is the first identifier
  // after the nearest #/$ section head.
  //   # foo (range 0.2 0.8 "^22")     → foo
  //   # crush (someWrapper "^22")     → crush
  //   # octerbus N (range … "^35")    → octer
  const hashIdx   = prefix.lastIndexOf('#');
  const dollarIdx = prefix.lastIndexOf('$');
  const sepIdx = Math.max(hashIdx, dollarIdx);
  if (sepIdx >= 0) {
    const rest = prefix.slice(sepIdx + 1);
    const idm = rest.match(/([a-zA-Z_][a-zA-Z0-9_]*)/);
    if (idm && !SKIP_IDS.has(idm[1])) {
      return classifyBinding(idm[1], line);
    }
  }

  // Final fallback: just use the immediate identifier (covers
  // `(midiOn "^X" ...)` deep inside a let body with no visible #/$).
  return classifyBinding(immediate, line);
}

// Short, perf-readable effect labels — we trade verbosity for at-a-glance
// scannability during livecoding. Verbose names still appear in tooltips.
const SHORT_LABELS = {
  midiOn:       'on',
  midiOff:      'off',
  sometimesBy:  'some',
  someCyclesBy: 'cyc',
  sometimes:    'some',
  someCycles:   'cyc',
  superimpose:  'sup',
  loopAt:       'loop',
  whenmod:      'when',
  delaytime:    'dt',
  delayfb:      'dfb',
  attack:       'att',
  release:      'rel',
  sustain:      'sus'
};

// Map the matched keyword onto a short human-readable effect label.
// Strips "bus" suffixes; classifies midiOn-with-mask as 'mute'.
export function classifyBinding(keyword, lineText) {
  if (!keyword) return '?';
  if (keyword.endsWith('bus')) return keyword.slice(0, -3);
  if ((keyword === 'midiOn' || keyword === 'midiOff') && /mask\s+"f\*\d+"/.test(lineText)) {
    return 'mute';
  }
  return SHORT_LABELS[keyword] || keyword;
}
