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

const sampleRate = 32000;
const outputDir = 'src/assets/audio/bgm';

mkdirSync(outputDir, { recursive: true });

const tracks = [
  {
    name: 'title-theme',
    root: 50,
    tempo: 64,
    bars: 12,
    progression: [
      [0, 5, 9],
      [2, 7, 11],
      [-3, 2, 7],
      [0, 5, 12]
    ],
    melody: [12, 14, 16, 19, 21, 19, 16, 14],
    warmth: 1,
    motion: 0.45
  },
  {
    name: 'story-dark',
    root: 45,
    tempo: 56,
    bars: 10,
    progression: [
      [0, 3, 7],
      [-5, 0, 5],
      [-2, 2, 7],
      [-7, 0, 3]
    ],
    melody: [10, 7, 5, 3, 5, 7],
    warmth: 0.72,
    motion: 0.18
  },
  {
    name: 'oath-theme',
    root: 52,
    tempo: 60,
    bars: 12,
    progression: [
      [0, 5, 9],
      [4, 9, 12],
      [5, 9, 14],
      [2, 7, 11]
    ],
    melody: [12, 16, 19, 21, 24, 21, 19, 16],
    warmth: 1,
    motion: 0.32
  },
  {
    name: 'militia-theme',
    root: 48,
    tempo: 72,
    bars: 12,
    progression: [
      [0, 5, 9],
      [5, 9, 12],
      [7, 11, 14],
      [2, 7, 11]
    ],
    melody: [12, 14, 16, 19, 16, 14, 12, 14],
    warmth: 0.9,
    motion: 0.5
  },
  {
    name: 'battle-prep',
    root: 43,
    tempo: 68,
    bars: 12,
    progression: [
      [0, 3, 7],
      [2, 7, 10],
      [-2, 3, 7],
      [-5, 0, 5]
    ],
    melody: [12, 15, 17, 19, 17, 15, 12, 10],
    warmth: 0.82,
    motion: 0.42
  }
];

for (const track of tracks) {
  const buffer = buildTrack(track);
  normalize(buffer, 0.48);
  applySoftLimiter(buffer);
  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 = new Float32Array(Math.ceil(duration * sampleRate));

  for (let barIndex = 0; barIndex < track.bars; barIndex += 1) {
    const start = barIndex * bar;
    const chord = track.progression[barIndex % track.progression.length];
    const lift = 0.82 + Math.sin((barIndex / track.bars) * Math.PI) * 0.18;
    const bass = track.root + chord[0] - 12;

    chord.forEach((step, index) => {
      const midi = track.root + step + 12;
      addPad(buffer, start + index * 0.04, bar * 1.08, midiToFrequency(midi), 0.07 * track.warmth * lift);
      addPad(buffer, start + 0.06 + index * 0.03, bar * 1.02, midiToFrequency(midi + 12), 0.025 * track.warmth * lift);
    });

    addLowPad(buffer, start, bar * 1.04, midiToFrequency(bass), 0.055 * lift);
    addLowPad(buffer, start + beat * 2, bar * 0.54, midiToFrequency(bass + 7), 0.035 * lift);

    if (track.motion > 0.25) {
      addGentlePulse(buffer, start, beat, track.root, chord, track.motion * 0.026);
    }
  }

  addSoftMelody(buffer, track, beat, bar);
  smoothEdges(buffer, Math.floor(sampleRate * 1.2));
  return buffer;
}

function addSoftMelody(buffer, track, beat, bar) {
  for (let i = 0; i < track.bars * 2; i += 1) {
    const barIndex = Math.floor(i / 2);
    const start = barIndex * bar + (i % 2 === 0 ? beat * 0.38 : beat * 2.38);
    const midi = track.root + track.melody[i % track.melody.length] + 12;
    addWoodwind(buffer, start, beat * 1.12, midiToFrequency(midi), 0.034);
  }
}

function addGentlePulse(buffer, start, beat, root, chord, gain) {
  const pattern = [0, 1, 2, 1, 0, 2, 1, 2];

  for (let i = 0; i < pattern.length; i += 1) {
    const midi = root + chord[pattern[i] % chord.length] + 12;
    addPluckedString(buffer, start + i * beat * 0.5, beat * 0.42, midiToFrequency(midi), gain);
  }
}

function addPad(buffer, start, duration, frequency, gain) {
  addInstrument(buffer, start, duration, frequency, gain, (phase, t) => {
    const slow = Math.sin(2 * Math.PI * 0.18 * t) * 0.03;
    return (
      Math.sin(phase + slow) * 0.72 +
      Math.sin(phase * 2.001) * 0.12 +
      Math.sin(phase * 0.5) * 0.08
    );
  }, 0.78, 1.05);
}

function addLowPad(buffer, start, duration, frequency, gain) {
  addInstrument(buffer, start, duration, frequency, gain, (phase) => {
    return Math.sin(phase) * 0.84 + Math.sin(phase * 2) * 0.1;
  }, 0.46, 0.9);
}

function addWoodwind(buffer, start, duration, frequency, gain) {
  addInstrument(buffer, start, duration, frequency, gain, (phase, t) => {
    const vibrato = Math.sin(2 * Math.PI * 4.6 * t) * 0.012;
    return Math.sin(phase + vibrato) * 0.82 + Math.sin(phase * 2) * 0.06;
  }, 0.12, 0.42);
}

function addPluckedString(buffer, start, duration, frequency, gain) {
  addInstrument(buffer, start, duration, frequency, gain, (phase) => {
    return Math.sin(phase) * 0.75 + Math.sin(phase * 2) * 0.1;
  }, 0.02, 0.18, true);
}

function addInstrument(buffer, start, duration, frequency, gain, wave, attack, release, pluck = false) {
  const startIndex = Math.max(0, Math.floor(start * sampleRate));
  const count = Math.max(1, Math.floor(duration * sampleRate));

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

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

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

function smoothEdges(buffer, samples) {
  for (let i = 0; i < Math.min(samples, buffer.length); i += 1) {
    const fadeIn = Math.sin((i / samples) * Math.PI * 0.5);
    const fadeOut = Math.sin((i / samples) * Math.PI * 0.5);
    buffer[i] *= fadeIn;
    buffer[buffer.length - 1 - i] *= fadeOut;
  }
}

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

function normalize(buffer, target) {
  let peak = 0;
  for (const value of buffer) {
    peak = Math.max(peak, Math.abs(value));
  }

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

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

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

function encodeWav(buffer) {
  const bytesPerSample = 2;
  const dataSize = buffer.length * 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(1, 22);
  wav.writeUInt32LE(sampleRate, 24);
  wav.writeUInt32LE(sampleRate * bytesPerSample, 28);
  wav.writeUInt16LE(bytesPerSample, 32);
  wav.writeUInt16LE(16, 34);
  wav.write('data', 36);
  wav.writeUInt32LE(dataSize, 40);

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

  return wav;
}
