import { Midi, Track } from '@tonejs/midi';

import {
  MidiInstrumentSchema,
  MidiNoteSchema,
} from '@/hooks/useMidiTranscript';

const groupNameToProgram: Record<string, number> = {
  Vocals: 55,
  Backing_Vocals: 54,
  Drums: 119,
  Bass: 33,
  Guitar: 25,
  Keyboard: 1,
  Percussion: 10, // easter egg
  Strings: 41,
  Synth: 81,
  FX: 97,
  Brass: 57,
  Woodwinds: 76,
};

const isDrumProgram = (program: number) => {
  return [119, 10].includes(program);
};

export const jsonToMidi = (
  songName: string | null,
  midiData: MidiInstrumentSchema[],
  downbeats: [number, number][],
  key: string | null,
  groupName: string | null
) => {
  const TPQN = 480;
  const MAX_SEGMENT_TEMPO = 300;

  const tempoChanges: { beat: number; tempo: number }[] = [];
  const beatTimes = downbeats.map((d) => d[0]);

  if (midiData.length === 0) {
    return null;
  }

  let prevTime = 0.0;
  let prevSegmentTempo = -1.0;
  for (let i = 0; i < beatTimes.length - 1; i++) {
    const currentTime = beatTimes[i];
    if (Math.abs(currentTime - prevTime) < 0.0000001) {
      continue;
    }
    const segmentTempo = 60 / (currentTime - prevTime);
    if (segmentTempo > MAX_SEGMENT_TEMPO) {
      continue;
    }
    if (Math.abs(segmentTempo - prevSegmentTempo) >= 0.0001) {
      tempoChanges.push({
        beat: i,
        tempo: segmentTempo,
      });
    }
    prevTime = currentTime;
    prevSegmentTempo = segmentTempo;
  }

  const midi = new Midi();
  midi.header.fromJSON({
    name: songName || '',
    tempos: tempoChanges.map((t) => ({
      bpm: t.tempo,
      ticks: Math.round(t.beat * TPQN),
    })),
    keySignatures: key
      ? [
          {
            ticks: 0,
            key: key.split('_')[0],
            scale: key.split('_')[1],
          },
        ]
      : [],
    timeSignatures: [],
    ppq: TPQN,
    meta: [],
  });

  const secondsToBeats = (seconds: number) => {
    // find beat indices bracketing the time
    const i = beatTimes.findIndex((t) => t > seconds);
    if (i === -1) {
      return beatTimes.length - 1;
    }
    if (i === 0) {
      return 0;
    }
    const prevBeat = beatTimes[i - 1];
    const nextBeat = beatTimes[i];
    const progress = (seconds - prevBeat) / (nextBeat - prevBeat);
    return i + progress;
  };

  const program = groupName ? groupNameToProgram[groupName] : null;

  const writeNotesToTrack = (notes: MidiNoteSchema[], track: Track) => {
    notes.forEach((n) => {
      const startBeat = secondsToBeats(n.start);
      const endBeat = secondsToBeats(n.end);
      const durationInBeats = endBeat - startBeat;

      track.addNote({
        midi: n.pitch,
        durationTicks: Math.round(durationInBeats * TPQN),
        ticks: Math.round(startBeat * TPQN),
        velocity: n.velocity,
      });
    });
  };

  if (program) {
    // Single track for everything
    const track = midi.addTrack();
    track.name = songName || '';
    track.channel = 1;

    if (isDrumProgram(program)) {
      track.channel = 10;
    }
    track.instrument.fromJSON({
      number: program,
      // tonejs/midi doesn't read these properties
      name: 'ignored',
      family: 'ignored',
    });

    midiData.forEach((c) => {
      writeNotesToTrack(c.notes, track);
    });
  } else {
    // One track per instrument
    midiData.forEach((c) => {
      const track = midi.addTrack();
      track.name = c.name;
      track.channel = 1;
      track.instrument.fromJSON({
        number: 1, // TODO: reverse from GM name
        // tonejs/midi doesn't read these properties
        name: 'ignored',
        family: 'ignored',
      });
      writeNotesToTrack(c.notes, track);
    });
  }

  return new Blob([new Uint8Array(midi.toArray())], {
    type: 'audio/midi',
  });
};

export default jsonToMidi;
