import { mkdirSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';

const sampleRate = 44100;
const channels = 2;
const outputDir = 'src/assets/audio/bgm';

mkdirSync(outputDir, { recursive: true });

const tracks = [
  {
    name: 'title-theme',
    root: 50,
    tempo: 74,
    bars: 12,
    mood: 'open',
    progression: [
      [0, 7, 12],
      [5, 9, 14],
      [2, 7, 11],
      [-3, 4, 9]
    ],
    melody: [12, 16, 19, 21, 19, 16, 14, 12],
    harp: [0, 7, 12, 19, 12, 7, 5, 12]
  },
  {
    name: 'story-dark',
    root: 45,
    tempo: 62,
    bars: 10,
    mood: 'quiet',
    progression: [
      [0, 7, 10],
      [-5, 2, 7],
      [-3, 4, 9],
      [-7, 0, 7]
    ],
    melody: [10, 7, 5, 3, 5, 7],
    harp: [0, 7, 10, 14, 10, 7]
  },
  {
    name: 'oath-theme',
    root: 52,
    tempo: 68,
    bars: 12,
    mood: 'warm',
    progression: [
      [0, 7, 12],
      [4, 9, 16],
      [5, 12, 17],
      [2, 9, 14]
    ],
    melody: [12, 16, 19, 21, 24, 21, 19, 16],
    harp: [0, 7, 12, 16, 19, 16, 12, 7]
  },
  {
    name: 'militia-theme',
    root: 48,
    tempo: 84,
    bars: 12,
    mood: 'march',
    progression: [
      [0, 7, 12],
      [5, 12, 17],
      [7, 14, 19],
      [2, 9, 14]
    ],
    melody: [12, 14, 16, 19, 16, 14, 12, 14],
    harp: [0, 7, 12, 7, 5, 12, 7, 12]
  },
  {
    name: 'battle-prep',
    root: 43,
    tempo: 78,
    bars: 12,
    mood: 'tense',
    progression: [
      [0, 7, 10],
      [2, 9, 14],
      [-2, 5, 10],
      [-5, 2, 7]
    ],
    melody: [12, 15, 17, 19, 17, 15, 12, 10],
    harp: [0, 7, 10, 15, 10, 7, 2, 10]
  }
];

for (const track of tracks) {
  const buffer = buildTrack(track);
  removeLowRumble(buffer, 72);
  applySmallRoom(buffer);
  normalize(buffer, 0.34);
  applySoftLimiter(buffer);
  smoothEdges(buffer, Math.floor(sampleRate * 1.6));

  const fileName = join(outputDir, `${track.name}.wav`);
  writeFileSync(fileName, encodeWav(buffer));
  console.log(`Generated ${fileName}`);
}

function buildTrack(track) {
  const beat = 60 / track.tempo;
  const bar = beat * 4;
  const duration = bar * track.bars;
  const buffer = createBuffer(duration);

  for (let barIndex = 0; barIndex < track.bars; barIndex += 1) {
    const start = barIndex * bar;
    const chord = track.progression[barIndex % track.progression.length];
    const lift = 0.86 + Math.sin((barIndex / Math.max(1, track.bars - 1)) * Math.PI) * 0.14;

    addStringBed(buffer, start, bar * 0.96, track.root, chord, lift, track.mood);
    addCelloPulse(buffer, start, beat, track.root + chord[0], track.mood);
    addHarpFigure(buffer, start, beat, track.root, chord, track.harp, track.mood);

    if (track.mood === 'march' || track.mood === 'tense') {
      addSoftDrum(buffer, start, beat, track.mood);
    }
  }

  addLeadLine(buffer, track, beat, bar);
  return buffer;
}

function createBuffer(duration) {
  const length = Math.ceil(duration * sampleRate);
  return {
    left: new Float32Array(length),
    right: new Float32Array(length)
  };
}

function addStringBed(buffer, start, duration, root, chord, lift, mood) {
  const baseGain = mood === 'quiet' ? 0.019 : mood === 'tense' ? 0.021 : 0.026;
  const notes = [
    { midi: root + chord[0] + 12, pan: -0.22, gain: baseGain * 0.72 },
    { midi: root + chord[1] + 12, pan: 0.24, gain: baseGain * 0.64 },
    { midi: root + chord[2] + 12, pan: -0.04, gain: baseGain * 0.56 },
    { midi: root + chord[1] + 24, pan: 0.18, gain: baseGain * 0.28 }
  ];

  notes.forEach((note, index) => {
    addTone(buffer, {
      start: start + index * 0.045,
      duration,
      frequency: midiToFrequency(note.midi),
      gain: note.gain * lift,
      pan: note.pan,
      attack: mood === 'tense' ? 1.1 : 1.45,
      release: 1.25,
      wave: softStringWave
    });
  });
}

function addCelloPulse(buffer, start, beat, midi, mood) {
  const pulses = mood === 'quiet' ? [0] : [0, 2];
  const gain = mood === 'tense' ? 0.024 : mood === 'march' ? 0.026 : 0.018;

  pulses.forEach((beatOffset) => {
    addTone(buffer, {
      start: start + beat * beatOffset,
      duration: beat * 1.35,
      frequency: midiToFrequency(Math.max(43, midi + 12)),
      gain,
      pan: -0.08,
      attack: 0.08,
      release: 0.55,
      wave: mellowCelloWave
    });
  });
}

function addHarpFigure(buffer, start, beat, root, chord, pattern, mood) {
  const gain = mood === 'quiet' ? 0.011 : mood === 'tense' ? 0.014 : 0.016;
  const stepDuration = beat * 0.46;
  const stepGap = mood === 'quiet' ? beat : beat * 0.5;
  const steps = mood === 'quiet' ? 4 : 8;

  for (let i = 0; i < steps; i += 1) {
    const offset = pattern[i % pattern.length];
    const chordTone = chord[i % chord.length];
    addPluck(buffer, {
      start: start + i * stepGap + beat * 0.08,
      duration: stepDuration,
      frequency: midiToFrequency(root + offset + 12 + (i % 2 === 0 ? 0 : chordTone % 5)),
      gain,
      pan: i % 2 === 0 ? -0.32 : 0.32
    });
  }
}

function addLeadLine(buffer, track, beat, bar) {
  const noteEvery = track.mood === 'quiet' ? 2 : 1;
  const gain = track.mood === 'tense' ? 0.018 : track.mood === 'quiet' ? 0.016 : 0.021;

  for (let i = 0; i < track.bars * noteEvery; i += 1) {
    const barIndex = Math.floor(i / noteEvery);
    const offsetInBar = noteEvery === 1 ? beat * 1.42 : beat * 1.85;
    const start = barIndex * bar + offsetInBar + (i % noteEvery) * beat * 1.34;
    const midi = track.root + track.melody[i % track.melody.length] + 12;

    addTone(buffer, {
      start,
      duration: beat * 1.05,
      frequency: midiToFrequency(midi),
      gain,
      pan: 0.12,
      attack: 0.13,
      release: 0.5,
      wave: woodwindWave
    });
  }
}

function addSoftDrum(buffer, start, beat, mood) {
  const accents = mood === 'tense' ? [0, 2] : [0, 1.5, 2.5];
  const gain = mood === 'tense' ? 0.034 : 0.026;

  accents.forEach((beatOffset, index) => {
    addTone(buffer, {
      start: start + beat * beatOffset,
      duration: beat * 0.5,
      frequency: index === 0 ? 92 : 122,
      gain: gain * (index === 0 ? 1 : 0.62),
      pan: 0,
      attack: 0.006,
      release: 0.16,
      wave: drumWave,
      pluck: true,
      decay: 8.5
    });
  });
}

function addTone(buffer, options) {
  const {
    start,
    duration,
    frequency,
    gain,
    pan,
    attack,
    release,
    wave,
    pluck = false,
    decay = 5
  } = options;
  const startIndex = Math.max(0, Math.floor(start * sampleRate));
  const count = Math.max(1, Math.floor(duration * sampleRate));
  const leftGain = Math.cos((pan + 1) * Math.PI * 0.25);
  const rightGain = Math.sin((pan + 1) * Math.PI * 0.25);

  for (let i = 0; i < count; i += 1) {
    const index = startIndex + i;
    if (index >= buffer.left.length) {
      break;
    }

    const t = i / sampleRate;
    const env = pluck
      ? Math.exp(-t * decay) * Math.min(1, t / Math.max(0.001, attack))
      : envelope(t, duration, attack, release);
    const phase = 2 * Math.PI * frequency * t;
    const sample = wave(phase, t) * gain * env;
    buffer.left[index] += sample * leftGain;
    buffer.right[index] += sample * rightGain;
  }
}

function addPluck(buffer, options) {
  addTone(buffer, {
    ...options,
    attack: 0.01,
    release: 0.22,
    wave: harpWave,
    pluck: true,
    decay: 4.6
  });
}

function softStringWave(phase, t) {
  const bow = 1 + Math.sin(2 * Math.PI * 0.7 * t) * 0.018;
  return (
    Math.sin(phase * bow) * 0.7 +
    Math.sin(phase * 2) * 0.13 +
    Math.sin(phase * 3) * 0.045
  );
}

function mellowCelloWave(phase) {
  return Math.sin(phase) * 0.78 + Math.sin(phase * 2) * 0.09 + Math.sin(phase * 0.5) * 0.04;
}

function woodwindWave(phase, t) {
  const vibrato = Math.sin(2 * Math.PI * 4.2 * t) * 0.006;
  return Math.sin(phase + vibrato) * 0.86 + Math.sin(phase * 2) * 0.045;
}

function harpWave(phase) {
  return Math.sin(phase) * 0.82 + Math.sin(phase * 2) * 0.12 + Math.sin(phase * 3) * 0.035;
}

function drumWave(phase, t) {
  const drop = 1 - Math.min(1, t * 5.2) * 0.42;
  return Math.sin(phase * drop) * 0.92 + Math.sin(phase * 2.02) * 0.06;
}

function envelope(t, duration, attack, release) {
  const attackLevel = Math.min(1, t / Math.max(0.001, attack));
  const releaseLevel = Math.min(1, (duration - t) / Math.max(0.001, release));
  const attackCurve = Math.sin(attackLevel * Math.PI * 0.5);
  return Math.max(0, Math.min(attackCurve, releaseLevel));
}

function removeLowRumble(buffer, cutoff) {
  highPass(buffer.left, cutoff);
  highPass(buffer.right, cutoff);
}

function highPass(samples, cutoff) {
  const rc = 1 / (2 * Math.PI * cutoff);
  const dt = 1 / sampleRate;
  const alpha = rc / (rc + dt);
  let previousOutput = 0;
  let previousInput = samples[0] ?? 0;

  for (let i = 0; i < samples.length; i += 1) {
    const input = samples[i];
    const output = alpha * (previousOutput + input - previousInput);
    samples[i] = output;
    previousOutput = output;
    previousInput = input;
  }
}

function applySmallRoom(buffer) {
  const originalLeft = Float32Array.from(buffer.left);
  const originalRight = Float32Array.from(buffer.right);
  const delays = [
    { samples: Math.floor(sampleRate * 0.075), gain: 0.09 },
    { samples: Math.floor(sampleRate * 0.132), gain: 0.055 }
  ];

  delays.forEach(({ samples, gain }) => {
    for (let i = samples; i < buffer.left.length; i += 1) {
      buffer.left[i] += originalRight[i - samples] * gain;
      buffer.right[i] += originalLeft[i - samples] * gain;
    }
  });
}

function smoothEdges(buffer, samples) {
  const limit = Math.min(samples, buffer.left.length);

  for (let i = 0; i < limit; i += 1) {
    const fadeIn = Math.sin((i / limit) * Math.PI * 0.5);
    const fadeOut = Math.sin((i / limit) * Math.PI * 0.5);
    buffer.left[i] *= fadeIn;
    buffer.right[i] *= fadeIn;
    buffer.left[buffer.left.length - 1 - i] *= fadeOut;
    buffer.right[buffer.right.length - 1 - i] *= fadeOut;
  }
}

function midiToFrequency(midi) {
  return 440 * 2 ** ((midi - 69) / 12);
}

function normalize(buffer, target) {
  let peak = 0;
  for (let i = 0; i < buffer.left.length; i += 1) {
    peak = Math.max(peak, Math.abs(buffer.left[i]), Math.abs(buffer.right[i]));
  }

  if (peak === 0) {
    return;
  }

  const scale = target / peak;
  for (let i = 0; i < buffer.left.length; i += 1) {
    buffer.left[i] *= scale;
    buffer.right[i] *= scale;
  }
}

function applySoftLimiter(buffer) {
  for (let i = 0; i < buffer.left.length; i += 1) {
    buffer.left[i] = Math.tanh(buffer.left[i] * 1.08) / Math.tanh(1.08);
    buffer.right[i] = Math.tanh(buffer.right[i] * 1.08) / Math.tanh(1.08);
  }
}

function encodeWav(buffer) {
  const bytesPerSample = 2;
  const frameCount = buffer.left.length;
  const dataSize = frameCount * channels * bytesPerSample;
  const wav = Buffer.alloc(44 + dataSize);

  wav.write('RIFF', 0);
  wav.writeUInt32LE(36 + dataSize, 4);
  wav.write('WAVE', 8);
  wav.write('fmt ', 12);
  wav.writeUInt32LE(16, 16);
  wav.writeUInt16LE(1, 20);
  wav.writeUInt16LE(channels, 22);
  wav.writeUInt32LE(sampleRate, 24);
  wav.writeUInt32LE(sampleRate * channels * bytesPerSample, 28);
  wav.writeUInt16LE(channels * bytesPerSample, 32);
  wav.writeUInt16LE(16, 34);
  wav.write('data', 36);
  wav.writeUInt32LE(dataSize, 40);

  for (let i = 0; i < frameCount; i += 1) {
    const left = Math.max(-1, Math.min(1, buffer.left[i]));
    const right = Math.max(-1, Math.min(1, buffer.right[i]));
    const offset = 44 + i * channels * bytesPerSample;
    wav.writeInt16LE(Math.round(left * 32767), offset);
    wav.writeInt16LE(Math.round(right * 32767), offset + bytesPerSample);
  }

  return wav;
}
