export type TMixed = number | null; // - mixed type

/**
 * - check if the parameter is an array of numbers or null values
 * @param {number[]} arr - array to be checked
 * @param {string} parameName - name of the parameter
 */
export function checkArrayOfNumbers(arr: number[], parameName: string) {
  if (!Array.isArray(arr) || arr.some((item) => typeof item !== "number")) {
    throw new Error(`The ${parameName} parameter must be an array of numbers`);
  }
}

/**
 * - check if the parameter is a number between min and max
 * @param {number} num - number to be checked
 * @param {number} min - minimum value
 * @param {number} max - maximum value
 * @param {string} parameName - name of the parameter
 * @param {string} maxName - name of the array
 */
export function checkNumber(
  num: number,
  min: number,
  max: TMixed[] | number,
  parameName: string,
  maxName: string
) {
  const maxStr = Array.isArray(max)
    ? `the length of the ${maxName} array`
    : maxName;
  if (
    typeof num !== "number" ||
    num < min ||
    num > (Array.isArray(max) ? max.length : max)
  ) {
    throw new Error(
      `The ${parameName} parameter must be an array of numbers between ${min} and ${maxStr}`
    );
  }
}

/**
 * - Sum of n-period moving averages (SUM)
 * @param {TMixed[]} arr - closing prices of the stock
 * @param {number} n - number of periods to calculate the SUM, must be greater than 1 and less than the length of the CLOSE array
 */
export function funMS(arr: TMixed[], n: number): TMixed[] {
  const doSum = (tArr: TMixed[]): TMixed => {
    if (tArr.every((item) => item === null || isNaN(item))) {
      return null;
    }
    return tArr.reduce(
      (acc: number, cur) => (cur !== null && !isNaN(cur) ? acc + cur : acc),
      0
    );
  };

  // calculate the SUM for each period, starting from the second period
  const result: TMixed[] = [];
  for (let i = 0; i < arr.length; i++) {
    result.push(doSum(arr.slice(Math.max(0, i - n + 1), i + 1)));
  }

  return result;
}

/**
 * - Moving Average (MA)
 * @param {TMixed[]} arr - - closing prices of the stock
 * @param {number} n - - number of periods to calculate the moving average, must be between 2 and the length of the CLOSE array
 * @returns { TMixed[] } } - array of moving average values
 */
export function funMA(arr: TMixed[], n: number): TMixed[] {
  const result: TMixed[] = [];
  //对给定的数组arr进行检查，如果包含null，则返回null，否则计算n个周期的移动平均值
  const doMA = (iarr: TMixed[], count: number) => {
    if (iarr.length < count) {
      return null;
    }
    let sum = 0;
    for (let i = 0; i < iarr.length; i++) {
      if (typeof iarr[i] === "number" && !isNaN(iarr[i] as number)) {
        sum += iarr[i] as number;
      } else {
        return null;
      }
    }
    return sum / count;
  };
  for (let i = 0; i < arr.length; i++) {
    result.push(doMA(arr.slice(Math.max(0, i - n + 1), i + 1), n));
  }
  return result;
}

/**
 * SMA (Simple Moving Average)
 * @param {TMixed[]} arr - An array of the closing price of the stock
 * @param {number} n - The number of periods to calculate the SMA for. Must be between 2 and the length of the CLOSE array.
 * @param {number} m - The number of periods to calculate the SMA for. Must be between 0 and N-1.
 * @returns {{ SMA: TMixed[] }} - The SMA values for the given data and parameters
 */
export function funSMA(arr: TMixed[], n: number, m: number): TMixed[] {
  return arr.slice(1).reduce(
    (acc, cur, i) => {
      if (cur === null) {
        acc.push(null);
      } else if (i === 0 && (acc[0] === null || acc[0] === 0)) {
        acc.push(cur);
      } else {
        acc.push((cur * m + (acc[i] as number) * (n - m)) / n);
      }
      return acc;
    },
    [arr[0]]
  );
}

/**
 * - Exponential Moving Average (EMA)
 * @param {TMixed[]} arr - closing prices of the stock
 * @param {number} n - number of periods to calculate the EMA，must be greater than 1 and less than the length of the CLOSE array
 * @param {number} m - number of periods to calculate the initial EMA, default is 1，must be less than N
 * @returns {{ EMA: TMixed[] }} - object with the EMA values
 */
export function funEMA(arr: TMixed[], n: number, m: number = 1): TMixed[] {
  return arr.slice(1).reduce(
    (acc, cur) => {
      acc.push(
        cur
          ? (cur * (m + 1) + (acc.slice(-1)[0] ?? cur) * (n - m)) / (n + 1)
          : null
      );
      return acc;
    },
    [arr[0]]
  );
}

/**
 * WMA - Weighted Moving Average
 * @param {TMixed[]} arr should be an array of numbers or null values
 * @param {number} n should be a number between 2 and the length of the data array
 * @returns {TMixed[]} WMA values
 */
export function funWMA(arr: TMixed[], n: number): TMixed[] {
  // calculate WMA
  const cumsum = (arr: TMixed[]): TMixed => {
    const sum = arr
      .slice(1)
      .reduce(
        (acc, cur, i) => (acc && cur ? acc + cur * (i + 1) : null),
        arr[0]
      );
    return sum ? sum / ((arr.length * (arr.length + 1)) / 2) : null;
  };
  return arr.reduce((acc: TMixed[], cur, i) => {
    if (i < n - 1) {
      acc.push(null);
    } else {
      const res = cumsum(arr.slice(i - n + 1, i + 1));
      acc.push(res);
    }
    return acc;
  }, []);
}

/**
 * - Standard Deviation (STD)
 * @param {TMixed[]} arr - closing prices of the stock
 * @param {number} n - number of periods to calculate the STD, must be greater than 1 and less than the length of the CLOSE array
 * @returns { TMixed[] } - array of standard deviation values
 */
export function funSTD(arr: TMixed[], n: number): TMixed[] {
  return arr.reduce((acc: TMixed[], cur, i) => {
    if (i < n - 1) {
      acc.push(null);
    } else {
      const mean =
        arr
          .slice(i - n + 1, i + 1)
          .reduce((acc: number, cur) => acc + (cur ?? 0), 0) / n;
      const variance = arr
        .slice(i - n + 1, i + 1)
        .reduce(
          (acc: number, cur) => acc + ((cur ?? 0) - mean) ** 2 / (n - 1),
          0
        );
      const std = Math.sqrt(variance);
      acc.push(std);
    }
    return acc;
  }, []);
}

/**
 * - Average Variance Enhanced (AVEDEV)
 * @param {TMixed[]} arr - closing prices of the stock
 * @param {number} n - number of periods to calculate the AVEDEV, must be greater than 1 and less than the length of the CLOSE array
 * @returns { TMixed[] } - array of average variance enhanced values
 */
export function funAVEDEV(arr: TMixed[], n: number): TMixed[] {
  const result: TMixed[] = [];
  for (let i = 0; i < arr.length; i++) {
    const curArr = arr.slice(Math.max(0, i - n + 1), i + 1);
    if (
      curArr.some((item) => item === null || isNaN(item)) ||
      curArr.length < n
    ) {
      result.push(null);
      continue;
    }
    const mean: number =
      curArr.reduce((acc: number, cur) => acc + (cur ?? 0), 0) / n;
    result.push(
      (curArr as number[]).reduce(
        (acc: number, cur: number) => acc + Math.abs(cur - mean),
        0
      ) / n
    );
  }
  return result;
}

/**
 * - subtract two arrays element by element
 * @param {TMixed[]} subtractor - array to be subtracted from
 * @param {TMixed[]} minuend - array to be subtracted
 * @returns {TMixed[]} - array of subtracted values
 */
export function funArrSub(subtractor: TMixed[], minuend: TMixed[]): TMixed[] {
  const count = Math.max(subtractor.length, minuend.length);
  const results: TMixed[] = [];
  for (let i = 0; i < count; i++) {
    results.push(
      subtractor[i] !== null && minuend[i] !== null
        ? (subtractor[i] ?? 0) - (minuend[i] ?? 0)
        : null
    );
  }
  return results;
}

/**
 * - add two or more arrays element by element
 * @param {TMixed[]}...args - arrays to be added
 * @returns {TMixed[]} - array of added values
 */
export function funArrAdd(...args: TMixed[][]): TMixed[] {
  const result: number[] = [];
  const maxLength = args.reduce((p, v) => Math.max(p, v.length), 0);
  for (let i = 0; i < maxLength; i++) {
    let sum = 0;
    for (let j = 0; j < args.length; j++) {
      if (i < args[j].length) {
        sum += args[j][i] ?? 0;
      }
    }
    result.push(sum);
  }
  return result;
}

/**
 * - divide two arrays element by element
 * @param {TMixed[]} divisor - array to be divided
 * @param {TMixed[]} dividend - array to divide by
 * @returns {TMixed[]}- array of divided values
 */
export function funArrDiv(divisor: TMixed[], dividend: TMixed[]): TMixed[] {
  const count = Math.max(divisor.length, dividend.length);
  const results: TMixed[] = [];
  for (let i = 0; i < count; i++) {
    if (dividend[i] === null || divisor[i] === null || dividend[i] === 0) {
      results.push(null);
      continue;
    } else {
      results.push((divisor[i] as number) / (dividend[i] as number));
    }
  }
  return results;
}

/**
 * - multiply two arrays element by element
 * @param {TMixed[]} arr1 - first array to be multiplied
 * @param {TMixed[]} arr2 - second array to be multiplied
 * @returns {TMixed[]} - array of multiplied values
 */
export function funArrMul(arr1: TMixed[], arr2: TMixed[]): TMixed[] {
  const count = Math.max(arr1.length, arr2.length);
  const results: TMixed[] = [];
  for (let i = 0; i < count; i++) {
    if (arr1[i] === null || arr2[i] === null) {
      results.push(null);
      continue;
    } else {
      results.push((arr1[i] as number) * (arr2[i] as number));
    }
  }
  return results;
}

/**
 * - absolute value of an array
 * @param {TMixed[]} arr - array to be absolute value of
 * @returns {TMixed[]} - array of absolute values
 */
export function funArrAbs(arr: TMixed[]): TMixed[] {
  return arr.map((v) => (v === null ? null : Math.abs(v)));
}

/**
 * - rounded to n decimal places
 * @param {number} num - number to be rounded
 * @param {number} n - number of decimal places to round to
 * @returns {number} - rounded number
 */
export function funRound(num: TMixed, n: number): TMixed {
  if (typeof num !== "number" || isNaN(num)) return null;
  return Math.round(num * Math.pow(10, n)) / Math.pow(10, n);
}

/**
 * - check if a is greater than b with a given precision
 * @param {number} a - first number
 * @param {number} b - second number
 * @param {number} precision - precision to compare the numbers
 * @returns {boolean} - true if a is greater than b, false otherwise
 */
export function aGtB(
  a: number,
  b: number,
  precision: number = 0.0001
): boolean {
  return a - b > precision;
}

/**
 * - check if a is less than b with a given precision
 * @param {number} a - first number
 * @param {number} b - second number
 * @param {number} precision - precision to compare the numbers
 * @returns {boolean} - true if a is less than b, false otherwise
 */
export function aLtB(
  a: number,
  b: number,
  precision: number = 0.0001
): boolean {
  return b - a > precision;
}

/**
 * - check if a is equal to b with a given precision
 * @param {number} a - first number
 * @param {number} b - second number
 * @param {number} precision - precision to compare the numbers
 * @returns {boolean} - true if a is equal to b, false otherwise
 */
export function aEqB(
  a: number,
  b: number,
  precision: number = 0.0001
): boolean {
  return Math.abs(a - b) <= precision;
}

/**
 * - check if a is not equal to b with a given precision
 * @param {number} a - first number
 * @param {number} b - second number
 * @param {number} precision - precision to compare the numbers
 * @returns {boolean} - true if a is not equal to b, false otherwise
 */
export function aNeqB(
  a: number,
  b: number,
  precision: number = 0.0001
): boolean {
  return Math.abs(a - b) > precision;
}
