import Lookup from './Lookup';

const log2 = Math.log(2);
const log1000 = Math.log(1000);
const log1000OverLog2 = log1000 / log2;

const FREQ_LOOKUP_BITS = 14;
class HzToZeroOneLookup extends Lookup {
  compute = (input: number) => Math.log(input / 20) / log2 / log1000OverLog2;
  getNthPrecomputedInput = (n: number) =>
    20 * Math.pow(2, (n / (this.tableSize - 1)) * log1000OverLog2);
}
const hzToZeroOneLookup = new HzToZeroOneLookup();
hzToZeroOneLookup.initialize(Math.pow(2, FREQ_LOOKUP_BITS));

class ZeroOneToHzLookup extends Lookup {
  compute = (input: number) => 20 * Math.pow(2, input * log1000OverLog2);
  getNthPrecomputedInput = (n: number) => n / (this.tableSize - 1);
}
const zeroOneToHzLookup = new ZeroOneToHzLookup();
zeroOneToHzLookup.initialize(Math.pow(2, FREQ_LOOKUP_BITS), true);

class PitchToHzLookup extends Lookup {
  compute = (pitch: number) => 440 * Math.pow(2, (pitch - 69) / 12);
  getNthPrecomputedInput = (n: number) => (n / (this.tableSize - 1)) * 127;
}
const pitchToHzLookup = new PitchToHzLookup();
pitchToHzLookup.initialize(Math.pow(2, FREQ_LOOKUP_BITS), true);

class HzToPitchLookup extends HzToZeroOneLookup {
  compute = (hz: number) => 12 * Math.log2(hz / 440) + 69;
}
const hzToPitchLookup = new HzToPitchLookup();
hzToPitchLookup.initialize(Math.pow(2, FREQ_LOOKUP_BITS));

class Octave2PowLookup extends Lookup {
  compute = (octave: number) => Math.pow(2, octave);
  getNthPrecomputedInput = (n: number) => (n / (this.tableSize - 1)) * 20 - 10;
}
const octave2PowLookup = new Octave2PowLookup();
octave2PowLookup.initialize(Math.pow(2, FREQ_LOOKUP_BITS + 1));

export const modulo = (n: number, m: number) => ((n % m) + m) % m;
export const clampZeroOne = (input: number) => Math.max(0, Math.min(1, input));
export const zeroOneToHz = (input: number, exact: boolean = false) => {
  if (exact) {
    return zeroOneToHzLookup.compute(input);
  } else {
    return zeroOneToHzLookup.get(input);
  }
};
export const hzToZeroOne = (input: number, exact: boolean = false) => {
  if (exact) {
    return hzToZeroOneLookup.compute(input);
  } else {
    return hzToZeroOneLookup.get(input);
  }
};

export const pitchToHz = (pitch: number, exact: boolean = false) => {
  if (exact) {
    return pitchToHzLookup.compute(pitch);
  } else {
    return pitchToHzLookup.get(pitch);
  }
};
export const hzToPitch = (hz: number, exact: boolean = false) => {
  if (exact) {
    return hzToPitchLookup.compute(hz);
  } else {
    return hzToPitchLookup.get(hz);
  }
};
export const transposeSemitones = (hz: number, semitones: number) =>
  hz * octave2PowLookup.get(semitones / 12);
export const transposeOctaves = (hz: number, octaves: number) =>
  hz * octave2PowLookup.get(octaves);

export const qToBandWidth = (q: number) => {
  const x1 = (2 * q * q + 1) / (2 * q * q);
  const x2 = Math.pow(2 * x1, 2) / 4;
  const x3 = Math.sqrt(x2 - 1);
  return Math.log2(x1 + x3);
};

const log10 = Math.log(10);
const log10Over20 = log10 / 20;

export const fast10Pow = (x: number) => Math.exp(x * log10);

export const dbToMultiplier = (db: number) => {
  // Math.pow(10, db / 20)
  // ...but 5x faster.
  return Math.exp(db * log10Over20);
};

export const multiplierToDb = (magnitude: number) => {
  return 20 * Math.log10(magnitude);
};

export const msToSamples = (ms: number, sampleRate: number) => {
  return (ms * sampleRate) / 1000;
};

export const samplesToMs = (samples: number, sampleRate: number) => {
  return (samples / sampleRate) * 1000;
};

export const pitchToMultiplier = (pitch: number) => {
  return Math.pow(2, pitch / 12);
};

export const multiplierToPitch = (multiplier: number) => {
  return 12 * Math.log2(multiplier);
};

if (typeof window !== 'undefined') {
  (window as any)['pitchToHz'] = pitchToHz;
  (window as any)['hzToPitch'] = hzToPitch;
  (window as any)['zeroOneToHz'] = zeroOneToHz;
  (window as any)['hzToZeroOne'] = hzToZeroOne;
  (window as any)['qToBandWidth'] = qToBandWidth;
  (window as any)['dbToMultiplier'] = dbToMultiplier;
  (window as any)['multiplierToDb'] = multiplierToDb;
  (window as any)['msToSamples'] = msToSamples;
  (window as any)['samplesToMs'] = samplesToMs;
}
