/**
 * The rack, written out as a `.tidal` block you can paste into a live file.
 *
 * This is the only reason the sequencer earns its place: clicking cells is a fast way
 * to find out whether four loops belong together, but the answer is worthless unless
 * it survives the trip to the rig. So the export is not a pretty-printer — it emits
 * ParVagues' own idioms, on the orbits the kit's family maps to, with the mask
 * pattern the cells describe, so the block plays the same thing the browser did.
 *
 * `loopAt` is the hinge: the deck plays every bar-loop at `dur / (bars * barlen)`,
 * which is what `loopAt bars` does, so the same two numbers appear in both places.
 */
import { PHRASE_BARS, CHOP_STEPS, type Voice } from './engine'

/** Which `dN` a family belongs on, following the channel convention in `live/`. */
export const FAMILY_ORBIT: Record<string, number> = {
  drums: 1, hits: 2, bass: 3, tonal: 5, vox: 6, fx: 8,
}

/** `mask "t f t t"` for a bar-loop row; `"t ~ f ~"`-style for a chop row.
 *  Returns null when every cell is on — a mask of all `t` is noise in the source. */
export function maskOf(cells: boolean[], stride: number, len: number): string | null {
  const used: boolean[] = []
  for (let i = 0; i < len; i += stride) used.push(!!cells[i])
  if (used.every(Boolean)) return null
  return used.map((b) => (b ? 't' : 'f')).join(' ')
}

/** A chop row's 16 steps as a Tidal structure string: `"x ~ ~ x ~ ~ x ~"`. */
export function stepsOf(cells: boolean[]): string {
  return Array.from({ length: CHOP_STEPS }, (_, i) => (cells[i] ? 'x' : '~')).join(' ')
}

export type ExportRow = { voice: Voice; kit: string; n: number; family?: string | null }

export function toTidal(rows: ExportRow[], bpm: number, opts?: { swing?: number }): string {
  const cps = bpm / 60 / 4
  const out: string[] = [
    `-- from the kit auditioner · ${bpm} bpm`,
    `setcps (${cps.toFixed(4)})   -- ${bpm} bpm`,
    '',
  ]
  const usedOrbits = new Set<number>()
  const nextOrbit = (family?: string | null) => {
    let d = FAMILY_ORBIT[family ?? ''] ?? 4
    while (usedOrbits.has(d)) d = (d % 12) + 1
    usedOrbits.add(d)
    return d
  }

  for (const r of rows) {
    if (r.voice.muted) continue
    const d = nextOrbit(r.family)
    // The mixer fader is linear; Tidal's `gain` is quartic (amp = gain^4), so a fader
    // at 0.5 is `gain (0.5 ** 0.25)` = 0.84, not `gain 0.5`. Getting this backwards is
    // a 12 dB error and the reason the rack's fader is not just copied across.
    const gain = r.voice.gain === 1 ? '' : ` # gain ${Math.pow(r.voice.gain, 0.25).toFixed(2)}`
    if (r.voice.bars > 0) {
      const m = maskOf(r.voice.cells, r.voice.bars, PHRASE_BARS)
      const mask = m ? `mask "${m}" $ ` : ''
      out.push(`d${d} $ ${mask}loopAt ${r.voice.bars} $ s "${r.kit}" # n ${r.n}${gain}`
        + `   -- ${r.voice.id.split('/')[1]}`)
    } else {
      const st = stepsOf(r.voice.cells)
      out.push(`d${d} $ s "${st}" # s "${r.kit}" # n ${r.n}${gain}`
        + `   -- chop, ${r.voice.durS.toFixed(2)}s`)
    }
  }
  if (opts?.swing) out.push('', `-- swing ${(opts.swing * 100).toFixed(0)}% — nudge by ear, `
    + `Tidal has no direct equivalent (try \`(# nudge 0.01)\` on the odd steps)`)
  out.push('', 'hush')
  return out.join('\n')
}

/** The same rack as a Strudel one-liner, for the browser playground (strudel.cc) or
 *  a `@strudel/web` pane. Emitted as text: the deck below already plays this rack, so
 *  a second audio graph would only add a way for the two to disagree. */
export function toStrudel(rows: ExportRow[], bpm: number): string {
  const live = rows.filter((r) => !r.voice.muted)
  if (!live.length) return 'silence'
  const parts = live.map((r) => {
    const g = r.voice.gain === 1 ? '' : `.gain(${r.voice.gain.toFixed(2)})`
    if (r.voice.bars > 0) {
      const m = maskOf(r.voice.cells, r.voice.bars, PHRASE_BARS)
      const mask = m ? `.mask("<${m.replace(/ /g, ' ')}>")` : ''
      return `s("${r.kit}").n(${r.n}).loopAt(${r.voice.bars})${mask}${g}`
    }
    return `s("${stepsOf(r.voice.cells).replace(/x/g, r.kit)}").n(${r.n})${g}`
  })
  return `setcps(${(bpm / 60 / 4).toFixed(4)})\nstack(\n  ${parts.join(',\n  ')}\n)`
}
