import { MODIFIERS, NOTES_TO_INTERVALS, getScale, getScaleLength } from "./defaults";
import { isScale, CHORDS, CIRCLE_OF_FIFTHS, INTERVALS_TO_NOTES, ROMANS } from "./defaults";
import { parse as parseScala } from "./parser/scalaParser";
import { Pitch } from "./types";
import { safeMod } from "./utils";

// TODO: Change parameter to this type
export type PitchType = {
  pitch?: number,
  originalPitch?: number,
  octave?: number,
  addedOctave?: number,
  pitchOctave?: number,
  modifier?: number,
  key?: number,
  scaleName?: string,
  parsedScale?: number[],
  degrees?: boolean
}

export const resolvePitchClass = (
  root: number | string,
  pitchClass: number,
  scale: string | Array<number>,
  octave: number = 0,
  modifier: number = 0,
  degrees: boolean = false
): {note: number, pitch: number, octave: number, pitchOctave: number, pitchBend?: number, modifier: number, root: number, parsedScale: number[]} => {
  // Initialization
  pitchClass = degrees && pitchClass > 0 ? pitchClass - 1 : pitchClass;
  root = typeof root === 'string' ? noteNameToMidi(root) : root;
  const intervals = typeof scale === 'string' ? safeScale(scale) : scale;
  const scale_length = intervals.length;
  let pitchOctave = 0;

  // Resolve pitch classes to the scale and calculate octave
  if (pitchClass >= scale_length || pitchClass < 0) {
    pitchOctave += Math.floor(pitchClass / scale_length);
    pitchClass = pitchClass < 0 ? scale_length - (Math.abs(pitchClass) % scale_length) : pitchClass % scale_length;
    if (pitchClass === scale_length) {
      pitchClass = 0;
    }
  }

  // Computing the result
  let note = root + intervals.slice(0, pitchClass).reduce((a, b) => a + b, 0);
  note = note + (octave + pitchOctave) * intervals.reduce((a, b) => a + b, 0) + modifier;

  if (Number.isInteger(note)) {
    return {note, pitch: pitchClass, octave, pitchOctave: pitchOctave, modifier, root, parsedScale: intervals}    
  } else {
    const bend = resolvePitchBend(note);
    return {note, pitch: pitchClass, octave, pitchOctave: pitchOctave, pitchBend: bend[1], modifier, root, parsedScale: intervals}
  }
  
}

export const noteFromPc = (
    root: number | string,
    pitch_class: number,
    scale: string | Array<number>,
    octave: number = 0,
    modifier: number = 0,
    degrees: boolean = false
  ): [number, number | undefined] => {
    const result = resolvePitchClass(root, pitch_class, scale, octave, modifier, degrees);
    return [result.note, result.pitchBend];
}

export const noteNameToMidi = (name: string, defaultOctave: number = 4): number => {
  const items = name.match(/^([a-gA-G])([#bs])?([1-9])?$/);
  if (items === null) {
    return 60; // Default MIDI note C4 if the input is invalid
  }
  const [, noteName, modifierSymbol, octaveStr] = items;
  const octave = octaveStr ? parseInt(octaveStr, 10) : defaultOctave;
  const modifier = MODIFIERS[modifierSymbol] || 0;
  const interval = NOTES_TO_INTERVALS[noteName.toUpperCase()];
  return 12 + octave * 12 + interval + modifier;
}

export const resolvePitchBend = (note_value: number, semitones: number = 1): [number, number] => {
  let midi_bend_value = 8192;
  if (note_value % 1 !== 0) {
    const start_value = note_value > Math.round(note_value) ? note_value : Math.round(note_value);
    const end_value = note_value > Math.round(note_value) ? Math.round(note_value) : note_value;
    const bend_diff = midiToFreq(start_value) / midiToFreq(end_value);
    const bend_target = 1200 * Math.log2(bend_diff);
    // https://www.cs.cmu.edu/~rbd/doc/cmt/part7.html
    midi_bend_value = midi_bend_value + Math.floor(8191 * (bend_target / (100 * semitones)));
  }
  return [note_value, midi_bend_value];
}

export const midiToFreq = (note: number): number => {
  const freq = 440  // Frequency of A
  return (freq / 32) * (2 ** ((note - 9) / 12))
}

export const freqToMidi = (freq: number): number => {
  return (12 / Math.log(2)) * Math.log(freq / 440) + 69
}

export const ratioToCents = (ratio: number): number => {
  return 1200.0 * Math.log2(ratio);
}

export const primeSieve = function* () {
  const sieve: { [key: number]: number[] } = {};
  let current = 2;

  while (true) {
      if (!(current in sieve)) {
          yield current;
          sieve[current * current] = [current];
      } else {
          for (const composite of sieve[current]) {
              (sieve[composite + current] ||= []).push(composite);
          }
          delete sieve[current];
      }

      current++;
  }
}

export const getPrimes = (n: number): number[] => {
  const primeGenerator: Generator<number> = primeSieve();
  return Array.from({ length: n }, () => primeGenerator.next().value);
}

export const monzoToCents = (monzo: number[]): number => {
  const maxIndex = monzo.length;
  const primes = getPrimes(maxIndex+1);

  let ratio = 1;
  for (let i = 0; i < maxIndex; i++) {
      ratio *= Math.pow(primes[i], monzo[i]);
  }

  return ratioToCents(ratio);

}

export const centsToSemitones = (cents: number[]): number[] => {
  if (cents[0] !== 0) {
      cents = [0, ...cents];
  }
  const semitoneScale: number[] = [];
  for (let i = 0; i < cents.length - 1; i++) {
      const semitoneInterval = (cents[i + 1] - cents[i]) / 100;
      semitoneScale.push(semitoneInterval);
  }
  return semitoneScale;
}

export const ratiosToCents = (ratios: number[]): number[] => {
  return ratios.map(ratioToCents);
}

export const ratiosToSemitones = (ratios: number[]): number[] => {
  return centsToSemitones(ratiosToCents(ratios));
}

export const scaleLength = (scale: string|number[]): number => {
  return typeof scale === 'string' ? getScale(scale).length : scale.length;
}

export const stepsToScale = (steps: number[]) => {
  return [0,...steps].reduce((scale: number[], step: number, index: number) => {
    const value = index === 0 ? 0 : step + scale[index-1];
    return [...scale, value];
  }, []);
}

export const scaleToSteps = (pcs: number[]): number[] => {
  const pc_int = (a: number, b: number): number => {
    const r = (b - a) % 12;
    return r < 0 ? r+12 : r;
  }
  return pcs.map((pc, i) => pc_int(pc, pcs[(i + 1) % pcs.length]));
}

export const numberToScale = (number: number): number[] => {
  if (number < 0 || number > 4095) {
    console.log("Input number must be odd and between 0 and 4095. Using major (2741) instead.");
    number = 2741;
  }
  if(number % 2 === 0) {
    console.log("Even numbers doesnt create a 'real' scale");
  }
  const arr = (number >>> 0).toString(2).padStart(12, '0').split('');
  return arr.reduce((acc, bit, i) => bit === '1' ? [11 - i, ...acc] : acc, [] as number[]);
}

export function scaleToNumber(pcs: number[]): number {
  if (pcs.length > 0 && pcs[pcs.length - 1] === 12) {
    pcs.pop(); // Remove the last value if 12
  }
  let number = 0;
  for (const pc of pcs) {
    number |= (1 << pc);
  }
  return number;
}

export const parseScalaScale = (scala: string): number[] => {
  try {
    return parseScala(scala) as number[];
  } catch (error) {
    return [];
  }
}

export const safeScale = (scale: string|number|number[]): number[] => {
  if(typeof scale === 'string') {
    if(isScale(scale)) {
      return getScale(scale);
    } else {
      const scalaScale = parseScalaScale(scale) as number[];
      if(scalaScale && scalaScale.length > 0) {
        return scalaScale;
      } else {
        return getScale('MAJOR');
      }
    }
  } else if(typeof scale === 'number') {
    return scaleToSteps(numberToScale(scale));
  }
  // TODO: Check for valid intervals?
  return scale;
}

export const edoToCents = (edo: number, intervals: string|number[] = new Array(edo).fill(1))  => {
  intervals = safeScale(intervals);
  const centsInSemitone = (12 / edo) * 100;
  const scale = stepsToScale(intervals);
  return scale.map((pc) => pc * centsInSemitone);
}

export const edoToSemitones = (edo: number, intervals: string|number[] = new Array(edo).fill(1))  => {
  const scaleInCents = edoToCents(edo, intervals);
  return centsToSemitones(scaleInCents);
}

export const namedChordFromDegree = (
  degree: number,
  name: string = "major",
  root: number|string = 60,
  scale: string = "CHROMATIC",
  numOctaves: number = 1
): number[] => {
  const intervals: number[] = CHORDS[name] || CHORDS["major"];
  root = typeof root === "string" ? noteNameToMidi(root) : root;
  const scaleDegree: number = getScaleNotes(scale, root)[degree - 1];
  const notes: number[] = [];

  for (let curOct = 0; curOct <= numOctaves; curOct++) {
    for (const interval of intervals) {
      notes.push(scaleDegree + interval + curOct * 12);
    }
  }
   return notes;
}

export const getPitchesFromNamedChord = (
  name: string = "major",
  root: number|string = 60,
  scale: string = "MAJOR",
  numOctaves: number = 1,
  duration: number
): Pitch[] => {
  const notes = namedChordFromDegree(1, name, root, "CHROMATIC", numOctaves);
  const parsedScale = typeof scale === "string" ? getScale(scale) : scale;
  const pitches: Pitch[] = notes.map(note => {
    const pitch = midiToPitchClass(note, root, scale);
    return new Pitch({text: pitch.text, note: note, pitch: pitch.pc, octave: pitch.octave, add: pitch.add, duration: duration, scaleName: scale, parsedScale: parsedScale, key: root});
  });
  return pitches;
}

export const getScaleNotes = (
  name: string|number|number[], 
  root: number|string = 60, 
  numOctaves: number = 1
  ): number[] => {
  const scale = safeScale(name);
  let scaleRoot = typeof root === "string" ? noteNameToMidi(root) : root;
  const scaleNotes: number[] = [scaleRoot];

  for (let i = 0; i < numOctaves; i++) {
    for (const semitone of scale) {
      scaleRoot += semitone;
      scaleNotes.push(scaleRoot);
    }
  }

  return scaleNotes;
}

/* Get all scale notes, defaults for full sized keyboard from 21 to 108 */
export const getAllScaleNotes = (
  name: string|number|number[],
  key: string|number = "C",
  from: number = 21,
  to: number = 108
): number[] => {
  const scale = safeScale(name);
  const scaleNotes: number[] = [];
  let scaleRoot = typeof key === "string" ? noteNameToMidi(key, 0) : key;
  for (let i = 0; i < 9; i++) {
    for (const semitone of scale) {
      scaleRoot += semitone;
      scaleNotes.push(scaleRoot);
    }
  }
  return scaleNotes.filter(note => note >= from && note <= to);
}

export const chordFromDegree = (
  degree: number,
  scale: string = "MAJOR",
  root: string | number = 60,
  numOctaves: number = 1,
  name: string | undefined = undefined,
): number[] => {
  const rootMidi: number = typeof root === "string" ? noteNameToMidi(root) : root;
  if (
    !name &&
    typeof scale === "string" &&
    scale.toUpperCase() === "CHROMATIC"
  ) { name = "major"; }

  if (name) {
    return namedChordFromDegree(degree, name, rootMidi, scale, numOctaves);
  } else {
    return getChordFromScale(degree, rootMidi, scale);
  }
}

export const getChordFromScale = (
  degree: number,
  root: number = 60,
  scale: string | number[] = "Major",
  numNotes: number = 3,
  skip: number = 2
): number[] => {
  const scaleLength: number = typeof scale === "string" ? getScaleLength(scale) : scale.length;

  const numOctaves: number = Math.floor((numNotes * skip + degree - 1) / scaleLength) + 1;
  const scaleNotes: number[] = getScaleNotes(scale, root, numOctaves);

  const chord: number[] = [];

  for (let i = degree - 1; chord.length < numNotes && i < scaleNotes.length; i += skip) {
    chord.push(scaleNotes[i]);
  }

  return chord;
}

export const chord = (name: string): number[] => {
  // Parse chord name from notation scientific notation + chord name
  // For example Cmaj or C7
  const parsedChord = name.match(/([a-gA-G][#bs]?)([0-9])?([a-zA-Z0-9]+)/);
  if (parsedChord === null) {
    // C major chord by default
    return [60, 64, 67];
  }
  let [, root, oct, chordName] = parsedChord;
  const rootMidi = noteNameToMidi(root);
  const octave = oct ? parseInt(oct, 10) : 0;
  const namedChord = namedChordFromDegree(1, chordName, rootMidi, "CHROMATIC", octave);	
  return namedChord;
}

export const parseRoman = (numeral: string): number => {
  const values: number[] = numeral.split('').map(val => ROMANS[val]);

  return values.reduce((result, current, index, array) => {
    if (index < array.length - 1 && current < array[index + 1]) {
      return result - current;
    } else {
      return result + current;
    }
  }, 0);
}

export const accidentalsFromNoteName = (name: string): number => {
  if (!CIRCLE_OF_FIFTHS.includes(name)) {
    name = midiToNoteName(noteNameToMidi(name));
  }

  const idx: number = CIRCLE_OF_FIFTHS.indexOf(name);
  return idx - 6;
}

export const midiToNoteName = (midi: number): string => {
  return INTERVALS_TO_NOTES[midi % 12];
}

export const accidentalsFromMidiNote = (note: number): number => {
  const name: string = midiToNoteName(note);
  return accidentalsFromNoteName(name);
}

export const chordToPcSet = (chord: number[]): number[] => {
  /** 
   * Convert a chord to a pitch class set
   * @param {number[]} chord - The chord in midi notes
   * @returns {number[]} The unique pitch class set
   */
  return chord.map((note) => note % 12)
  .filter((value, index, self) => self.indexOf(value) === index)
  .sort((a, b) => a - b);
}

export const midiToTpc = (note: number, key: string | number): number => {
  let acc: number;

  if (typeof key === "string") {
    acc = accidentalsFromNoteName(key[0]);
  } else {
    acc = accidentalsFromMidiNote(key);
  }

  return ((note * 7 + 26 - (11 + acc)) % 12 + (11 + acc)) as number;
}

export const midiToOctave = (note: number, key: number = 60): number => {
  return note <= 0 ? 0 : Math.floor((note - key) / 12);
}

type PitchClass = {
  text: string,
  pc: number,
  octave: number,
  add: number,
}

export const midiToPitchClass = (note: number, key: string | number = 60, scale: string = "MAJOR"): PitchClass => {
  function repeatSign(num: number): string {
    return num > 0 ? "^".repeat(num) : num < 0 ? "_".repeat(Math.abs(num)) : "";
  }
  
  const keyNumber = typeof key == "number" ? key : noteNameToMidi(key);
  const pitchClass: number = safeMod(note - keyNumber, 12);

  const octave: number = midiToOctave(note, keyNumber);
  if(typeof scale === "string" && scale.toUpperCase() === "CHROMATIC") {
    return {
      text: pitchClass.toString(),
      pc: pitchClass,
      octave: octave,
      add: 0
    };
  }
  
  const sharps: string[] = ["0", "#0", "1", "#1", "2", "3", "#3", "4", "#4", "5", "#5", "6"];
  const flats: string[] = ["0", "b1", "1", "b2", "2", "3", "b4", "4", "b5", "5", "b6", "6"];
  
  const tpc: number = midiToTpc(note, key);
  
  let npc: string;

  if ((tpc >= 6 && tpc <= 12 && flats[pitchClass].length === 2)) {
    npc = flats[pitchClass];
  } else {
    npc = sharps[pitchClass];
  }

  if (npc.length > 1) {
    const modifier: number = npc[0] === "#" ? 1 : -1;
     return {
      text: repeatSign(octave) + npc,
      pc: parseInt(npc[1]),
      octave: octave,
      add: modifier,
    };
  }

  return {
    text: repeatSign(octave) + npc,
    pc: parseInt(npc),
    octave: octave,
    add: 0
  };
}

export const noteNameToPitchClass = (name: string, key: string, scale: string): PitchClass => {
  const midiNote = noteNameToMidi(name);
  return midiToPitchClass(midiNote, key, scale);
}

// Dmitri Tymoczko voice leading algorithm

export const octaveTransform = (inputChord: number[], root: number): number[] => {
  return inputChord.map(x => root + (x % 12)).sort((a, b) => a - b);
}

export const tMatrix = (chordA: number[], chordB: number[]): (number|undefined)[] => {
  const root = chordA[0];
  const transformedA = octaveTransform(chordA, root);
  const transformedB = octaveTransform(chordB, root);
  return transformedA.map((a, index) => transformedB[index] ? transformedB[index] - a : undefined);
}

export const voiceLead = (chordA: number[], chordB: number[]): number[] => {
  const root = chordA[0];
  const aLeadings = chordA.map(x => [x,
    octaveTransform(chordA, root).indexOf(root + (x % 12))
  ]);

  const tMatrixResult = tMatrix(chordA, chordB);

  const bVoicing = aLeadings.map(([x, y]) => {
      return tMatrixResult[y] ? x + tMatrixResult[y]! : x;
  });

  // TODO: Check octave for extra notes in chordB?

  return bVoicing;
}

export const voiceLeadChords = (inputChords: number[][]): number[][] => {
  // Initialize the result array with the first chord as-is
  const voiceLedChords: number[][] = [inputChords[0]];

  // Iterate through each subsequent chord and voice lead it to the previous chord
  for (let i = 1; i < inputChords.length; i++) {
    const voiceLedChord = voiceLead(inputChords[i], voiceLedChords[i - 1]);
    voiceLedChords.push(voiceLedChord);
  }

  return voiceLedChords;
}

export function transpose(pitches: number[], interval: number, base: number = 12): number[] {
  return pitches.map((x) => (x + interval) % base);
}

export function invert(pitches: number[], axis: number = 0, base: number = 12): number[] {
  return pitches.map((x) => { return (axis - x) % base });
}

export function multiply(pitches: number[], m: number = 5, base: number = 12): number[] {
  return pitches.map((x) => (x * m) % base);
}

export function zero(pitches: number[], base: number = 12): number[] {
  return transpose(pitches, -pitches[0], base);
}

export function cycles(pitches: number[]) {
  const sortedPitches = pitches.slice().sort((a, b) => a - b);
  const cyclicVariations = [];
  for (let i = 0; i < sortedPitches.length; i++) {
    cyclicVariations.push(sortedPitches.slice(i).concat(sortedPitches.slice(0, i)));
  }
  return cyclicVariations;
}

export function normalForm(pitches: number[], base: number = 12): number[] {
  return mostLeftCompact(cycles(pitches), base);
}

export function prime(pitches: number[], base: number = 12): number[] {
  return mostLeftCompact([normalForm(pitches, base), normalForm(invert(pitches, 0, base), base)], base);
}

export function arrayToBinary(array: number[]): number {
  return array.reduce((sum, n) => sum + 2 ** n, 0);
}

export function mostLeftCompact(pcsetArray: number[][], base: number): number[] {
  if (!pcsetArray.every((pcs) => pcs.length === pcsetArray[0].length)) {
    throw new Error("Format error: All pitch sets must have the same cardinality");
  }
  const zeroedPitchArrays = pcsetArray.map((pcs) => zero(pcs, base));
  const binaries = zeroedPitchArrays.map((array) => arrayToBinary(array));
  const minBinary = Math.min(...binaries);
  const winners = pcsetArray.filter((_, i) => binaries[i] === minBinary);
  return winners.sort()[0];
}

export function nearScales(scale: number): number[] {
  const near: number[] = [];
  for (let i = 1; i < 12; i++) {
    let copy = scale;
    if (scale & (1 << i)) {
      // Tone off
      const off = copy ^ (1 << i);
      near.push(off);
      // Down one semitone
      copy = off | (1 << (i - 1));
      near.push(copy);
      // Up one semitone but not octave
      if (i !== 11) {
        copy = off | (1 << (i + 1));
        near.push(copy);
      }
    } else {
      copy = copy | (1 << i);
      near.push(copy);
    }
  }
  const uniq = Array.from(new Set(near));
  return uniq;
}