import { FLUIDR3_INSTRUMENT_MAP } from '../../constants/generalMidiConstants';
import type { KeySignature } from '../../core/KGProject';
import type { KGMidiNote } from '../../core/midi/KGMidiNote';
import type { KGMidiRegion } from '../../core/region/KGMidiRegion';
import type { InstrumentType } from '../../core/track/KGMidiTrack';
import type {
  SheetDisplayEvent,
  SheetMeasureMetric,
  SheetMeasureModel,
  SheetQuantization,
} from './sheetNotationTypes';

const SHEET_QUANTIZATION_OPTIONS = [
  '4', '4,3', '4,6', '4,12',
  '8', '8,6', '8,12', '8,24',
  '16', '16,12', '16,24', '16,48',
  '32', '32,24', '32,48', '32,96',
  '64', '64,48', '64,96', '64,192',
  '128', '128,96', '128,192', '128,384',
] as const;

const EPSILON = 1e-6;

export type SheetClef = 'treble' | 'bass' | 'percussion';

export interface BuildSheetNotationOptions {
  scope?: 'region' | 'track';
  region: KGMidiRegion;
  regions?: KGMidiRegion[];
  projectMaxBars?: number;
  timeSignature: { numerator: number; denominator: number };
  quantization: SheetQuantization;
}

interface WorkingEvent {
  keys: string[];
  startBeat: number;
  endBeat: number;
  isRest: boolean;
}

interface NormalizedNoteInput {
  pitch: number;
  startBeat: number;
  endBeat: number;
}

export function getSheetQuantizationOptions(): string[] {
  return [...SHEET_QUANTIZATION_OPTIONS];
}

export function projectKeySignatureToVexFlow(keySignature: KeySignature): string {
  const [tonic, quality] = keySignature.split(' ');
  return quality === 'minor' ? `${tonic}m` : tonic;
}

export function parseSheetQuantization(value: string): SheetQuantization {
  const [primaryText, subdivisionText] = value.split(',');
  const primary = Number.parseInt(primaryText, 10);
  const subdivision = Number.parseInt(subdivisionText ?? primaryText, 10);

  if (!Number.isFinite(primary) || primary <= 0 || !Number.isFinite(subdivision) || subdivision <= 0) {
    throw new Error(`Invalid sheet quantization value: ${value}`);
  }

  return {
    raw: value,
    primary,
    subdivision,
    stepBeats: 4 / subdivision,
  };
}

export function isDrumInstrument(instrument: InstrumentType): boolean {
  const key = String(instrument);
  return key === 'standard' || FLUIDR3_INSTRUMENT_MAP[key]?.group === 'PERCUSSION_KIT';
}

export function resolveSheetClef(
  notes: KGMidiNote[],
  instrument: InstrumentType,
  supportsPercussion = true
): SheetClef {
  if (isDrumInstrument(instrument)) {
    return supportsPercussion ? 'percussion' : 'treble';
  }

  if (notes.length === 0) {
    return 'treble';
  }

  const averagePitch = notes.reduce((sum, note) => sum + note.getPitch(), 0) / notes.length;
  return averagePitch >= 60 ? 'treble' : 'bass';
}

export function buildSheetMeasureMetrics(measures: SheetMeasureModel[], widths: number[]): SheetMeasureMetric[] {
  let leftPx = 0;
  return measures.map((measure, index) => {
    const widthPx = widths[index] ?? 0;
    const metric: SheetMeasureMetric = {
      barIndex: measure.barIndex,
      startBeat: measure.startBeat,
      endBeat: measure.endBeat,
      leftPx,
      widthPx,
    };
    leftPx += widthPx;
    return metric;
  });
}

export function getSheetPlayheadPixel(
  sheetBeat: number,
  metrics: SheetMeasureMetric[]
): number {
  if (metrics.length === 0) {
    return 0;
  }

  if (sheetBeat <= metrics[0].startBeat) {
    return metrics[0].leftPx;
  }

  const lastMetric = metrics[metrics.length - 1];
  if (sheetBeat >= lastMetric.endBeat) {
    return lastMetric.leftPx + lastMetric.widthPx;
  }

  const activeMetric = metrics.find(metric => sheetBeat >= metric.startBeat && sheetBeat < metric.endBeat);
  if (!activeMetric) {
    return 0;
  }

  const span = Math.max(activeMetric.endBeat - activeMetric.startBeat, EPSILON);
  const progress = (sheetBeat - activeMetric.startBeat) / span;
  return activeMetric.leftPx + activeMetric.widthPx * progress;
}

export function getSheetBeatAtPixel(
  pixel: number,
  metrics: SheetMeasureMetric[]
): number {
  if (metrics.length === 0) {
    return 0;
  }

  const firstMetric = metrics[0];
  if (pixel <= firstMetric.leftPx) {
    return firstMetric.startBeat;
  }

  const lastMetric = metrics[metrics.length - 1];
  if (pixel >= lastMetric.leftPx + lastMetric.widthPx) {
    return lastMetric.endBeat;
  }

  const activeMetric = metrics.find(metric => pixel >= metric.leftPx && pixel < metric.leftPx + metric.widthPx);
  if (!activeMetric) {
    return lastMetric.endBeat;
  }

  const progress = activeMetric.widthPx > 0 ? (pixel - activeMetric.leftPx) / activeMetric.widthPx : 0;
  return activeMetric.startBeat + progress * (activeMetric.endBeat - activeMetric.startBeat);
}

export function resolveDurationSpec(durationBeats: number, isRest: boolean): { duration: string; dots: number } {
  const withRest = (value: string) => (isRest ? `${value}r` : value);
  const options = [
    { beats: 6, duration: 'w', dots: 1 },
    { beats: 4, duration: 'w', dots: 0 },
    { beats: 3, duration: 'h', dots: 1 },
    { beats: 2, duration: 'h', dots: 0 },
    { beats: 1.5, duration: 'q', dots: 1 },
    { beats: 1, duration: 'q', dots: 0 },
    { beats: 0.75, duration: '8', dots: 1 },
    { beats: 0.5, duration: '8', dots: 0 },
    { beats: 0.375, duration: '16', dots: 1 },
    { beats: 0.25, duration: '16', dots: 0 },
    { beats: 0.1875, duration: '32', dots: 1 },
    { beats: 0.125, duration: '32', dots: 0 },
    { beats: 0.09375, duration: '64', dots: 1 },
    { beats: 0.0625, duration: '64', dots: 0 },
  ];

  const match = options.find(option => Math.abs(durationBeats - option.beats) < EPSILON);
  if (match) {
    return { duration: withRest(match.duration), dots: match.dots };
  }

  if (durationBeats >= 4 - EPSILON) return { duration: withRest('w'), dots: 0 };
  if (durationBeats >= 2 - EPSILON) return { duration: withRest('h'), dots: 0 };
  if (durationBeats >= 1 - EPSILON) return { duration: withRest('q'), dots: 0 };
  if (durationBeats >= 0.5 - EPSILON) return { duration: withRest('8'), dots: 0 };
  if (durationBeats >= 0.25 - EPSILON) return { duration: withRest('16'), dots: 0 };
  if (durationBeats >= 0.125 - EPSILON) return { duration: withRest('32'), dots: 0 };
  return { duration: withRest('64'), dots: 0 };
}

export function buildSheetMeasureModels({
  scope = 'region',
  region,
  regions = [region],
  projectMaxBars,
  timeSignature,
  quantization,
}: BuildSheetNotationOptions): SheetMeasureModel[] {
  const beatsPerBar = timeSignature.numerator;
  const isTrackScope = scope === 'track';
  const timelineStartBeat = isTrackScope ? 0 : 0;
  const measureCount = isTrackScope
    ? Math.max(1, projectMaxBars ?? 1)
    : Math.max(1, Math.ceil(region.getLength() / beatsPerBar));
  const measureEndBeat = isTrackScope
    ? measureCount * beatsPerBar
    : measureCount * beatsPerBar;
  const noteInputs = isTrackScope
    ? collectTrackScopeNotes(regions)
    : region.getNotes().map(note => ({
        pitch: note.getPitch(),
        startBeat: note.getStartBeat(),
        endBeat: note.getEndBeat(),
      }));
  const workingEvents = normalizeNotes(noteInputs, quantization.stepBeats, measureEndBeat);
  const withRests = insertRests(workingEvents, measureEndBeat);
  const splitEvents = splitAcrossBars(withRests, beatsPerBar);

  const measures: SheetMeasureModel[] = Array.from({ length: measureCount }, (_, barIndex) => ({
    barIndex,
    startBeat: timelineStartBeat + barIndex * beatsPerBar,
    endBeat: timelineStartBeat + (barIndex + 1) * beatsPerBar,
    events: [],
  }));

  splitEvents.forEach(event => {
    const barIndex = Math.min(measures.length - 1, Math.max(0, Math.floor(event.startBeat / beatsPerBar)));
    measures[barIndex].events.push(event);
  });

  measures.forEach((measure) => {
    if (measure.events.length === 0) {
      measure.events.push({
        keys: ['b/4'],
        startBeat: measure.startBeat,
        endBeat: measure.endBeat,
        isRest: true,
        tieStart: false,
        tieEnd: false,
      });
    }
  });

  return measures;
}

function normalizeNotes(notes: NormalizedNoteInput[], stepBeats: number, measureEndBeat: number): WorkingEvent[] {
  const clippedNotes = notes
    .map(note => ({
      keys: [midiPitchToVexKey(note.pitch)],
      startBeat: quantizeBeat(note.startBeat, stepBeats),
      endBeat: quantizeBeat(note.endBeat, stepBeats),
      isRest: false,
    }))
    .map(note => ({
      ...note,
      startBeat: clampBeat(note.startBeat, 0, measureEndBeat),
      endBeat: clampBeat(Math.max(note.endBeat, note.startBeat + stepBeats), 0, measureEndBeat),
    }))
    .filter(note => note.endBeat - note.startBeat > EPSILON)
    .sort((a, b) => {
      if (a.startBeat !== b.startBeat) return a.startBeat - b.startBeat;
      if (a.endBeat !== b.endBeat) return a.endBeat - b.endBeat;
      return a.keys[0].localeCompare(b.keys[0]);
    });

  const merged: WorkingEvent[] = [];

  for (const note of clippedNotes) {
    const previous = merged[merged.length - 1];
    if (
      previous &&
      !previous.isRest &&
      Math.abs(previous.startBeat - note.startBeat) < EPSILON &&
      Math.abs(previous.endBeat - note.endBeat) < EPSILON
    ) {
      previous.keys.push(...note.keys);
      continue;
    }

    merged.push(note);
  }

  for (let index = 0; index < merged.length - 1; index += 1) {
    const current = merged[index];
    const next = merged[index + 1];
    if (current.endBeat > next.startBeat + EPSILON) {
      current.endBeat = Math.max(current.startBeat + stepBeats, next.startBeat);
    }
  }

  return merged.filter(note => note.endBeat - note.startBeat > EPSILON);
}

function collectTrackScopeNotes(regions: KGMidiRegion[]): NormalizedNoteInput[] {
  return [...regions]
    .sort((left, right) => {
      if (left.getStartFromBeat() !== right.getStartFromBeat()) {
        return left.getStartFromBeat() - right.getStartFromBeat();
      }

      return left.getId().localeCompare(right.getId());
    })
    .flatMap(region => {
      const regionStart = region.getStartFromBeat();
      // Overlapping regions are flattened in deterministic start-beat/id order so
      // the existing note normalization path can resolve collisions consistently.
      return region.getNotes().map(note => ({
        pitch: note.getPitch(),
        startBeat: regionStart + note.getStartBeat(),
        endBeat: regionStart + note.getEndBeat(),
      }));
    });
}

function insertRests(events: WorkingEvent[], measureEndBeat: number): WorkingEvent[] {
  if (events.length === 0) {
    return [{ keys: ['b/4'], startBeat: 0, endBeat: measureEndBeat, isRest: true }];
  }

  const result: WorkingEvent[] = [];
  let cursorBeat = 0;

  for (const event of events) {
    if (event.startBeat > cursorBeat + EPSILON) {
      result.push({
        keys: ['b/4'],
        startBeat: cursorBeat,
        endBeat: event.startBeat,
        isRest: true,
      });
    }

    result.push(event);
    cursorBeat = event.endBeat;
  }

  if (cursorBeat < measureEndBeat - EPSILON) {
    result.push({
      keys: ['b/4'],
      startBeat: cursorBeat,
      endBeat: measureEndBeat,
      isRest: true,
    });
  }

  return result;
}

function splitAcrossBars(events: WorkingEvent[], beatsPerBar: number): SheetDisplayEvent[] {
  const result: SheetDisplayEvent[] = [];

  for (const event of events) {
    let segmentStart = event.startBeat;
    const eventEnd = event.endBeat;

    while (segmentStart < eventEnd - EPSILON) {
      const currentBar = Math.floor(segmentStart / beatsPerBar);
      const barEnd = (currentBar + 1) * beatsPerBar;
      const segmentEnd = Math.min(eventEnd, barEnd);

      result.push({
        keys: [...event.keys],
        startBeat: segmentStart,
        endBeat: segmentEnd,
        isRest: event.isRest,
        tieStart: !event.isRest && segmentStart > event.startBeat + EPSILON,
        tieEnd: !event.isRest && segmentEnd < eventEnd - EPSILON,
      });

      segmentStart = segmentEnd;
    }
  }

  return result;
}

function quantizeBeat(beat: number, stepBeats: number): number {
  return Math.round(beat / stepBeats) * stepBeats;
}

function clampBeat(value: number, min: number, max: number): number {
  return Math.max(min, Math.min(max, value));
}

function midiPitchToVexKey(pitch: number): string {
  const semitone = ((pitch % 12) + 12) % 12;
  const octave = Math.floor(pitch / 12) - 1;
  const names = ['c', 'c#', 'd', 'd#', 'e', 'f', 'f#', 'g', 'g#', 'a', 'a#', 'b'];
  return `${names[semitone]}/${octave}`;
}
