import { DEBUG_MODE } from '../constants/uiConstants';
import type { TimeSignature } from '../types/projectTypes';
import { KGProject, type KeySignature } from '../core/KGProject';
import { KGMidiTrack, type InstrumentType } from '../core/track/KGMidiTrack';
import { FLUIDR3_INSTRUMENT_MAP, STANDARD_MIDI_INSTRUMENT_MAP } from '../constants/generalMidiConstants';
import { KGMidiRegion } from '../core/region/KGMidiRegion';
import { KGMidiNote } from '../core/midi/KGMidiNote';
import { generateUniqueId } from './miscUtil';
import {
  MIDI_HEADER,
  MIDI_TRACK,
  MIDI_TIMING,
  MIDI_EVENTS,
  MIDI_VELOCITY,
  MIDI_TIME_SIGNATURE,
  MIDI_KEY_SIGNATURE,
  MIDI_UTILS
} from '../constants/midiConstants';

export const pianoRollIndexToPitch = (index: number) => {
  return 107 /* MIDI note B7 */ - index;
};

export const MIDI_EVENT_TICKS_PER_BEAT = 480;
export const MIDI_PITCH_BEND_MIN = 0;
export const MIDI_PITCH_BEND_CENTER = 8192;
export const MIDI_PITCH_BEND_MAX = 16383;
export const MIDI_PITCH_BEND_MIN_SIGNED = -8192;
export const MIDI_PITCH_BEND_MAX_SIGNED = 8191;
export const MIDI_CONTROLLER_MIN = 0;
export const MIDI_CONTROLLER_MAX = 127;

export const pitchToNoteName = (pitch: number) => {
  const noteNames = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
  return {
    note: noteNames[pitch % 12],
    octave: Math.floor(pitch / 12) - 1
  };
};

export const pitchToNoteNameString = (pitch: number) => {
    const { note, octave } = pitchToNoteName(pitch);
    return `${note}${octave}`;
  };

export const clampMidiPitchBendValue = (value: number): number => (
  Math.max(MIDI_PITCH_BEND_MIN, Math.min(MIDI_PITCH_BEND_MAX, Math.round(value)))
);

export const clampMidiControllerValue = (value: number): number => (
  Math.max(MIDI_CONTROLLER_MIN, Math.min(MIDI_CONTROLLER_MAX, Math.round(value)))
);

export const midiPitchBendToSignedValue = (value: number): number => (
  clampMidiPitchBendValue(value) - MIDI_PITCH_BEND_CENTER
);

export const signedPitchBendToMidiValue = (value: number): number => (
  clampMidiPitchBendValue(value + MIDI_PITCH_BEND_CENTER)
);

export const midiPitchBendToNormalized = (value: number): number => (
  midiPitchBendToSignedValue(value) / MIDI_PITCH_BEND_CENTER
);

export const noteNameToPitch = (noteName: string): number => {
  const noteMap: { [key: string]: number } = {
    'C': 0, 'C#': 1, 'Cb': -1, 'D': 2, 'D#': 3, 'Db': 1, 'E': 4, 'E#': 5, 'Eb': 3,
    'F': 5, 'F#': 6, 'Fb': 4, 'G': 7, 'G#': 8, 'Gb': 6, 'A': 9, 'A#': 10, 'Ab': 8,
    'B': 11, 'B#': 12, 'Bb': 10
  };
  
  // Parse note name (e.g., "C4", "F#2", "Cb3", "A#7")
  const match = noteName.trim().match(/^([A-G](?:#|b)?)(-?\d+)$/);
  if (!match) {
    throw new Error(`Invalid note name: ${noteName}`);
  }
  
  const note = match[1];
  const octave = parseInt(match[2], 10);
  
  if (!(note in noteMap)) {
    throw new Error(`Invalid note: ${note}`);
  }
  
  const pitch = noteMap[note] + (octave + 1) * 12;
  if (pitch < 0 || pitch > 127) {
    throw new Error(`Note out of MIDI range: ${noteName}`);
  }
  return pitch;
};

export const beatsToBar = (beats: number, timeSignature: TimeSignature) => {
  return {
    bar: Math.floor(beats / timeSignature.numerator),
    beatInBar: beats % timeSignature.numerator
  };
};

export const formatMidiEventPosition = (
  beats: number,
  timeSignature: TimeSignature,
  ticksPerBeat: number = MIDI_EVENT_TICKS_PER_BEAT
): string => {
  const { bar, beatInBar } = beatsToBar(beats, timeSignature);
  const beatInteger = Math.floor(beatInBar);
  let tick = Math.round((beatInBar - beatInteger) * ticksPerBeat);
  let normalizedBeat = beatInteger;
  let normalizedBar = bar;

  if (tick >= ticksPerBeat) {
    tick = 0;
    normalizedBeat += 1;
  }

  if (normalizedBeat >= timeSignature.numerator) {
    normalizedBeat = 0;
    normalizedBar += 1;
  }

  return `${normalizedBar + 1} ${normalizedBeat + 1} ${tick}`;
};

export const formatMidiEventLength = (
  beats: number,
  ticksPerBeat: number = MIDI_EVENT_TICKS_PER_BEAT
): string => {
  const fullBeats = Math.floor(beats);
  let tick = Math.round((beats - fullBeats) * ticksPerBeat);
  let normalizedBeats = fullBeats;

  if (tick >= ticksPerBeat) {
    tick = 0;
    normalizedBeats += 1;
  }

  return `${normalizedBeats} ${tick}`;
};

export type MidiEventPositionParseResult =
  | { absoluteBeat: number }
  | { error: string };

export const parseMidiEventPosition = (
  raw: string,
  timeSignature: TimeSignature,
  ticksPerBeat: number = MIDI_EVENT_TICKS_PER_BEAT
): MidiEventPositionParseResult => {
  const match = raw.trim().match(/^(\d+)\s+(\d+)\s+(\d+)$/);
  if (!match) {
    return { error: 'Use Position as "bar beat tick", for example "4 2 120".' };
  }

  const bar = parseInt(match[1], 10);
  const beat = parseInt(match[2], 10);
  let tick = parseInt(match[3], 10);

  if (bar < 1) {
    return { error: 'Bar number must be 1 or greater.' };
  }
  if (beat < 1 || beat > timeSignature.numerator) {
    return { error: `Beat must be between 1 and ${timeSignature.numerator} for the current time signature.` };
  }
  if (tick < 0 || tick > ticksPerBeat) {
    return { error: `Tick must be between 0 and ${ticksPerBeat}.` };
  }

  let normalizedBar = bar;
  let normalizedBeat = beat;
  if (tick === ticksPerBeat) {
    tick = 0;
    normalizedBeat += 1;
    if (normalizedBeat > timeSignature.numerator) {
      normalizedBeat = 1;
      normalizedBar += 1;
    }
  }

  const absoluteBeat = ((normalizedBar - 1) * timeSignature.numerator) +
    (normalizedBeat - 1) +
    (tick / ticksPerBeat);

  return { absoluteBeat };
};

export type MidiEventPositionDeltaParseResult =
  | { deltaBeats: number }
  | { error: string };

export const parseMidiEventPositionDelta = (
  raw: string,
  timeSignature: TimeSignature,
  ticksPerBeat: number = MIDI_EVENT_TICKS_PER_BEAT
): MidiEventPositionDeltaParseResult => {
  const match = raw.trim().match(/^([+-])(\d+)\s+(\d+)\s+(\d+)$/);
  if (!match) {
    return { error: 'Use position delta as "+bars beats tick" or "-bars beats tick", for example "+0 1 120".' };
  }

  const sign = match[1] === '-' ? -1 : 1;
  const bars = parseInt(match[2], 10);
  const beats = parseInt(match[3], 10);
  let tick = parseInt(match[4], 10);

  if (beats < 0 || beats > timeSignature.numerator) {
    return { error: `Delta beat component must be between 0 and ${timeSignature.numerator}.` };
  }
  if (tick < 0 || tick > ticksPerBeat) {
    return { error: `Delta tick must be between 0 and ${ticksPerBeat}.` };
  }

  let normalizedBars = bars;
  let normalizedBeats = beats;
  if (tick === ticksPerBeat) {
    tick = 0;
    normalizedBeats += 1;
    if (normalizedBeats >= timeSignature.numerator) {
      normalizedBars += Math.floor(normalizedBeats / timeSignature.numerator);
      normalizedBeats = normalizedBeats % timeSignature.numerator;
    }
  }

  const deltaBeats = sign * (
    (normalizedBars * timeSignature.numerator) +
    normalizedBeats +
    (tick / ticksPerBeat)
  );

  return { deltaBeats };
};

export type MidiEventLengthParseResult =
  | { duration: number }
  | { error: string };

export const parseMidiEventLength = (
  raw: string,
  ticksPerBeat: number = MIDI_EVENT_TICKS_PER_BEAT
): MidiEventLengthParseResult => {
  const match = raw.trim().match(/^(\d+)\s+(\d+)$/);
  if (!match) {
    return { error: 'Use Length as "beats tick", for example "1 240".' };
  }

  let beats = parseInt(match[1], 10);
  let tick = parseInt(match[2], 10);

  if (beats < 0) {
    return { error: 'Length beats must be 0 or greater.' };
  }
  if (tick < 0 || tick > ticksPerBeat) {
    return { error: `Length tick must be between 0 and ${ticksPerBeat}.` };
  }

  if (tick === ticksPerBeat) {
    tick = 0;
    beats += 1;
  }

  const duration = beats + (tick / ticksPerBeat);
  if (duration <= 0) {
    return { error: 'Length must be greater than 0.' };
  }

  return { duration };
};

export type MidiEventLengthDeltaParseResult =
  | { deltaBeats: number }
  | { error: string };

export const parseMidiEventLengthDelta = (
  raw: string,
  ticksPerBeat: number = MIDI_EVENT_TICKS_PER_BEAT
): MidiEventLengthDeltaParseResult => {
  const match = raw.trim().match(/^([+-])(\d+)\s+(\d+)$/);
  if (!match) {
    return { error: 'Use length delta as "+beats tick" or "-beats tick", for example "+1 240".' };
  }

  const sign = match[1] === '-' ? -1 : 1;
  let beats = parseInt(match[2], 10);
  let tick = parseInt(match[3], 10);

  if (tick < 0 || tick > ticksPerBeat) {
    return { error: `Length delta tick must be between 0 and ${ticksPerBeat}.` };
  }

  if (tick === ticksPerBeat) {
    tick = 0;
    beats += 1;
  }

  return { deltaBeats: sign * (beats + (tick / ticksPerBeat)) };
};

export const midiPercussionKeyMap: Record<number, { fullName: string; shortName: string }> = {
  35: { fullName: 'Acoustic Bass Drum', shortName: 'Ac.Bass' },
  36: { fullName: 'Bass Drum 1', shortName: 'BassDrum' },
  37: { fullName: 'Side Stick', shortName: 'SideStick' },
  38: { fullName: 'Acoustic Snare', shortName: 'Ac.Snare' },
  39: { fullName: 'Hand Clap', shortName: 'HandClap' },
  40: { fullName: 'Electric Snare', shortName: 'ElecSnare' },
  41: { fullName: 'Low Floor Tom', shortName: 'LowFloor' },
  42: { fullName: 'Closed Hi Hat', shortName: 'ClosedHH' },
  43: { fullName: 'High Floor Tom', shortName: 'HighFloor' },
  44: { fullName: 'Pedal Hi-Hat', shortName: 'PedalHH' },
  45: { fullName: 'Low Tom', shortName: 'LowTom' },
  46: { fullName: 'Open Hi-Hat', shortName: 'OpenHH' },
  47: { fullName: 'Low-Mid Tom', shortName: 'LowMidTom' },
  48: { fullName: 'Hi Mid Tom', shortName: 'HiMidTom' },
  49: { fullName: 'Crash Cymbal 1', shortName: 'Crash1' },
  50: { fullName: 'High Tom', shortName: 'HighTom' },
  51: { fullName: 'Ride Cymbal 1', shortName: 'Ride1' },
  52: { fullName: 'Chinese Cymbal', shortName: 'Chinese' },
  53: { fullName: 'Ride Bell', shortName: 'RideBell' },
  54: { fullName: 'Tambourine', shortName: 'Tambourine' },
  55: { fullName: 'Splash Cymbal', shortName: 'Splash' },
  56: { fullName: 'Cowbell', shortName: 'Cowbell' },
  57: { fullName: 'Crash Cymbal 2', shortName: 'Crash2' },
  58: { fullName: 'Vibraslap', shortName: 'Vibraslap' },
  59: { fullName: 'Ride Cymbal 2', shortName: 'Ride2' },
  60: { fullName: 'Hi Bongo', shortName: 'HiBongo' },
  61: { fullName: 'Low Bongo', shortName: 'LowBongo' },
  62: { fullName: 'Mute Hi Conga', shortName: 'MuteHiCon' },
  63: { fullName: 'Open Hi Conga', shortName: 'OpenHiCon' },
  64: { fullName: 'Low Conga', shortName: 'LowConga' },
  65: { fullName: 'High Timbale', shortName: 'HighTimba' },
  66: { fullName: 'Low Timbale', shortName: 'LowTimba' },
  67: { fullName: 'High Agogo', shortName: 'HighAgo' },
  68: { fullName: 'Low Agogo', shortName: 'LowAgo' },
  69: { fullName: 'Cabasa', shortName: 'Cabasa' },
  70: { fullName: 'Maracas', shortName: 'Maracas' },
  71: { fullName: 'Short Whistle', shortName: 'ShortWhis' },
  72: { fullName: 'Long Whistle', shortName: 'LongWhis' },
  73: { fullName: 'Short Guiro', shortName: 'ShortGui' },
  74: { fullName: 'Long Guiro', shortName: 'LongGui' },
  75: { fullName: 'Claves', shortName: 'Claves' },
  76: { fullName: 'Hi Wood Block', shortName: 'HiWood' },
  77: { fullName: 'Low Wood Block', shortName: 'LowWood' },
  78: { fullName: 'Mute Cuica', shortName: 'MuteCuica' },
  79: { fullName: 'Open Cuica', shortName: 'OpenCuica' },
  80: { fullName: 'Mute Triangle', shortName: 'MuteTri' },
  81: { fullName: 'Open Triangle', shortName: 'OpenTri' }
};

/**
 * Converts a KGSP project to standard MIDI format
 * @param project - The KGProject to convert
 * @returns Uint8Array containing the MIDI file data
 */
export const convertProjectToMidi = (project: KGProject): Uint8Array => {
  // Calculate number of tracks (1 tempo track + MIDI tracks)
  const midiTracks = project.getTracks().filter(track => track.getCurrentType() === 'KGMidiTrack') as KGMidiTrack[];
  const totalTracks = 1 + midiTracks.length; // Tempo track + instrument tracks
  
  const chunks: Uint8Array[] = [];
  
  // Set up channel assignment map for 16 MIDI channels (0-15)
  // Value is the assigned GM program number (1-128), 'DRUMS' for percussion, or null if free
  const channelAssignments: (number | 'DRUMS' | null)[] = new Array(16).fill(null);
  // Reserve channel 1 (index 0) for Acoustic Grand Piano (GM program 1)
  channelAssignments[0] = 1;
  // Reserve channel 10 (index 9) for drum kit
  channelAssignments[9] = 'DRUMS';
  
  // 1. Create MIDI header chunk
  chunks.push(...createMidiHeader(totalTracks));
  
  // 2. Create tempo/meta track
  chunks.push(...createTempoTrack(project));
  
  // 3. Create instrument tracks
  midiTracks.forEach((track) => {
    const instrument = track.getInstrument();
    const isDrums = isDrumInstrument(instrument);
    const gmProgram = getGMInstrument(instrument); // 1-128
    const channel = assignChannelForInstrument(gmProgram, isDrums, channelAssignments);

    chunks.push(...createInstrumentTrack(track, project, channel, gmProgram, isDrums));
  });
  
  // Combine all chunks into final MIDI file
  const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
  const result = new Uint8Array(totalLength);
  let offset = 0;
  
  chunks.forEach(chunk => {
    result.set(chunk, offset);
    offset += chunk.length;
  });
  
  return result;
};

/**
 * Creates the MIDI file header chunk
 */
function createMidiHeader(trackCount: number): Uint8Array[] {
  const chunks: Uint8Array[] = [];
  
  // Header chunk type ("MThd")
  chunks.push(MIDI_HEADER.CHUNK_TYPE);
  
  // Header chunk size (6 bytes)
  chunks.push(MIDI_HEADER.HEADER_SIZE);
  
  // Format type 1 (multi-track)
  chunks.push(MIDI_HEADER.FORMAT_1);
  
  // Number of tracks
  chunks.push(MIDI_UTILS.int16ToBytes(trackCount));
  
  // Time division (TPQN)
  chunks.push(MIDI_TIMING.TPQN_BYTES);
  
  return chunks;
}

/**
 * Creates the tempo/meta track with project settings
 */
function createTempoTrack(project: KGProject): Uint8Array[] {
  const events: Uint8Array[] = [];
  
  // Track name meta event
  events.push(MIDI_UTILS.encodeVLQ(0)); // Delta time 0
  events.push(new Uint8Array([MIDI_EVENTS.META_EVENT, MIDI_EVENTS.META_TRACK_NAME]));
  const trackNameBytes = new TextEncoder().encode('Tempo Track');
  events.push(MIDI_UTILS.encodeVLQ(trackNameBytes.length));
  events.push(trackNameBytes);
  
  // Time signature meta event
  events.push(MIDI_UTILS.encodeVLQ(0)); // Delta time 0
  events.push(new Uint8Array([MIDI_EVENTS.META_EVENT, MIDI_EVENTS.META_TIME_SIGNATURE, 0x04])); // 4 bytes
  const timeSignature = project.getTimeSignature();
  const denomPower = MIDI_TIME_SIGNATURE.DENOMINATOR_MAP[timeSignature.denominator as keyof typeof MIDI_TIME_SIGNATURE.DENOMINATOR_MAP] || 2;
  events.push(new Uint8Array([
    timeSignature.numerator,
    denomPower,
    MIDI_TIME_SIGNATURE.DEFAULT_METRONOME_PULSE,
    MIDI_TIME_SIGNATURE.DEFAULT_32ND_NOTES_PER_QUARTER
  ]));
  
  // Key signature meta event
  events.push(MIDI_UTILS.encodeVLQ(0)); // Delta time 0
  events.push(new Uint8Array([MIDI_EVENTS.META_EVENT, MIDI_EVENTS.META_KEY_SIGNATURE, 0x02])); // 2 bytes
  const keySignature = getKeySignatureBytes(project.getKeySignature());
  events.push(keySignature);
  
  // Tempo meta event
  events.push(MIDI_UTILS.encodeVLQ(0)); // Delta time 0
  events.push(new Uint8Array([MIDI_EVENTS.META_EVENT, MIDI_EVENTS.META_TEMPO, 0x03])); // 3 bytes
  const microsecondsPerQuarter = MIDI_UTILS.bpmToMicrosecondsPerQuarter(project.getBpm());
  events.push(new Uint8Array([
    (microsecondsPerQuarter >> 16) & 0xFF,
    (microsecondsPerQuarter >> 8) & 0xFF,
    microsecondsPerQuarter & 0xFF
  ]));
  
  // End of track
  events.push(MIDI_TRACK.END_OF_TRACK);
  
  return createTrackChunk(events);
}

/**
 * Creates a MIDI track for an instrument
 */
function createInstrumentTrack(
  track: KGMidiTrack,
  project: KGProject,
  channel: number,
  gmProgram: number,
  isDrums: boolean
): Uint8Array[] {
  const events: Uint8Array[] = [];
  
  // Track name meta event
  events.push(MIDI_UTILS.encodeVLQ(0)); // Delta time 0
  events.push(new Uint8Array([MIDI_EVENTS.META_EVENT, MIDI_EVENTS.META_TRACK_NAME]));
  const trackNameBytes = new TextEncoder().encode(track.getName());
  events.push(MIDI_UTILS.encodeVLQ(trackNameBytes.length));
  events.push(trackNameBytes);
  
  // Program change (instrument selection) - skip for drums (channel 10)
  if (!isDrums && channel !== 9) {
    events.push(MIDI_UTILS.encodeVLQ(0)); // Delta time 0
    events.push(new Uint8Array([
      MIDI_EVENTS.PROGRAM_CHANGE | channel,
      Math.max(0, Math.min(127, gmProgram - 1)) // MIDI program change is 0-127
    ]));
  }
  
  // Collect all notes from all regions and sort by absolute time
  const allNotes: Array<{
    note: KGMidiNote;
    absoluteStartBeat: number;
    absoluteEndBeat: number;
  }> = [];
  
  track.getRegions().forEach((region) => {
    // Type guard to ensure we have a KGMidiRegion
    if (region.getCurrentType() === 'KGMidiRegion') {
      const midiRegion = region as KGMidiRegion;
      midiRegion.getNotes().forEach((note: KGMidiNote) => {
        allNotes.push({
          note,
          absoluteStartBeat: midiRegion.getStartFromBeat() + note.getStartBeat(),
          absoluteEndBeat: midiRegion.getStartFromBeat() + note.getEndBeat(),
        });
      });
    }
  });
  
  // Sort notes by start time, then by pitch for consistent ordering
  allNotes.sort((a, b) => {
    const timeDiff = a.absoluteStartBeat - b.absoluteStartBeat;
    return timeDiff !== 0 ? timeDiff : a.note.getPitch() - b.note.getPitch();
  });
  
  // Create MIDI events for notes
  const midiEvents: Array<{
    tick: number;
    event: Uint8Array;
  }> = [];
  
  // Add note on/off events
  allNotes.forEach(({ note, absoluteStartBeat, absoluteEndBeat }) => {
    const startTick = beatToTicks(absoluteStartBeat, project.getTimeSignature());
    const endTick = beatToTicks(absoluteEndBeat, project.getTimeSignature());
    const velocity = Math.max(MIDI_VELOCITY.MIN, Math.min(MIDI_VELOCITY.MAX, note.getVelocity()));
    const pitch = Math.max(0, Math.min(127, note.getPitch()));
    
    // Note On event
    midiEvents.push({
      tick: startTick,
      event: new Uint8Array([
        MIDI_EVENTS.NOTE_ON | channel,
        pitch,
        velocity
      ])
    });
    
    // Note Off event
    midiEvents.push({
      tick: endTick,
      event: new Uint8Array([
        MIDI_EVENTS.NOTE_OFF | channel,
        pitch,
        0 // Release velocity
      ])
    });
  });
  
  // Sort all events by tick time
  midiEvents.sort((a, b) => {
    const timeDiff = a.tick - b.tick;
    // If same time, prioritize note off before note on to avoid conflicts
    if (timeDiff === 0) {
      const aIsNoteOff = (a.event[0] & 0xF0) === MIDI_EVENTS.NOTE_OFF;
      const bIsNoteOff = (b.event[0] & 0xF0) === MIDI_EVENTS.NOTE_OFF;
      if (aIsNoteOff && !bIsNoteOff) return -1;
      if (!aIsNoteOff && bIsNoteOff) return 1;
    }
    return timeDiff;
  });
  
  // Convert to delta time format and add to events
  let previousTick = 0;
  midiEvents.forEach(({ tick, event }) => {
    const deltaTime = tick - previousTick;
    events.push(MIDI_UTILS.encodeVLQ(deltaTime));
    events.push(event);
    previousTick = tick;
  });
  
  // End of track
  events.push(MIDI_TRACK.END_OF_TRACK);
  
  return createTrackChunk(events);
}

/**
 * Creates a track chunk with the given events
 */
function createTrackChunk(events: Uint8Array[]): Uint8Array[] {
  const chunks: Uint8Array[] = [];
  
  // Track chunk type ("MTrk")
  chunks.push(MIDI_TRACK.CHUNK_TYPE);
  
  // Calculate total data length
  const totalLength = events.reduce((sum, event) => sum + event.length, 0);
  
  // Track chunk size
  chunks.push(MIDI_UTILS.int32ToBytes(totalLength));
  
  // Track data
  chunks.push(...events);
  
  return chunks;
}

/**
 * Converts beats to MIDI ticks
 */
function beatToTicks(beats: number, timeSignature: { numerator: number; denominator: number }): number {
  // In MIDI, ticks are relative to quarter notes
  // Convert beats to quarter notes based on time signature
  const quarterNotesPerBeat = 4 / timeSignature.denominator;
  const quarterNotes = beats * quarterNotesPerBeat;
  return Math.round(quarterNotes * MIDI_TIMING.TPQN);
}

/**
 * Determines if an instrument is a drum kit per FluidR3 map
 */
function isDrumInstrument(instrument: InstrumentType): boolean {
  const key = String(instrument);
  return key === 'standard' || FLUIDR3_INSTRUMENT_MAP[key]?.group === 'PERCUSSION_KIT';
}

/**
 * Assign a MIDI channel for the given GM program and drum flag and update the assignment map
 * - Reserves channel 10 (index 9) for drums
 * - Reserves channel 1 (index 0) for Acoustic Grand Piano (program 1)
 * - Reuses channels that already match the requested program
 * - Falls back to channel 1 if no free channels are available
 */
function assignChannelForInstrument(
  gmProgram: number,
  isDrums: boolean,
  channelAssignments: (number | 'DRUMS' | null)[]
): number {
  // Drums always go to channel 10
  if (isDrums) {
    return 9;
  }
  
  // If requesting Acoustic Grand Piano, use channel 1
  if (gmProgram === 1) {
    return 0;
  }
  
  // Reuse an existing channel already assigned to this program
  for (let ch = 0; ch < 16; ch++) {
    if (channelAssignments[ch] === gmProgram) {
      return ch;
    }
  }
  
  // Find a free channel (skip 0 and 9 which are reserved)
  for (let ch = 0; ch < 16; ch++) {
    if (ch === 0 || ch === 9) continue;
    if (channelAssignments[ch] === null) {
      channelAssignments[ch] = gmProgram;
      return ch;
    }
  }
  
  // No available channel: fall back to channel 1 (piano)
  return 0;
}

/**
 * Gets the General MIDI instrument number
 */
function getGMInstrument(instrument: InstrumentType): number {
  return FLUIDR3_INSTRUMENT_MAP[instrument]?.midiInstrument || 1; // fallback to piano when instrument is not found
}

/**
 * Gets key signature bytes for MIDI format
 */
function getKeySignatureBytes(keySignature: KeySignature): Uint8Array {
  // Check sharp keys
  if (keySignature in MIDI_KEY_SIGNATURE.SHARP_KEYS) {
    const sharps = MIDI_KEY_SIGNATURE.SHARP_KEYS[keySignature as keyof typeof MIDI_KEY_SIGNATURE.SHARP_KEYS];
    return new Uint8Array([sharps, 0]); // 0 = major
  }
  
  // Check flat keys
  if (keySignature in MIDI_KEY_SIGNATURE.FLAT_KEYS) {
    const flats = MIDI_KEY_SIGNATURE.FLAT_KEYS[keySignature as keyof typeof MIDI_KEY_SIGNATURE.FLAT_KEYS];
    return new Uint8Array([flats & 0xFF, 0]); // Convert to unsigned byte, 0 = major
  }
  
  // Check minor keys
  if (keySignature in MIDI_KEY_SIGNATURE.MINOR_KEYS) {
    const accidentals = MIDI_KEY_SIGNATURE.MINOR_KEYS[keySignature as keyof typeof MIDI_KEY_SIGNATURE.MINOR_KEYS];
    return new Uint8Array([accidentals & 0xFF, 1]); // Convert to unsigned byte, 1 = minor
  }
  
  // Default to C major
  return new Uint8Array([0, 0]);
}

// ─── K.G.One clip MIDI import helpers ────────────────────────────────────────

export interface RawMidiNote {
  startBeat: number;
  endBeat: number;
  pitch: number;
  velocity: number;
}

/**
 * Parses a MIDI binary and returns all notes from the first track that has
 * notes, with beat offsets normalised so the earliest note starts at beat 0.
 * Used by the K.G.One Clip drag-to-MIDI-track feature.
 */
export function parseMidiFirstTrackNotes(data: Uint8Array): {
  notes: RawMidiNote[];
  totalBeats: number;
} {
  const midiFile = parseMidiFile(data);
  const firstTrack = midiFile.tracks.find(t => t.notes.length > 0);
  if (!firstTrack || firstTrack.notes.length === 0) {
    return { notes: [], totalBeats: 0 };
  }
  const minBeat = Math.min(...firstTrack.notes.map(n => n.startBeat));
  const notes: RawMidiNote[] = firstTrack.notes.map(n => ({
    startBeat: n.startBeat - minBeat,
    endBeat: n.endBeat - minBeat,
    pitch: n.pitch,
    velocity: n.velocity,
  }));
  const totalBeats = Math.max(...notes.map(n => n.endBeat));
  return { notes, totalBeats };
}

/**
 * Converts a MIDI binary file to a KGSP project
 * @param midiData - The MIDI file data as Uint8Array
 * @param existingProject - Optional existing project to merge into (creates new if null/undefined)
 * @returns KGProject with MIDI data converted to KGSP format
 */
export const convertMidiToProject = (midiData: Uint8Array, existingProject?: KGProject | null): KGProject => {
  // Create new project if none provided
  const project = existingProject || new KGProject();
  
  // Parse MIDI file
  const midiFile = parseMidiFile(midiData);
  
  // Apply project-level settings from MIDI meta events (only for new projects)
  if (!existingProject || existingProject.getTracks().length === 0) {
    if (midiFile.tempo) {
      project.setBpm(Math.round(60000000 / midiFile.tempo)); // Convert microseconds per quarter to BPM
    }
    
    if (midiFile.timeSignature) {
      project.setTimeSignature(midiFile.timeSignature);
    }
    
    if (midiFile.keySignature) {
      project.setKeySignature(midiFile.keySignature);
    }
  }
  
  // Get existing tracks to calculate proper track indices
  const existingTracks = project.getTracks();
  const startingTrackIndex = existingTracks.length;
  
  // Convert MIDI tracks to KGSP tracks
  let addedTrackCount = 0;
  midiFile.tracks.forEach((midiTrack, midiTrackIndex) => {
    // Skip tempo tracks (they usually don't have notes)
    if (midiTrack.notes.length === 0) {
      return;
    }
    
    // Determine instrument from MIDI channel and program
    const instrument = getKGInstrumentFromMidi(midiTrack.channel, midiTrack.program);
    
    // Calculate proper track index (append to existing tracks)
    const actualTrackIndex = startingTrackIndex + addedTrackCount;
    
    // Create new KGSP track
    const trackId = Date.now() + midiTrackIndex; // Simple unique ID
    const trackName = midiTrack.name || `Track ${actualTrackIndex + 1}`;
    const kgTrack = new KGMidiTrack(trackName, trackId, instrument);
    kgTrack.setTrackIndex(actualTrackIndex);
    
    // Group notes into regions (every 16 beats for now, could be more sophisticated)
    const regions = groupNotesIntoRegions(midiTrack.notes, project.getTimeSignature());
    
    regions.forEach((regionData, regionIndex) => {
      const regionId = generateUniqueId('KGMidiRegion');
      const regionName = `${trackName} Region ${regionIndex + 1}`;
      const region = new KGMidiRegion(
        regionId,
        trackId.toString(),
        actualTrackIndex,
        regionName,
        regionData.startBeat,
        regionData.length
      );
      
      // Convert MIDI notes to KG notes (with relative timing)
      regionData.notes.forEach(midiNote => {
        const noteId = generateUniqueId('KGMidiNote');
        const relativeStartBeat = midiNote.startBeat - regionData.startBeat;
        const relativeEndBeat = midiNote.endBeat - regionData.startBeat;
        
        const kgNote = new KGMidiNote(
          noteId,
          relativeStartBeat,
          relativeEndBeat,
          midiNote.pitch,
          midiNote.velocity
        );
        
        region.addNote(kgNote);
      });
      
      kgTrack.addRegion(region);
    });
    
    // Add track to project
    project.getTracks().push(kgTrack);
    addedTrackCount += 1;
  });
  
  return project;
};

// MIDI parsing types and interfaces
interface ParsedMidiFile {
  format: number;
  trackCount: number;
  ticksPerQuarter: number;
  tempo?: number; // microseconds per quarter note
  timeSignature?: TimeSignature;
  keySignature?: KeySignature;
  tracks: ParsedMidiTrack[];
}

interface ParsedMidiTrack {
  name?: string;
  channel: number;
  program?: number; // instrument program
  notes: ParsedMidiNote[];
}

interface ParsedMidiNote {
  startBeat: number;
  endBeat: number;
  pitch: number;
  velocity: number;
}

interface RegionData {
  startBeat: number;
  length: number;
  notes: ParsedMidiNote[];
}

/**
 * Parses a MIDI binary file into a structured format
 */
function parseMidiFile(data: Uint8Array): ParsedMidiFile {
  let offset = 0;
  
  if (DEBUG_MODE.MIDI_IMPORT) {
    console.log('🎵 Starting MIDI file parsing...');
  }
  
  // Parse header chunk
  const headerChunk = parseChunk(data, offset);
  offset += headerChunk.totalSize;
  
  if (headerChunk.type !== 'MThd') {
    throw new Error('Invalid MIDI file: Missing header chunk');
  }
  
  const format = readUint16(headerChunk.data, 0);
  const trackCount = readUint16(headerChunk.data, 2);
  const timeDivision = readUint16(headerChunk.data, 4);
  
  if (DEBUG_MODE.MIDI_IMPORT) {
    console.log('📋 MIDI Header parsed:');
    console.log(`  ├── Format: ${format} (${format === 0 ? 'Single track' : format === 1 ? 'Multi-track' : 'Unknown'})`);
    console.log(`  ├── Track count: ${trackCount}`);
    console.log(`  └── Time division: ${timeDivision} (${timeDivision & 0x8000 ? 'SMPTE' : 'TPQN'})`);
  }
  
  // Only support TPQN format (bit 15 = 0)
  if (timeDivision & 0x8000) {
    throw new Error('SMPTE time division not supported');
  }
  
  const ticksPerQuarter = timeDivision;
  
  const midiFile: ParsedMidiFile = {
    format,
    trackCount,
    ticksPerQuarter,
    tracks: []
  };
  
  if (DEBUG_MODE.MIDI_IMPORT) {
    console.log('🎼 Parsing tracks...');
  }
  
  // Parse track chunks
  for (let i = 0; i < trackCount; i++) {
    if (offset >= data.length) break;
    
    if (DEBUG_MODE.MIDI_IMPORT) {
      console.log(`📝 Parsing track ${i + 1}/${trackCount}...`);
    }
    
    const trackChunk = parseChunk(data, offset);
    offset += trackChunk.totalSize;
    
    if (trackChunk.type !== 'MTrk') {
      console.warn(`Expected track chunk, got ${trackChunk.type}`);
      continue;
    }
    
    const track = parseTrack(trackChunk.data, ticksPerQuarter, midiFile);
    midiFile.tracks.push(track);
    
    if (DEBUG_MODE.MIDI_IMPORT) {
      console.log(`├── ${track.name || `Track ${i + 1}`} (Channel: ${track.channel}, Program: ${track.program ?? 'none'}, Notes: ${track.notes.length})`);
      track.notes.forEach(note => {
        const startTick = Math.round(note.startBeat * ticksPerQuarter);
        const endTick = Math.round(note.endBeat * ticksPerQuarter);
        console.log(`│   ├── ${note.pitch} | ${startTick} | ${endTick}`);
      });
    }
  }
  
  return midiFile;
}

/**
 * Parses a generic MIDI chunk (header or track)
 */
function parseChunk(data: Uint8Array, offset: number) {
  if (offset + 8 > data.length) {
    throw new Error('Invalid MIDI file: Incomplete chunk header');
  }
  
  const type = String.fromCharCode(...data.slice(offset, offset + 4));
  const length = readUint32(data, offset + 4);
  
  if (offset + 8 + length > data.length) {
    throw new Error('Invalid MIDI file: Chunk data exceeds file length');
  }
  
  return {
    type,
    length,
    data: data.slice(offset + 8, offset + 8 + length),
    totalSize: 8 + length
  };
}

/**
 * Parses a MIDI track chunk
 */
function parseTrack(data: Uint8Array, ticksPerQuarter: number, midiFile: ParsedMidiFile): ParsedMidiTrack {
  let offset = 0;
  let currentTick = 0;
  let runningStatus = 0;
  
  const track: ParsedMidiTrack = {
    channel: 0, // Default channel
    notes: []
  };
  
  // Track active notes (for note off events)
  const activeNotes = new Map<string, { startTick: number; pitch: number; velocity: number }>();
  let primaryChannelDetected = false;
  
  while (offset < data.length) {
    // Read delta time
    const deltaTime = readVLQ(data, offset);
    offset += deltaTime.bytesRead;
    currentTick += deltaTime.value;
    
    if (offset >= data.length) break;
    
    // Read status byte (or apply running status)
    let status = data[offset];
    if (status < 0x80) {
      // running status: use previous status, do not advance offset here
      status = runningStatus;
    } else {
      offset++;
      runningStatus = status;
    }

    // Handle Meta and System Exclusive separately (they don't follow the channel-voice pattern)
    if (status === MIDI_EVENTS.META_EVENT) {
      if (offset + 1 > data.length) break;
      const metaType = data[offset];
      offset += 1;

      const metaLength = readVLQ(data, offset);
      offset += metaLength.bytesRead;
      if (offset + metaLength.value > data.length) break;

      const metaData = data.slice(offset, offset + metaLength.value);
      offset += metaLength.value;

      switch (metaType) {
        case MIDI_EVENTS.META_TRACK_NAME:
          track.name = new TextDecoder().decode(metaData);
          if (DEBUG_MODE.MIDI_IMPORT) {
            console.log(`🏷️  Track name: "${track.name}"`);
          }
          break;

        case MIDI_EVENTS.META_TEMPO:
          if (metaData.length >= 3) {
            const tempo = (metaData[0] << 16) | (metaData[1] << 8) | metaData[2];
            midiFile.tempo = tempo;
            const bpm = Math.round(60000000 / tempo);
            if (DEBUG_MODE.MIDI_IMPORT) {
              console.log(`🥁 Tempo found: ${tempo} μs/quarter note (${bpm} BPM)`);
            }
          }
          break;

        case MIDI_EVENTS.META_TIME_SIGNATURE:
          if (metaData.length >= 4) {
            const numerator = metaData[0];
            const denominatorPower = metaData[1];
            const denominator = Math.pow(2, denominatorPower);
            midiFile.timeSignature = { numerator, denominator };
            if (DEBUG_MODE.MIDI_IMPORT) {
              console.log(`🎵 Time signature found: ${numerator}/${denominator}`);
            }
          }
          break;

        case MIDI_EVENTS.META_KEY_SIGNATURE:
          if (metaData.length >= 2) {
            const sharpsFlats = metaData[0];
            const majorMinor = metaData[1]; // 0 = major, 1 = minor
            midiFile.keySignature = getKeySignatureFromMidi(sharpsFlats, majorMinor);
            if (DEBUG_MODE.MIDI_IMPORT) {
              const accidentals = sharpsFlats > 127 ? sharpsFlats - 256 : sharpsFlats;
              console.log(`🎼 Key signature found: ${midiFile.keySignature} (${accidentals} ${accidentals >= 0 ? 'sharps' : 'flats'}, ${majorMinor === 0 ? 'major' : 'minor'})`);
            }
          }
          break;

        case MIDI_EVENTS.META_END_OF_TRACK:
          if (DEBUG_MODE.MIDI_IMPORT) {
            console.log('🏁 End of track reached');
          }
          return track;
      }

      continue;
    }

    // System Exclusive events (0xF0/0xF7): skip their payload length
    if (status === 0xF0 || status === 0xF7) {
      const sysexLength = readVLQ(data, offset);
      offset += sysexLength.bytesRead + Math.min(sysexLength.value, data.length - (offset));
      continue;
    }

    const eventType = status & 0xF0;
    const channel = status & 0x0F;
    if (!primaryChannelDetected && eventType >= 0x80 && eventType <= 0xE0) {
      track.channel = channel;
      primaryChannelDetected = true;
    }

    switch (eventType) {
      case MIDI_EVENTS.NOTE_ON: {
        if (offset + 2 > data.length) break;
        const pitch = data[offset];
        const velocity = data[offset + 1];
        offset += 2;

        if (velocity > 0) {
          const noteKey = `${pitch}-${channel}`;
          activeNotes.set(noteKey, { startTick: currentTick, pitch, velocity });
          if (DEBUG_MODE.MIDI_IMPORT) {
            console.log(`🎵 Note ON:  Pitch ${pitch}, Velocity ${velocity}, Tick ${currentTick}, Channel ${channel}`);
          }
        } else {
          // velocity 0 == Note Off
          const noteKey = `${pitch}-${channel}`;
          const activeNote = activeNotes.get(noteKey);
          if (activeNote) {
            const note = {
              startBeat: ticksToBeat(activeNote.startTick, ticksPerQuarter),
              endBeat: ticksToBeat(currentTick, ticksPerQuarter),
              pitch: activeNote.pitch,
              velocity: activeNote.velocity
            };
            track.notes.push(note);
            if (DEBUG_MODE.MIDI_IMPORT) {
              console.log(`🎵 Note OFF (vel=0): Pitch ${pitch}, Duration ${currentTick - activeNote.startTick} ticks`);
            }
            activeNotes.delete(noteKey);
          }
        }
        break;
      }

      case MIDI_EVENTS.NOTE_OFF: {
        if (offset + 2 > data.length) break;
        const offPitch = data[offset];
        // const offVelocity = data[offset + 1]; // unused
        offset += 2;

        const noteKey = `${offPitch}-${channel}`;
        const activeNote = activeNotes.get(noteKey);
        if (activeNote) {
          const note = {
            startBeat: ticksToBeat(activeNote.startTick, ticksPerQuarter),
            endBeat: ticksToBeat(currentTick, ticksPerQuarter),
            pitch: activeNote.pitch,
            velocity: activeNote.velocity
          };
          track.notes.push(note);
          if (DEBUG_MODE.MIDI_IMPORT) {
            console.log(`🎵 Note OFF: Pitch ${offPitch}, Duration ${currentTick - activeNote.startTick} ticks`);
          }
          activeNotes.delete(noteKey);
        }
        break;
      }

      case MIDI_EVENTS.PROGRAM_CHANGE: {
        if (offset + 1 > data.length) break;
        track.program = data[offset];
        if (DEBUG_MODE.MIDI_IMPORT) {
          console.log(`🎸 Program change: Channel ${channel}, Program ${track.program} (GM Instrument)`);
        }
        offset += 1;
        break;
      }

      case MIDI_EVENTS.CONTROL_CHANGE:
      case MIDI_EVENTS.POLY_PRESSURE:
      case MIDI_EVENTS.CHANNEL_PRESSURE: {
        // Skip these events (advance by appropriate number of data bytes)
        const bytesToSkip = (eventType === MIDI_EVENTS.CHANNEL_PRESSURE) ? 1 : 2;
        offset += Math.min(bytesToSkip, data.length - offset);
        break;
      }

      case MIDI_EVENTS.PITCH_BEND: {
        offset += Math.min(2, data.length - offset);
        break;
      }

      default: {
        // Unknown channel event, skip at most 2 data bytes to recover
        offset += Math.min(2, data.length - offset);
        break;
      }
    }
  }
  
  return track;
}

/**
 * Reads a variable-length quantity from MIDI data
 */
function readVLQ(data: Uint8Array, offset: number): { value: number; bytesRead: number } {
  let value = 0;
  let bytesRead = 0;
  let byte;
  
  do {
    if (offset + bytesRead >= data.length) {
      throw new Error('Invalid VLQ: Unexpected end of data');
    }
    
    byte = data[offset + bytesRead];
    bytesRead++;
    
    value = (value << 7) | (byte & 0x7F);
  } while (byte & 0x80);
  
  return { value, bytesRead };
}

/**
 * Reads a 16-bit unsigned integer (big-endian)
 */
function readUint16(data: Uint8Array, offset: number): number {
  if (offset + 2 > data.length) {
    throw new Error('Cannot read Uint16: Not enough data');
  }
  return (data[offset] << 8) | data[offset + 1];
}

/**
 * Reads a 32-bit unsigned integer (big-endian)
 */
function readUint32(data: Uint8Array, offset: number): number {
  if (offset + 4 > data.length) {
    throw new Error('Cannot read Uint32: Not enough data');
  }
  return (data[offset] << 24) | (data[offset + 1] << 16) | (data[offset + 2] << 8) | data[offset + 3];
}

/**
 * Converts MIDI ticks to beats
 */
function ticksToBeat(ticks: number, ticksPerQuarter: number): number {
  return ticks / ticksPerQuarter;
}

/**
 * Maps MIDI channel and program to KGSP instrument
 */
function getKGInstrumentFromMidi(channel: number, program?: number): InstrumentType {
  // Channel 9 (0-based) is always drums
  if (channel === 9) {
    return 'standard' as InstrumentType;
  }
  
  // Program is 0-127 in MIDI; STANDARD_MIDI_INSTRUMENT_MAP is 1-128
  if (program !== undefined) {
    const gmProgram = Math.max(1, Math.min(128, program + 1));
    const mapping = STANDARD_MIDI_INSTRUMENT_MAP[gmProgram as keyof typeof STANDARD_MIDI_INSTRUMENT_MAP];
    if (mapping) {
      return mapping.fluidR3InstrumentName as InstrumentType;
    }
  }
  
  // Default to acoustic grand piano
  return 'acoustic_grand_piano' as InstrumentType;
}

/**
 * Converts MIDI key signature to KGSP format
 */
function getKeySignatureFromMidi(sharpsFlats: number, majorMinor: number): KeySignature {
  // Handle signed byte
  const accidentals = sharpsFlats > 127 ? sharpsFlats - 256 : sharpsFlats;
  
  if (majorMinor === 0) {
    // Major keys
    switch (accidentals) {
      case 0: return 'C major';
      case 1: return 'G major';
      case 2: return 'D major';
      case 3: return 'A major';
      case 4: return 'E major';
      case 5: return 'B major';
      case 6: return 'F# major';
      case 7: return 'C# major';
      case -1: return 'F major';
      case -2: return 'Bb major';
      case -3: return 'Eb major';
      case -4: return 'Ab major';
      case -5: return 'Db major';
      case -6: return 'Gb major';
      case -7: return 'Cb major';
    }
  } else {
    // Minor keys
    switch (accidentals) {
      case 0: return 'A minor';
      case 1: return 'E minor';
      case 2: return 'B minor';
      case 3: return 'F# minor';
      case 4: return 'C# minor';
      case 5: return 'G# minor';
      case 6: return 'D# minor';
      case 7: return 'A# minor';
      case -1: return 'D minor';
      case -2: return 'G minor';
      case -3: return 'C minor';
      case -4: return 'F minor';
      case -5: return 'Bb minor';
      case -6: return 'Eb minor';
      case -7: return 'Ab minor';
    }
  }
  
  // Default to C major
  return 'C major';
}

/**
 * Groups MIDI notes into regions based on timing
 */
function groupNotesIntoRegions(notes: ParsedMidiNote[], timeSignature: TimeSignature): RegionData[] {
  if (notes.length === 0) return [];
  
  // Sort notes by start time
  const sortedNotes = [...notes].sort((a, b) => a.startBeat - b.startBeat);
  
  const regions: RegionData[] = [];
  const beatsPerBar = timeSignature.numerator;
  const regionLengthInBeats = beatsPerBar * 4; // 4 bars per region
  
  // Find the range of all notes
  const firstNoteBeat = Math.floor(sortedNotes[0].startBeat);
  const lastNoteBeat = Math.ceil(Math.max(...sortedNotes.map(n => n.endBeat)));
  
  // Create regions to cover all notes
  for (let regionStart = Math.floor(firstNoteBeat / regionLengthInBeats) * regionLengthInBeats; 
       regionStart < lastNoteBeat; 
       regionStart += regionLengthInBeats) {
    
    const regionEnd = regionStart + regionLengthInBeats;
    
    // Find notes that belong to this region
    const regionNotes = sortedNotes.filter(note => 
      note.startBeat >= regionStart && note.startBeat < regionEnd
    );
    
    if (regionNotes.length > 0) {
      regions.push({
        startBeat: regionStart,
        length: regionLengthInBeats,
        notes: regionNotes
      });
    }
  }
  
  return regions;
}
