/* eslint-disable @typescript-eslint/no-non-null-assertion */
import bs from 'binary-search';
import { Stimulus, Zeta, ZetaSymbolic } from './type';
import { fillZetaDefaults } from './corpus';

/**
 * Calculates the probability that someone with a given ability level theta will
 * answer correctly an item. Uses the 4 parameters logistic model
 *
 * @param {number} theta - ability estimate
 * @param {Zeta} zeta - item params
 * @returns {number} the probability
 */
export const itemResponseFunction = (theta: number, zeta: Zeta) => {
  const _zeta = fillZetaDefaults(zeta, 'symbolic') as ZetaSymbolic;
  return _zeta.c + (_zeta.d - _zeta.c) / (1 + Math.exp(-_zeta.a * (theta - _zeta.b)));
};

/**
 * A 3PL Fisher information function
 *
 * @param {number} theta - ability estimate
 * @param {Zeta} zeta - item params
 * @returns {number} - the expected value of the observed information
 */
export const fisherInformation = (theta: number, zeta: Zeta) => {
  const _zeta = fillZetaDefaults(zeta, 'symbolic') as ZetaSymbolic;
  const p = itemResponseFunction(theta, _zeta);
  const q = 1 - p;
  return Math.pow(_zeta.a, 2) * (q / p) * (Math.pow(p - _zeta.c, 2) / Math.pow(1 - _zeta.c, 2));
};

/**
 * Return a Gaussian distribution within a given range
 *
 * @param {number} mean
 * @param {number} stdDev
 * @param {number} min
 * @param {number} max
 * @param {number} stepSize - the quantization (step size) of the internal table, default = 0.1
 * @returns {Array<[number, number]>} - a normal distribution
 */
export const normal = (mean = 0, stdDev = 1, min = -4, max = 4, stepSize = 0.1) => {
  const distribution = [];
  for (let i = min; i <= max; i += stepSize) {
    distribution.push([i, y(i)]);
  }
  return distribution;

  function y(x: number) {
    return (1 / (Math.sqrt(2 * Math.PI) * stdDev)) * Math.exp(-Math.pow(x - mean, 2) / (2 * Math.pow(stdDev, 2)));
  }
};

/**
 * Find the item in a given array that has the difficulty closest to the target value
 *
 * @remarks
 * The input array of stimuli must be sorted by difficulty.
 *
 * @param {Stimulus[]} inputStimuli - an array of stimuli sorted by difficulty
 * @param {number} target - ability estimate
 * @returns {number} the index of stimuli
 */
export const findClosest = (inputStimuli: Array<Stimulus>, target: number) => {
  const stimuli = inputStimuli.map((stim) => fillZetaDefaults(stim, 'semantic'));
  // Let's consider the edge cases first
  if (target <= stimuli[0].difficulty!) {
    return 0;
  } else if (target >= stimuli[stimuli.length - 1].difficulty!) {
    return stimuli.length - 1;
  }

  const comparitor = (element: Stimulus, needle: number) => {
    return element.difficulty! - needle;
  };
  const indexOfTarget = bs(stimuli, target, comparitor);

  if (indexOfTarget >= 0) {
    // `bs` returns a positive integer index if it found an exact match.
    return indexOfTarget;
  } else {
    // If the value is not in the array, then -(index + 1) is returned, where
    // index is where the value should be inserted into the array to maintain
    // sorted order. Thus, the target is between the values at
    const lowIndex = -2 - indexOfTarget;
    const highIndex = -1 - indexOfTarget;

    // So we simply compare the differences between the target and the high and
    // low values, respectively
    const lowDiff = Math.abs(stimuli[lowIndex].difficulty! - target);
    const highDiff = Math.abs(stimuli[highIndex].difficulty! - target);

    if (lowDiff < highDiff) {
      return lowIndex;
    } else {
      return highIndex;
    }
  }
};
