package zemberek.core.math;

import static java.lang.Math.abs;
import static java.lang.Math.sqrt;

import com.google.common.base.Splitter;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;

public class DoubleArrays {

  public static final double[] ZERO_LENGTH_ARRAY = new double[0];

  // do not allow instantiation
  private DoubleArrays() {
  }

  /**
   * @return true if difference is smaller or equal to range
   */
  public static boolean inRange(double d1, double d2, double range) {
    return Math.abs(d1 - d2) <= range;
  }

  /**
   * @param input double array
   * @return reverse of the double array
   */
  public static double[] reverse(double[] input) {
    double[] result = new double[input.length];
    for (int i = 0; i < input.length; i++) {
      result[input.length - i - 1] = input[i];
    }
    return result;
  }

  /**
   * @param input int array
   * @return double array converted from input int array
   */
  public static double[] convert(int[] input) {
    double[] data = new double[input.length];
    int k = 0;
    for (int i : input) {
      data[k++] = i;
    }
    return data;
  }

  /**
   * @param input 2d int array
   * @return 2d double array converted from 2d int array
   */
  public static double[][] convert(int[][] input) {
    double[][] data = new double[input.length][];
    int k = 0;
    for (int[] i : input) {
      data[k] = new double[i.length];
      int j = 0;
      for (int ii : i) {
        data[k][j++] = ii;
      }
      k++;
    }
    return data;
  }

  /**
   * @param input float array
   * @return double array converted from input float array
   */
  public static double[] convert(float[] input) {
    double[] data = new double[input.length];
    int k = 0;
    for (float i : input) {
      data[k++] = i;
    }
    return data;
  }

  /**
   * @param d1 input double array
   * @param d2 input double array
   * @param range double input
   * @return true if the difference between elements of d1 and d2 is smaller than or equal to given
   * range
   */
  public static boolean arrayEqualsInRange(double[] d1, double[] d2, double range) {
    validateArrays(d1, d2);
    for (int i = 0; i < d1.length; i++) {
      if (Math.abs(d1[i] - d2[i]) > range) {
        return false;
      }
    }
    return true;
  }

  /**
   * @return true if two input double arrays are equal
   */
  public static boolean arrayEquals(double[] d1, double[] d2) {
    validateArrays(d1, d2);
    return Arrays.equals(d1, d2);
  }

  /**
   * @return the double array after appending zeros to its end with the given amount
   * @throws IllegalArgumentException when amount input is negative
   */
  public static double[] appendZeros(double[] darray, int zeroAmountToAppend) {
    if (zeroAmountToAppend < 0) {
      throw new IllegalArgumentException(
          "Cannot append negative amount of zeros. Amount:" + zeroAmountToAppend);
    }
    return Arrays.copyOf(darray, darray.length + zeroAmountToAppend);
  }

  public static double[] normalize16bitLittleEndian(byte[] bytez) {
    return normalize16bitLittleEndian(bytez, bytez.length);
  }

  /**
   * @param bytez input byte array
   * @param amount input, size of the byte array
   * @return double array inncluding the normalized double value of each byte elements as
   * Little-Endian representation For 0xABCD: Big-Endian Rep.-->0xABCD Little-Endian Rep-->0xCDBA
   */
  public static double[] normalize16bitLittleEndian(byte[] bytez, int amount) {
    if ((amount & 1) != 0) {
      throw new IllegalArgumentException(
          "Amount of bytes must be an order of 2. But it is: " + amount);
    }
    double[] result = new double[amount / 2];
    for (int i = 0; i < amount; i += 2) {
      final int val = (short) (bytez[i + 1] << 8) | (bytez[i] & 0xff);
      if (val >= 0) {
        result[i >>> 1] = (double) val / Short.MAX_VALUE;
      } else {
        result[i >>> 1] = -(double) val / Short.MIN_VALUE;
      }
    }
    return result;
  }

  /**
   * @param input input double array
   * @return byte array including the de-normalized 16-bit Big-Endian representations of double
   * values in double array
   */
  public static byte[] denormalize16BitLittleEndian(double[] input) {
    byte[] result = new byte[input.length * 2];
    for (int i = 0; i < input.length; i++) {
      int denorm;
      if (input[i] < 0) {
        denorm = (int) (-input[i] * Short.MIN_VALUE);
      } else {
        denorm = (int) (input[i] * Short.MAX_VALUE);
      }
      result[i * 2] = (byte) (denorm & 0xff);
      result[i * 2 + 1] = (byte) (denorm >>> 8);
    }
    return result;
  }

  /**
   * @param input input double array
   * @param bitsPerSample input as bit number
   * @return byte array including the de-normalized n-bit Big-Endian representations of double
   * values in double array where n is bitsPerSample
   */
  static byte[] denormalizeLittleEndian(double[] input, int bitsPerSample) {
    int bytesPerSample = bitsPerSample % 8 == 0 ? bitsPerSample / 8 : bitsPerSample / 8 + 1;
    int maxVal = 1 << bitsPerSample - 1;
    byte[] result = new byte[input.length * bytesPerSample];
    for (int i = 0; i < input.length; i++) {
      int denorm;
      if (input[i] < 0) {
        denorm = (int) (-input[i] * maxVal);
      } else {
        denorm = (int) (input[i] * maxVal);
      }
      for (int j = 0; j < bytesPerSample; j++) {
        result[i * bytesPerSample + j] = (byte) ((denorm >>> j * 8) & 0xff);
      }
    }
    return result;
  }

  /**
   * gets a double array with values between -1.0 and 1.0 and converts it to an integer in the range
   * of [0,max]
   *
   * @param input double array
   * @param max max integer value.
   * @return an integer array/
   */
  public static int[] toUnsignedInteger(double[] input, int max) {
    if (max < 1) {
      throw new IllegalArgumentException("Maximum int value must be positive. But it is:" + max);
    }
    int[] iarr = new int[input.length];
    double divider = (double) max / 2.0;
    for (int i = 0; i < input.length; i++) {
      double d = input[i];
      if (d < -1.0 || d > 1.0) {
        throw new IllegalArgumentException(
            "Array value should be between -1.0 and 1.0. But it is: " + d);
      }
      iarr[i] = (int) (input[i] * divider);
    }
    return iarr;
  }

  /**
   * finds the maximum value of an array.
   *
   * @param input input array
   * @return maximum value.
   * @throws IllegalArgumentException if array is empty or null.
   */
  public static double max(double... input) {
    validateArray(input);
    double max = input[0];
    for (int i = 1; i < input.length; i++) {
      if (input[i] > max) {
        max = input[i];
      }
    }
    return max;
  }

  /**
   * finds the minimum value of an array.
   *
   * @param input input array
   * @return minimum value.
   * @throws IllegalArgumentException if array is empty or null.
   */
  public static double min(double... input) {
    validateArray(input);
    double min = input[0];
    for (int i = 1; i < input.length; i++) {
      if (input[i] < min) {
        min = input[i];
      }
    }
    return min;
  }

  /**
   * checks whether the input array is null or empty
   *
   * @param input input double array
   */
  public static void validateArray(double... input) {
    if (input == null) {
      throw new IllegalArgumentException("array is null!");
    } else if (input.length == 0) {
      throw new IllegalArgumentException("array is empty!");
    }
  }

  /**
   * @param input input array
   * @return index at which the maximum value of input is, minimum index is returned when multiple
   * maximums
   */
  public static int maxIndex(double... input) {
    validateArray(input);
    double max = input[0];
    int index = 0;
    for (int i = 1; i < input.length; i++) {
      if (input[i] > max) {
        max = input[i];
        index = i;
      }
    }
    return index;
  }

  /**
   * @param input input array
   * @return index at which the minimum value element of input is, minimum index is returned when
   * multiple minimums
   */
  public static int minIndex(double... input) {
    validateArray(input);
    double min = input[0];
    int minIndex = 0;
    for (int i = 1; i < input.length; i++) {
      if (input[i] < min) {
        min = input[i];
        minIndex = i;
      }
    }
    return minIndex;
  }

  /**
   * @param input input
   * @return sum of all elements in array
   */
  public static double sum(double... input) {
    double sum = 0;
    for (double v : input) {
      sum += v;
    }
    return sum;
  }

  /**
   * @param a1 input
   * @param a2 input
   * @return double array of which elements are the sum of 2 input arrays' elements
   */
  public static double[] sum(double[] a1, double[] a2) {
    validateArrays(a1, a2);
    double[] sum = new double[a1.length];
    for (int i = 0; i < a1.length; i++) {
      sum[i] = a1[i] + a2[i];
    }
    return sum;
  }

  /**
   * sums two double vector. result is written to first vector.
   *
   * @param first first vector.
   * @param second second vector
   */
  public static void addToFirst(double[] first, double[] second) {
    validateArrays(first, second);
    for (int i = 0; i < first.length; i++) {
      first[i] = first[i] + second[i];
    }
  }

  /**
   * Adds a value to all elements of the [data] array.
   */
  public static void addToAll(double[] data, double valueToAdd) {
    validateArray(data);
    for (int i = 0; i < data.length; i++) {
      data[i] += valueToAdd;
    }
  }


  /**
   * sums two double vectors (second vector is scaled by scale factor). result is written to first
   * vector.
   *
   * @param first first vector.
   * @param second second vector
   * @param scale scale factor for second
   */
  public static void addToFirstScaled(double[] first, double[] second, double scale) {
    validateArrays(first, second);
    for (int i = 0; i < first.length; i++) {
      first[i] = first[i] + second[i] * scale;
    }
  }

  /**
   * @param input input double array
   * @return an array containing square-values of the input array's elements
   */
  public static double[] square(double... input) {
    double[] res = new double[input.length];
    for (int i = 0; i < input.length; i++) {
      res[i] = input[i] * input[i];
    }
    return res;
  }

  public static void squareInPlace(double... input) {
    for (int i = 0; i < input.length; i++) {
      input[i] = input[i] * input[i];
    }
  }

  /**
   * substracts two double vector.
   *
   * @param a1 first vector.
   * @param a2 second vector
   * @return substraction result
   */
  public static double[] subtract(double[] a1, double[] a2) {
    validateArrays(a1, a2);
    double[] diff = new double[a1.length];
    for (int i = 0; i < a1.length; i++) {
      diff[i] = a1[i] - a2[i];
    }
    return diff;
  }

  /**
   * substracts two double vector. result is written to first vector.
   *
   * @param first first vector.
   * @param second second vector
   */
  public static void subtractFromFirst(double[] first, double[] second) {
    validateArrays(first, second);
    for (int i = 0; i < first.length; i++) {
      first[i] = first[i] - second[i];
    }
  }

  /**
   * @param a1 input double array
   * @param a2 input double array
   * @return the array produced after multiplying the elements of input arrays
   */
  public static double[] multiply(double[] a1, double[] a2) {
    validateArrays(a1, a2);
    double[] mul = new double[a1.length];
    for (int i = 0; i < a1.length; i++) {
      mul[i] = a1[i] * a2[i];
    }
    return mul;
  }

  /**
   * @param a1 input double array
   * @param a2 input double array
   * @return the dot product value of elements in input arrays
   */

  public static double dotProduct(double[] a1, double[] a2) {
    return sum(multiply(a1, a2));
  }

  /**
   * multiplies two double vectors and result is written to the first vector.
   *
   * @param first first vector
   * @param second second vector.
   */
  public static void multiplyToFirst(double[] first, double[] second) {
    validateArrays(first, second);
    for (int i = 0; i < first.length; i++) {
      first[i] = first[i] * second[i];
    }
  }

  /**
   * Multiplies all elements of a vector with a double number and returns a new vector
   *
   * @param a1 vector
   * @param b scale factor
   * @return new scaled vector
   */
  public static double[] scale(double[] a1, double b) {
    validateArray(a1);
    double[] mul = new double[a1.length];
    for (int i = 0; i < a1.length; i++) {
      mul[i] = a1[i] * b;
    }
    return mul;
  }

  /**
   * Multiplies all elements of a vector with a double number
   *
   * @param a1 vector
   * @param b scale factor
   */
  public static void scaleInPlace(double[] a1, double b) {
    validateArray(a1);
    for (int i = 0; i < a1.length; i++) {
      a1[i] = a1[i] * b;
    }
  }

  /**
   * Calculates mean of a vector.
   *
   * @param input double array
   * @return mean
   */
  public static double mean(double... input) {
    validateArray(input);
    return sum(input) / input.length;
  }

  /**
   * for A=[a0, a1, ...,an] for B=[b0, b1, ...,bn] returns C=|a0-b0|+|a1-b1|+...+|an-bn|
   *
   * @param a input array a
   * @param b input array b
   * @return squared sum of array elements.
   */
  public static double absoluteSumOfDifferences(double[] a, double[] b) {
    return sum(absoluteDifference(a, b));
  }

  /**
   * for A=[a0, a1, ...,an] for B=[b0, b1, ...,bn] returns C=[|a0-b0|,|a1-b1|,...,|an-bn|]
   *
   * @param a input array a
   * @param b input array b
   * @return squared sum of array elements.
   */
  public static double[] absoluteDifference(double[] a, double[] b) {
    validateArrays(a, b);
    double[] diff = new double[a.length];
    for (int i = 0; i < a.length; i++) {
      diff[i] += abs(a[i] - b[i]);
    }
    return diff;
  }


  /**
   * checks whether one of the input arrays are null or not, and whether their length is equal or
   * not
   *
   * @param a1 input double array
   * @param a2 input double array
   */
  public static void validateArrays(double[] a1, double[] a2) {
    if (a1 == null) {
      throw new NullPointerException("first array is null!");
    }
    if (a2 == null) {
      throw new NullPointerException("second array is null!");
    }
    if (a1.length != a2.length) {
      throw new IllegalArgumentException("Array sizes must be equal. But, first:"
          + a1.length + ", and second:" + a2.length);
    }
  }

  /**
   * for A=[a0, a1, ...,an] returns a0*a0+a1*a1+....+an*an
   *
   * @param array input array
   * @return squared sum of array elements.
   */
  public static double squaredSum(double[] array) {
    double result = 0;
    validateArray(array);
    for (double a : array) {
      result += a * a;
    }
    return result;
  }

  public static double squaredSumOfDifferences(double[] a, double[] b) {
    return (squaredSum(subtract(a, b)));
  }

  /**
   * @param input input double array
   * @return variance value of the elements in the input array
   */
  public static double variance(double[] input) {
    double sigmaSquare = 0;
    double mean = mean(input);
    for (double a : input) {
      final double meanDiff = a - mean;
      sigmaSquare += meanDiff * meanDiff;
    }
    return sigmaSquare / (input.length - 1);
  }

  /**
   * @param a input double array
   * @return standard deviation value of the elements in the input array
   */
  public static double standardDeviation(double[] a) {
    return sqrt(variance(a));
  }

  /**
   * @param a input double array
   * @return true if array includes at least one Not-a-Number (NaN) value, false otherwise
   */
  public static boolean containsNaN(double[] a) {
    for (double v : a) {
      if (Double.isNaN(v)) {
        return true;
      }
    }
    return false;
  }

  /**
   * replaces the elements smaller than minValue with the minValue
   *
   * @param var input double array
   * @param minValue double
   */
  public static void floorInPlace(double[] var, double minValue) {
    for (int k = 0; k < var.length; k++) {
      if (var[k] < minValue) {
        var[k] = minValue;
      }
    }
  }

  /**
   * If a data point is non-zero and below 'floor' make it equal to floor
   *
   * @param data the data to floor
   * @param floor the floored value
   */
  public static void nonZeroFloorInPlace(double[] data, double floor) {
    for (int i = 0; i < data.length; i++) {
      if (data[i] != 0.0 && data[i] < floor) {
        data[i] = floor;
      }
    }
  }

  /**
   * Normalize the given data.
   *
   * @param data the data to normalize
   */
  public static void normalizeInPlace(double[] data) {
    double sum = sum(data);
    scaleInPlace(data, 1d / sum);
  }

  public static void serialize(DataOutputStream dos, double[] data) throws IOException {
    dos.writeInt(data.length);
    for (double v : data) {
      dos.writeDouble(v);
    }
  }

  public static void serializeRaw(DataOutputStream dos, double[] data) throws IOException {
    for (double v : data) {
      dos.writeDouble(v);
    }
  }


  public static void serialize(DataOutputStream dos, double[][] data) throws IOException {
    dos.writeInt(data.length);
    for (double[] doubles : data) {
      serialize(dos, doubles);
    }
  }

  public static void serializeRaw(DataOutputStream dos, double[][] data) throws IOException {
    for (double[] doubles : data) {
      serializeRaw(dos, doubles);
    }
  }


  public static double[] deserialize(DataInputStream dis) throws IOException {
    int amount = dis.readInt();
    double[] result = new double[amount];
    for (int i = 0; i < amount; i++) {
      result[i] = dis.readDouble();
    }
    return result;
  }

  public static double[] deserializeRaw(DataInputStream dis, int amount) throws IOException {
    double[] result = new double[amount];
    for (int i = 0; i < amount; i++) {
      result[i] = dis.readDouble();
    }
    return result;
  }

  public static void deserializeRaw(DataInputStream dis, double[] result) throws IOException {
    for (int i = 0; i < result.length; i++) {
      result[i] = dis.readDouble();
    }
  }

  public static double[][] deserialize2d(DataInputStream dis) throws IOException {
    int amount = dis.readInt();
    double[][] result = new double[amount][];
    for (int i = 0; i < amount; i++) {
      result[i] = deserialize(dis);
    }
    return result;
  }

  public static void deserialize2DRaw(DataInputStream dis, double[][] result) throws IOException {
    for (double[] row : result) {
      deserializeRaw(dis, row);
    }
  }

  public static double[][] clone2D(double[][] result) throws IOException {
    double[][] arr = new double[result.length][];
    for (int i = 0; i < arr.length; i++) {
      arr[i] = result[i].clone();
    }
    return arr;
  }


  public static double[] fromString(String str, String delimiter) {
    List<String> tokens = Splitter.on(delimiter).trimResults().omitEmptyStrings().splitToList(str);
    double[] result = new double[tokens.size()];
    for (int i = 0; i < result.length; i++) {
      result[i] = Float.parseFloat(tokens.get(i));
    }
    return result;
  }

  /**
   * Formats a float array as string using English Locale.
   */
  public static String format(double... input) {
    return format(10, 3, " ", input);
  }

  /**
   * Formats a float array as string using English Locale.
   */
  public static String format(int fractionDigits, String delimiter, double... input) {
    StringBuilder sb = new StringBuilder();
    String formatStr = "%." + fractionDigits + "f";
    int i = 0;
    for (double v : input) {
      sb.append(String.format(Locale.ENGLISH, formatStr, v));
      if (i++ < input.length - 1) {
        sb.append(delimiter);
      }
    }
    return sb.toString();
  }

  /**
   * Formats a float array as string using English Locale.
   */
  public static String format(int rightPad, int fractionDigits, String delimiter, double... input) {
    StringBuilder sb = new StringBuilder();
    String formatStr = "%." + fractionDigits + "f";
    int i = 0;
    for (double v : input) {
      String num = String.format(formatStr, v);
      sb.append(String.format(Locale.ENGLISH, "%-" + rightPad + "s", num));
      if (i++ < input.length - 1) {
        sb.append(delimiter);
      }
    }
    return sb.toString().trim();
  }


  public static double[] reduceFractionDigits(double[] arr, int digitCount) {
    if (digitCount < 1 || digitCount > 10) {
      throw new IllegalArgumentException("Digit count cannot be less than 1 or more than 10");
    }
    double[] newArr = new double[arr.length];
    int powerOfTen = (int) Math.pow(10, digitCount);
    for (int i = 0; i < arr.length; i++) {
      double val = arr[i] * powerOfTen;
      val = Math.round(val);
      val = val / powerOfTen;
      newArr[i] = val;
    }
    return newArr;
  }

  public static float[] fromDelimitedString(String input, String delimiter) {
    List<String> tokens = Splitter.on(delimiter).omitEmptyStrings().trimResults().splitToList(input);
    float[] result = new float[tokens.size()];
    for (int i = 0; i < tokens.size(); i++) {
      result[i] = Float.parseFloat(tokens.get(i));
    }
    return result;
  }

}
