import { NoteSpan, filterV1 } from "./filterSpans";
import { NON_HARMONIC_GM_ENTRIES } from "./generalMIDI";
import getKey from './getKey';
import looksLikeDrums from "./looksLikeDrums";
import removeDrumLanes from './removeDrumLanes';
import separateNotes from './separateNotes';
import { HighlightSpec, Note, ParsedMIDIFile } from "./types";

// this X average note length = minimum allowable length of a span
const MIN_HIGHLIGHT_NOTES = 8;
const MAX_HIGHLIGHT_GAP = 6;
const MIN_HIGHLIGHT_LENGTH = 6;
const QUANTIZE_TO = 0;
const EXAMPLE_BARS = 5;
const STEP_BARS = 4;
const NUM_SIMULTANEOUS_NOTES = 4;

const quantizeToSixteenthsOrTriplets = (beatsMod1: number) => {
  const options = [0, 0.25, 0.33, 0.5, 0.66, 0.75, 1];

  const quantized = options.reduce((prev, curr) => {
    return Math.abs(curr - beatsMod1) < Math.abs(prev - beatsMod1)
      ? curr
      : prev;
  }, 0);
  return quantized;
};

const getQuantizeAdjustmentAmount = (notes: Note[]) => {
  let amount = 0;
  for (let i = 0; i < notes.length; i++) {
    const beatsMod1 = notes[i].onBeat % 1;
    amount += Math.abs(beatsMod1 - quantizeToSixteenthsOrTriplets(beatsMod1));
  }
  return amount;
};

const getExampleRejectionReason = (example: Note[]) => {
  for (let i = 0; i < example.length; i++) {
    if (example[i].offBeat < example[i].onBeat) {
      return "negativeNoteLength";
    }
    if (
      !Number.isFinite(example[i].onBeat) ||
      !Number.isFinite(example[i].offBeat)
    ) {
      return "nonFiniteNote";
    }
  }

  if (example.length < 10) {
    return "noteCount";
  }

  const longestNote = Math.max(...example.map((n) => n.offBeat - n.onBeat));
  if (!Number.isFinite(longestNote)) {
    debugger;
  }

  const averageNote =
    example.reduce((a, c) => a + (c.offBeat - c.onBeat), 0) / example.length;
  const longestNoteRatio = longestNote / averageNote;

  if (longestNoteRatio > 8) {
    return "noteLengths";
  }

  let numShortNotes = 0;
  for (let i = 0; i < example.length; i ++) {
    if (example[i].offBeat - example[i].onBeat < 0.1) {
      numShortNotes++;
    }
  }

  if (numShortNotes > example.length / 2) {
    return "shortNotes";
  }

  const quantizeAdjustmentAmountPerNote =
    getQuantizeAdjustmentAmount(example) / example.length;

  if (quantizeAdjustmentAmountPerNote > 0.05) {
    return "quantize";
  }

  let numNotesOnTriplet = 0;
  let numNotesOnSixteenth = 0;

  for (let i = 0; i < example.length; i++) {
    const quantized = quantizeToSixteenthsOrTriplets(example[i].onBeat % 1);
    if (quantized === 0.33 || quantized === 0.66) {
      numNotesOnTriplet++;
    }
    if (quantized === 0.25 || quantized === 0.75) {
      numNotesOnSixteenth++;
    }
  }

  if (numNotesOnTriplet > 0 && numNotesOnSixteenth > 0) {
    return "tripletAndSixteenth";
  }

  return null;
};

const getHighlightExamples = (notes: Note[], highlight: HighlightSpec) => {
  const hnotes = notes.filter(
    (n) =>
      n.onBeat >= highlight.start &&
      n.offBeat <= highlight.end &&
      n.offBeat > n.onBeat
  );
  const res = [];
  const latestBarStart = Math.floor(highlight.start / 4) * 4;
  for (
    let pos = latestBarStart;
    pos <= highlight.end - 4 * EXAMPLE_BARS;
    // although the examples are 5 bars long, we want them to start every 4 bars to have a good chance of musical sensibility
    pos += 4 * STEP_BARS
  ) {
    const maybeExample = hnotes.filter(
      (n) => n.onBeat >= pos && n.offBeat < pos + 4 * EXAMPLE_BARS
    );
    res.push(maybeExample);
  }

  return res;
};

const getExampleStart = (example: Note[]) => {
  const earliestNoteStart = Math.min(...example.map((n) => n.onBeat));
  return earliestNoteStart - (earliestNoteStart % 4);
}

const quantizeExample = (example: Note[]) => {
  return example.map((n) => {
    const noteOn =
      Math.floor(n.onBeat) + quantizeToSixteenthsOrTriplets(n.onBeat % 1);
    const noteOff = Math.max(
      noteOn + 0.08,
      Math.floor(n.offBeat) + quantizeToSixteenthsOrTriplets(n.offBeat % 1)
    );
    return {
      ...n,
      onBeat: noteOn,
      offBeat: noteOff,
    };
  });
}

const shiftExample = (example: Note[], amount: number) => {
  return example.map((n) => {
    return {
      ...n,
      onBeat: n.onBeat + amount,
      offBeat: n.offBeat + amount,
    };
  });
}

export const getFileExamples = (
  file: ParsedMIDIFile,
  highlights: HighlightSpec[]
) => {
  const res = [];
  for (const highlight of highlights) {
    res.push(
      ...getHighlightExamples(
        file.channels[highlight.channel].instruments[highlight.instrument]
          .notes,
        highlight
      ).map((e) => quantizeExample(e))
    );
  }

  const keyCorrelationsByTime = getKey(file);

  return res.map((example, index, list) => {
    const exampleStart = getExampleStart(example);
    const accompaniments = list.filter((otherExample) => {
      if (otherExample === example) return false;
      const otherExampleStart = getExampleStart(otherExample);
      if (otherExampleStart !== exampleStart) {
        return false;
      }
      return !getExampleRejectionReason(otherExample);
    });
    return {
      keys: keyCorrelationsByTime,
      accompaniments: accompaniments.map((a) => shiftExample(a, -exampleStart)),
      example: shiftExample(example, -exampleStart),
      rejection_reason: getExampleRejectionReason(example),
    };
  });
};

const getHighlights = (file: ParsedMIDIFile): HighlightSpec[] => {
  const result: HighlightSpec[] = [];
  const beatNumerator = Number(file.time_signature.split("/")[0]);

  // if (file.channels.length === 1 && file.tracks.length > 1) {
  //   // weird drum tracks
  //   return [];
  // }

  const separatedNotes = separateNotes(file);
  const nonDrumNotes = removeDrumLanes(separatedNotes);
  nonDrumNotes.forEach((noteLane, index) => {
    let minTime = Infinity;
    let maxTime = -Infinity;
    const notes = noteLane.notes;
    const moments: { [key: string]: Note[] } = {};

    for (let i = 0; i < notes.length; i++) {
      const note = notes[i];

      const quantizedOn = QUANTIZE_TO
        ? Math.round(note.onBeat / QUANTIZE_TO) * QUANTIZE_TO
        : note.onBeat;
      const quantizedOff = QUANTIZE_TO
        ? Math.round(note.offBeat / QUANTIZE_TO) * QUANTIZE_TO
        : note.offBeat;

      if (quantizedOn < minTime) minTime = quantizedOn;
      if (quantizedOff > maxTime) maxTime = quantizedOff;
      if (!moments[quantizedOn]) {
        moments[quantizedOn] = [];
      }
      if (!moments[quantizedOff]) {
        moments[quantizedOff] = [];
      }
    }

    const sortedMoments = Object.keys(moments).sort(
      (a, b) => Number(a) - Number(b)
    );

    const spans = [];

    let currentSpan: NoteSpan = {
      start: 0,
      end: minTime,
      length: minTime,
      notes: [],
    };

    sortedMoments.forEach((k) => {
      currentSpan.end = Number(k);
      currentSpan.length = currentSpan.end - currentSpan.start;
      const overlappingNotes = notes.filter(
        (n) => n.onBeat <= Number(k) && n.offBeat > Number(k)
      );
      spans.push(currentSpan);
      currentSpan = {
        start: Number(k),
        end: Number(k),
        length: 0,
        notes: overlappingNotes,
      };
    });

    currentSpan.end = maxTime;
    currentSpan.length = currentSpan.end - currentSpan.start;

    spans.push(currentSpan);

    /*
      spans is now a big list of every distinct set of notes that are playing at the same time.
      if we play a triad and hold one of the notes a little longer,  we'd get:
      [
        { start: 0, end: 1, notes: [a, b, c] },
        { start: 1, end: 1.1, notes: [b] }
      ]
    */

    // now let's filter out spans that contain only trivial overlaps.
    // the intention here is to leave in any spans that contain long held groups of notes, but remove spans that just contain a subtle legato or something.
    const filteredSpans = filterV1(spans);

    const highlightsForTrack: HighlightSpec[] = [];

    // now, for each continuous block of spans where there's one or zero notes playing, we'll add a highlight
    let currentStreakStart = -1;
    let currentStreakEnd = -1;
    let notesInCurrentStreak = 0;
    for (let i = 0; i <= filteredSpans.length; i++) {
      const span = filteredSpans[i];
      if (
        (!span ||
          span.notes.length > NUM_SIMULTANEOUS_NOTES ||
          (span.notes.length === 0 && span.length >= MIN_HIGHLIGHT_LENGTH)) &&
        currentStreakStart > -1
      ) {
        const start = filteredSpans[currentStreakStart].start;
        const end = filteredSpans[currentStreakEnd].end;
        const length = end - start;
        if (
          length >= beatNumerator &&
          notesInCurrentStreak >= MIN_HIGHLIGHT_NOTES
        ) {
          highlightsForTrack.push({
            channel: noteLane.channel,
            instrument: noteLane.instrument,
            track: noteLane.track,
            start: filteredSpans[currentStreakStart].start,
            end: filteredSpans[currentStreakEnd].end,
          });
        }
        currentStreakStart = -1;
        notesInCurrentStreak = 0;
      } else if (
        span &&
        span.notes.length <= NUM_SIMULTANEOUS_NOTES &&
        span.notes.length > 0 &&
        currentStreakStart === -1
      ) {
        currentStreakStart = i;
      }

      if (currentStreakStart !== -1) {
        notesInCurrentStreak += span.notes.length;
      }

      if (
        span &&
        span.notes.length <= NUM_SIMULTANEOUS_NOTES &&
        span.notes.length > 0
      ) {
        currentStreakEnd = i;
      }
    }

    let lastHighightEnd = -Infinity;
    const groupedHighlights: HighlightSpec[] = [];
    highlightsForTrack.forEach((h, i) => {
      if (i > 0 && h.start - lastHighightEnd <= MAX_HIGHLIGHT_GAP) {
        groupedHighlights.splice(groupedHighlights.length - 1, 1);
        groupedHighlights.push({
          channel: noteLane.channel,
          instrument: noteLane.instrument,
          track: noteLane.track,
          start: highlightsForTrack[i - 1].start,
          end: h.end,
        });
      } else {
        groupedHighlights.push(h);
      }
      lastHighightEnd = h.end;
    });

    groupedHighlights.forEach((h) => {
      result.push(h);
    });
  });

  return result;
};

export default getHighlights;
