import { NON_HARMONIC_GM_ENTRIES } from './generalMIDI';
import looksLikeDrums from './looksLikeDrums';
import removeDrumLanes from './removeDrumLanes';
import separateNotes from './separateNotes';
import { Note, ParsedMIDIFile } from './types';

type KeyCorrelations = {
  major: {
    0: number;
    1: number;
    2: number;
    3: number;
    4: number;
    5: number;
    6: number;
    7: number;
    8: number;
    9: number;
    10: number;
    11: number;
  };
  minor: {
    0: number;
    1: number;
    2: number;
    3: number;
    4: number;
    5: number;
    6: number;
    7: number;
    8: number;
    9: number;
    10: number;
    11: number;
  };
};

const defaultKeyCorrelations: KeyCorrelations = {
  major: {
    0: 0,
    1: 0,
    2: 0,
    3: 0,
    4: 0,
    5: 0,
    6: 0,
    7: 0,
    8: 0,
    9: 0,
    10: 0,
    11: 0,
  },
  minor: {
    0: 0,
    1: 0,
    2: 0,
    3: 0,
    4: 0,
    5: 0,
    6: 0,
    7: 0,
    8: 0,
    9: 0,
    10: 0,
    11: 0,
  },
};

const MAJOR_KEY_DISTRIBUTION = {
  0: 6.35,
  1: 2.23,
  2: 3.48,
  3: 2.33,
  4: 4.38,
  5: 4.09,
  6: 2.52,
  7: 5.19,
  8: 2.39,
  9: 3.66,
  10: 2.29,
  11: 2.88,
}

const MINOR_KEY_DISTRIBUTION = {
  0: 6.33,
  1: 2.68,
  2: 3.52,
  3: 5.38,
  4: 2.60,
  5: 3.53,
  6: 2.54,
  7: 4.75,
  8: 3.98,
  9: 2.69,
  10: 3.34,
  11: 3.17,
};

type KeyAtTime = { weights: KeyCorrelations; startTime: number };

const getAlignment = (
  x: typeof MAJOR_KEY_DISTRIBUTION,
  y: typeof MAJOR_KEY_DISTRIBUTION
) => {
    const xAvg = Object.values(x).reduce((a, b) => a + b, 0) / 12;
    const yAvg = Object.values(y).reduce((a, b) => a + b, 0) / 12;

    let top = 0;
    let bottomLeft = 0;
    let bottomRight = 0;
    for (let i = 0; i < 12; i++) {
      top += (y[i as keyof typeof y] - yAvg) * (x[i as keyof typeof x] - xAvg);

      bottomRight += (x[i as keyof typeof x] - xAvg) ** 2;
      bottomLeft += (y[i as keyof typeof y] - yAvg) ** 2;
    }

    return top / Math.sqrt(bottomLeft * bottomRight);
};

export const getNonPercussionNotes = (file: ParsedMIDIFile): Note[][] => {
  const notes: Note[][] = [];
  const noteLanes = separateNotes(file);
  const nonDrumNotes = removeDrumLanes(noteLanes);
  nonDrumNotes.forEach((lane) => notes.push(lane.notes));
  return notes;
}

const getKeyDistribution = (notes: Note[][]): typeof MINOR_KEY_DISTRIBUTION => {
  const distribution = {
    0: 0,
    1: 0,
    2: 0,
    3: 0,
    4: 0,
    5: 0,
    6: 0,
    7: 0,
    8: 0,
    9: 0,
    10: 0,
    11: 0,
  } as typeof MINOR_KEY_DISTRIBUTION;

  const lastNoteEnd = notes.reduce((a, b) => Math.max(...b.map((n) => n.offBeat), a), 0);

  const allNotes = notes.flat();

  notes.forEach((instrumentNotes) => {
    instrumentNotes.forEach((n) => {
      const pitchClass = (n.note % 12) as keyof typeof distribution;
      const length = n.offBeat - n.onBeat;

      const closeToStartMultiplier = n.onBeat < 16 ? 4 : 1;
      const closeToEndMultiplier = n.offBeat > lastNoteEnd - 16 ? 4 : 1;

      const coincidentNotes = allNotes.filter((n2) => n.onBeat === n2.onBeat);

      const highestNoteMultiplier =
        n.note === Math.max(...coincidentNotes.map((n) => n.note)) ? 2 : 1;

      const lowestNoteMultiplier =
        n.note === Math.min(...coincidentNotes.map((n) => n.note)) ? 2 : 1;

      distribution[pitchClass] +=
        length *
        closeToStartMultiplier *
        closeToEndMultiplier *
        highestNoteMultiplier *
        lowestNoteMultiplier;
    });
  });

  return distribution;
}

const getKey = (file: ParsedMIDIFile): KeyAtTime[] => {
  const result: KeyAtTime[] = [];

  const distribution = getKeyDistribution(getNonPercussionNotes(file));

  const keyCorrelations = { major: { ...defaultKeyCorrelations.major }, minor: { ...defaultKeyCorrelations.minor } };

  Object.keys(keyCorrelations.major).forEach((k) => {
    const root = Number(k);
    const expectedDistribution: {[key: string]: number} = {};
    for (let i = 0; i < 12; i ++) {
      expectedDistribution[i] = MAJOR_KEY_DISTRIBUTION[((i + 12 - root) % 12) as keyof typeof MAJOR_KEY_DISTRIBUTION];
    }
    keyCorrelations.major[root as keyof typeof keyCorrelations.major] = getAlignment(
      distribution as any as typeof MAJOR_KEY_DISTRIBUTION,
      expectedDistribution as any as typeof MAJOR_KEY_DISTRIBUTION
    );
  });

  Object.keys(keyCorrelations.minor).forEach((k) => {
    const root = Number(k);
    const expectedDistribution: { [key: string]: number } = {};
    for (let i = 0; i < 12; i++) {
      expectedDistribution[i] =
        MINOR_KEY_DISTRIBUTION[
          ((i + 12 - root) % 12) as keyof typeof MAJOR_KEY_DISTRIBUTION
        ];
    }
    keyCorrelations.minor[root as keyof typeof keyCorrelations.major] = getAlignment(
      expectedDistribution as any as typeof MAJOR_KEY_DISTRIBUTION,
      distribution as any as typeof MAJOR_KEY_DISTRIBUTION
    );
  });

  result.push({
    weights: keyCorrelations,
    startTime: 0,
  });

  return result;
}

// console.log(
//   getKey({
//     channels: [
//       {
//         instruments: [
//           {
//             notes: [
//               {
//                 note: 7,
//                 onBeat: 0,
//                 offBeat: 0.5,
//               },
//               {
//                 note: 7,
//                 onBeat: 0.5,
//                 offBeat: 1,
//               },
//               {
//                 note: 9,
//                 onBeat: 1,
//                 offBeat: 1.5,
//               },
//               {
//                 note: 11,
//                 onBeat: 1.5,
//                 offBeat: 2,
//               },
//               {
//                 note: 7,
//                 onBeat: 2,
//                 offBeat: 2.5,
//               },
//               {
//                 note: 11,
//                 onBeat: 2.5,
//                 offBeat: 3,
//               },
//               {
//                 note: 9,
//                 onBeat: 3,
//                 offBeat: 3.5,
//               },
//               {
//                 note: 2,
//                 onBeat: 3.5,
//                 offBeat: 4,
//               },
//             ],
//           },
//         ],
//       },
//     ],
//   } as any)[0].weights
// );

export default getKey;
