package tech.tablesaw.api;

import com.google.common.base.Preconditions;
import it.unimi.dsi.fastutil.doubles.DoubleArrayList;
import it.unimi.dsi.fastutil.doubles.DoubleArrays;
import it.unimi.dsi.fastutil.doubles.DoubleComparators;
import it.unimi.dsi.fastutil.doubles.DoubleIterator;
import it.unimi.dsi.fastutil.doubles.DoubleListIterator;
import it.unimi.dsi.fastutil.doubles.DoubleOpenHashSet;
import it.unimi.dsi.fastutil.doubles.DoubleSet;
import java.math.BigDecimal;
import java.nio.ByteBuffer;
import java.util.Collection;
import java.util.Iterator;
import java.util.function.DoubleConsumer;
import java.util.function.DoublePredicate;
import java.util.function.DoubleSupplier;
import java.util.stream.DoubleStream;
import tech.tablesaw.columns.AbstractColumnParser;
import tech.tablesaw.columns.Column;
import tech.tablesaw.columns.numbers.DoubleColumnType;
import tech.tablesaw.columns.numbers.FloatColumnType;
import tech.tablesaw.columns.numbers.NumberColumnFormatter;
import tech.tablesaw.columns.numbers.NumberFillers;
import tech.tablesaw.columns.numbers.fillers.DoubleRangeIterable;
import tech.tablesaw.selection.Selection;

public class DoubleColumn extends NumberColumn<DoubleColumn, Double>
    implements NumberFillers<DoubleColumn> {

  private final DoubleArrayList data;

  protected DoubleColumn(String name, DoubleArrayList data) {
    super(DoubleColumnType.instance(), name);
    setPrintFormatter(NumberColumnFormatter.floatingPointDefault());
    this.data = data;
  }

  public static boolean valueIsMissing(double value) {
    return DoubleColumnType.valueIsMissing(value);
  }

  @Override
  public String getString(int row) {
    final double value = getDouble(row);
    if (DoubleColumnType.valueIsMissing(value)) {
      return "";
    }
    return String.valueOf(getPrintFormatter().format(value));
  }

  @Override
  public int size() {
    return data.size();
  }

  @Override
  public void clear() {
    data.clear();
  }

  public DoubleColumn setMissing(int index) {
    set(index, DoubleColumnType.missingValueIndicator());
    return this;
  }

  protected DoubleColumn(String name) {
    super(DoubleColumnType.instance(), name);
    setPrintFormatter(NumberColumnFormatter.floatingPointDefault());
    this.data = new DoubleArrayList(DEFAULT_ARRAY_SIZE);
  }

  public static DoubleColumn create(String name, double... arr) {
    return new DoubleColumn(name, new DoubleArrayList(arr));
  }

  public static DoubleColumn create(String name) {
    return new DoubleColumn(name);
  }

  public static DoubleColumn create(String name, float... arr) {
    final double[] doubles = new double[arr.length];
    for (int i = 0; i < arr.length; i++) {
      doubles[i] = arr[i];
    }
    return new DoubleColumn(name, new DoubleArrayList(doubles));
  }

  public static DoubleColumn create(String name, int... arr) {
    final double[] doubles = new double[arr.length];
    for (int i = 0; i < arr.length; i++) {
      doubles[i] = arr[i];
    }
    return new DoubleColumn(name, new DoubleArrayList(doubles));
  }

  public static DoubleColumn create(String name, long... arr) {
    final double[] doubles = new double[arr.length];
    for (int i = 0; i < arr.length; i++) {
      doubles[i] = arr[i];
    }
    return new DoubleColumn(name, new DoubleArrayList(doubles));
  }

  public static DoubleColumn create(String name, Collection<? extends Number> numberList) {
    DoubleColumn newColumn = new DoubleColumn(name, new DoubleArrayList(0));
    for (Number number : numberList) {
      newColumn.append(number);
    }
    return newColumn;
  }

  public static DoubleColumn create(String name, Number[] numbers) {
    DoubleColumn newColumn = new DoubleColumn(name, new DoubleArrayList(0));
    for (Number number : numbers) {
      newColumn.append(number);
    }
    return newColumn;
  }

  public static DoubleColumn create(String name, int initialSize) {
    DoubleColumn column = new DoubleColumn(name);
    for (int i = 0; i < initialSize; i++) {
      column.appendMissing();
    }
    return column;
  }

  public static DoubleColumn create(String name, DoubleStream stream) {
    DoubleArrayList list = new DoubleArrayList();
    stream.forEach(list::add);
    return new DoubleColumn(name, list);
  }

  @Override
  public DoubleColumn createCol(String name, int initialSize) {
    return create(name, initialSize);
  }

  @Override
  public DoubleColumn createCol(String name) {
    return create(name);
  }

  @Override
  public Double get(int index) {
    double result = getDouble(index);
    return isMissingValue(result) ? null : result;
  }

  @Override
  public DoubleColumn where(Selection selection) {
    return (DoubleColumn) super.where(selection);
  }

  @Override
  public DoubleColumn subset(int[] rows) {
    final DoubleColumn c = this.emptyCopy();
    for (final int row : rows) {
      c.append(getDouble(row));
    }
    return c;
  }

  @Override
  public DoubleColumn unique() {
    final DoubleSet doubles = new DoubleOpenHashSet();
    for (int i = 0; i < size(); i++) {
      doubles.add(getDouble(i));
    }
    final DoubleColumn column = DoubleColumn.create(name() + " Unique values");
    doubles.forEach((DoubleConsumer) column::append);
    return column;
  }

  @Override
  public DoubleColumn top(int n) {
    DoubleArrayList top = new DoubleArrayList();
    double[] values = data.toDoubleArray();
    DoubleArrays.parallelQuickSort(values, DoubleComparators.OPPOSITE_COMPARATOR);
    for (int i = 0; i < n && i < values.length; i++) {
      top.add(values[i]);
    }
    return new DoubleColumn(name() + "[Top " + n + "]", top);
  }

  @Override
  public DoubleColumn bottom(final int n) {
    DoubleArrayList bottom = new DoubleArrayList();
    double[] values = data.toDoubleArray();
    DoubleArrays.parallelQuickSort(values);
    for (int i = 0; i < n && i < values.length; i++) {
      bottom.add(values[i]);
    }
    return new DoubleColumn(name() + "[Bottoms " + n + "]", bottom);
  }

  @Override
  public DoubleColumn lag(int n) {
    final int srcPos = n >= 0 ? 0 : 0 - n;
    final double[] dest = new double[size()];
    final int destPos = n <= 0 ? 0 : n;
    final int length = n >= 0 ? size() - n : size() + n;

    for (int i = 0; i < size(); i++) {
      dest[i] = FloatColumnType.missingValueIndicator();
    }

    double[] array = data.toDoubleArray();

    System.arraycopy(array, srcPos, dest, destPos, length);
    return new DoubleColumn(name() + " lag(" + n + ")", new DoubleArrayList(dest));
  }

  @Override
  public DoubleColumn removeMissing() {
    DoubleColumn result = copy();
    result.clear();
    DoubleListIterator iterator = data.iterator();
    while (iterator.hasNext()) {
      double v = iterator.nextDouble();
      if (!isMissingValue(v)) {
        result.append(v);
      }
    }
    return result;
  }

  /** Adds the given float to this column */
  public DoubleColumn append(final float f) {
    data.add(f);
    return this;
  }

  /** Adds the given double to this column */
  public DoubleColumn append(double d) {
    data.add(d);
    return this;
  }

  public DoubleColumn append(int i) {
    data.add(i);
    return this;
  }

  @Override
  public DoubleColumn append(Double val) {
    if (val == null) {
      appendMissing();
    } else {
      append(val.doubleValue());
    }
    return this;
  }

  public DoubleColumn append(Number val) {
    if (val == null) {
      appendMissing();
    } else {
      append(val.doubleValue());
    }
    return this;
  }

  @Override
  public DoubleColumn copy() {
    return new DoubleColumn(name(), data.clone());
  }

  @Override
  public Iterator<Double> iterator() {
    return (Iterator<Double>) data.iterator();
  }

  @Override
  public Double[] asObjectArray() {
    final Double[] output = new Double[size()];
    for (int i = 0; i < size(); i++) {
      if (!isMissing(i)) {
        output[i] = getDouble(i);
      } else {
        output[i] = null;
      }
    }
    return output;
  }

  @Override
  public int compare(Double o1, Double o2) {
    return Double.compare(o1, o2);
  }

  @Override
  public DoubleColumn set(int i, Double val) {
    return val == null ? setMissing(i) : set(i, (double) val);
  }

  public DoubleColumn set(int i, double val) {
    data.set(i, val);
    return this;
  }

  /**
   * Updates this column where values matching the selection are replaced with the corresponding
   * value from the given column
   */
  public DoubleColumn set(DoublePredicate condition, NumericColumn<?> other) {
    for (int row = 0; row < size(); row++) {
      if (condition.test(getDouble(row))) {
        set(row, other.getDouble(row));
      }
    }
    return this;
  }

  @Override
  public Column<Double> set(int row, String stringValue, AbstractColumnParser<?> parser) {
    return set(row, parser.parseDouble(stringValue));
  }

  @Override
  public DoubleColumn append(final Column<Double> column) {
    Preconditions.checkArgument(column.type() == this.type());
    final DoubleColumn numberColumn = (DoubleColumn) column;
    final int size = numberColumn.size();
    for (int i = 0; i < size; i++) {
      append(numberColumn.getDouble(i));
    }
    return this;
  }

  @Override
  public DoubleColumn append(Column<Double> column, int row) {
    Preconditions.checkArgument(column.type() == this.type());
    DoubleColumn doubleColumn = (DoubleColumn) column;
    return append(doubleColumn.getDouble(row));
  }

  @Override
  public DoubleColumn set(int row, Column<Double> column, int sourceRow) {
    Preconditions.checkArgument(column.type() == this.type());
    DoubleColumn doubleColumn = (DoubleColumn) column;
    return set(row, doubleColumn.getDouble(sourceRow));
  }

  /**
   * Returns a new NumberColumn with only those rows satisfying the predicate
   *
   * @param test the predicate
   * @return a new NumberColumn with only those rows satisfying the predicate
   */
  public DoubleColumn filter(DoublePredicate test) {
    DoubleColumn result = DoubleColumn.create(name());
    for (int i = 0; i < size(); i++) {
      double d = getDouble(i);
      if (test.test(d)) {
        result.append(d);
      }
    }
    return result;
  }

  @Override
  public byte[] asBytes(int rowNumber) {
    return ByteBuffer.allocate(DoubleColumnType.instance().byteSize())
        .putDouble(getDouble(rowNumber))
        .array();
  }

  @Override
  public int countUnique() {
    DoubleSet uniqueElements = new DoubleOpenHashSet();
    for (int i = 0; i < size(); i++) {
      uniqueElements.add(getDouble(i));
    }
    return uniqueElements.size();
  }

  @Override
  public double getDouble(int row) {
    return data.getDouble(row);
  }

  public boolean isMissingValue(double value) {
    return DoubleColumnType.valueIsMissing(value);
  }

  @Override
  public boolean isMissing(int rowNumber) {
    return isMissingValue(getDouble(rowNumber));
  }

  @Override
  public void sortAscending() {
    data.sort(DoubleComparators.NATURAL_COMPARATOR);
  }

  @Override
  public void sortDescending() {
    data.sort(DoubleComparators.OPPOSITE_COMPARATOR);
  }

  @Override
  public DoubleColumn appendMissing() {
    return append(DoubleColumnType.missingValueIndicator());
  }

  @Override
  public DoubleColumn appendObj(Object obj) {
    if (obj == null) {
      return appendMissing();
    }
    if (obj instanceof Double) {
      return append((double) obj);
    }
    if (obj instanceof BigDecimal) {
      return append(((BigDecimal) obj).doubleValue());
    }
    throw new IllegalArgumentException("Could not append " + obj.getClass());
  }

  @Override
  public DoubleColumn appendCell(final String value) {
    try {
      return append(DoubleColumnType.DEFAULT_PARSER.parseDouble(value));
    } catch (final NumberFormatException e) {
      throw new NumberFormatException(
          "Error adding value to column " + name() + ": " + e.getMessage());
    }
  }

  @Override
  public DoubleColumn appendCell(final String value, AbstractColumnParser<?> parser) {
    try {
      return append(parser.parseDouble(value));
    } catch (final NumberFormatException e) {
      throw new NumberFormatException(
          "Error adding value to column " + name() + ": " + e.getMessage());
    }
  }

  @Override
  public String getUnformattedString(final int row) {
    final double value = getDouble(row);
    if (DoubleColumnType.valueIsMissing(value)) {
      return "";
    }
    return String.valueOf(value);
  }

  // fillWith methods

  @Override
  public DoubleColumn fillWith(final DoubleIterator iterator) {
    for (int r = 0; r < size(); r++) {
      if (!iterator.hasNext()) {
        break;
      }
      set(r, iterator.nextDouble());
    }
    return this;
  }

  @Override
  public DoubleColumn fillWith(final DoubleRangeIterable iterable) {
    DoubleIterator iterator = iterable.iterator();
    for (int r = 0; r < size(); r++) {
      if (!iterator.hasNext()) {
        iterator = iterable.iterator();
        if (!iterator.hasNext()) {
          break;
        }
      }
      set(r, iterator.nextDouble());
    }
    return this;
  }

  @Override
  public DoubleColumn fillWith(final DoubleSupplier supplier) {
    for (int r = 0; r < size(); r++) {
      try {
        set(r, supplier.getAsDouble());
      } catch (final Exception e) {
        break;
      }
    }
    return this;
  }

  @Override
  public DoubleColumn fillWith(double d) {
    for (int r = 0; r < size(); r++) {
      set(r, d);
    }
    return this;
  }

  /**
   * Returns a new LongColumn containing a value for each value in this column, truncating if
   * necessary
   *
   * <p>A narrowing primitive conversion such as this one may lose information about the overall
   * magnitude of a numeric value and may also lose precision and range. Specifically, if the value
   * is too small (a negative value of large magnitude or negative infinity), the result is the
   * smallest representable value of type long.
   *
   * <p>Similarly, if the value is too large (a positive value of large magnitude or positive
   * infinity), the result is the largest representable value of type long.
   *
   * <p>Despite the fact that overflow, underflow, or other loss of information may occur, a
   * narrowing primitive conversion never results in a run-time exception.
   *
   * <p>A missing value in the receiver is converted to a missing value in the result
   */
  @Override
  public LongColumn asLongColumn() {
    LongColumn result = LongColumn.create(name());
    for (double d : data) {
      if (DoubleColumnType.valueIsMissing(d)) {
        result.appendMissing();
      } else {
        result.append((long) d);
      }
    }
    return result;
  }

  /**
   * Returns a new IntColumn containing a value for each value in this column, truncating if
   * necessary.
   *
   * <p>A narrowing primitive conversion such as this one may lose information about the overall
   * magnitude of a numeric value and may also lose precision and range. Specifically, if the value
   * is too small (a negative value of large magnitude or negative infinity), the result is the
   * smallest representable value of type int.
   *
   * <p>Similarly, if the value is too large (a positive value of large magnitude or positive
   * infinity), the result is the largest representable value of type int.
   *
   * <p>Despite the fact that overflow, underflow, or other loss of information may occur, a
   * narrowing primitive conversion never results in a run-time exception.
   *
   * <p>A missing value in the receiver is converted to a missing value in the result
   */
  @Override
  public IntColumn asIntColumn() {
    IntColumn result = IntColumn.create(name());
    for (double d : data) {
      if (DoubleColumnType.valueIsMissing(d)) {
        result.appendMissing();
      } else {
        result.append((int) d);
      }
    }
    return result;
  }

  /**
   * Returns a new ShortColumn containing a value for each value in this column, truncating if
   * necessary.
   *
   * <p>A narrowing primitive conversion such as this one may lose information about the overall
   * magnitude of a numeric value and may also lose precision and range. Specifically, if the value
   * is too small (a negative value of large magnitude or negative infinity), the result is the
   * smallest representable value of type int.
   *
   * <p>Similarly, if the value is too large (a positive value of large magnitude or positive
   * infinity), the result is the largest representable value of type short.
   *
   * <p>Despite the fact that overflow, underflow, or other loss of information may occur, a
   * narrowing primitive conversion never results in a run-time exception.
   *
   * <p>A missing value in the receiver is converted to a missing value in the result
   */
  @Override
  public ShortColumn asShortColumn() {
    ShortColumn result = ShortColumn.create(name());
    for (double d : data) {
      if (DoubleColumnType.valueIsMissing(d)) {
        result.appendMissing();
      } else {
        result.append((short) d);
      }
    }
    return result;
  }

  /**
   * Returns a new FloatColumn containing a value for each value in this column, truncating if
   * necessary.
   *
   * <p>A narrowing primitive conversion such as this one may lose information about the overall
   * magnitude of a numeric value and may also lose precision and range. Specifically, if the value
   * is too small (a negative value of large magnitude or negative infinity), the result is the
   * smallest representable value of type float.
   *
   * <p>Similarly, if the value is too large (a positive value of large magnitude or positive
   * infinity), the result is the largest representable value of type float.
   *
   * <p>Despite the fact that overflow, underflow, or other loss of information may occur, a
   * narrowing primitive conversion never results in a run-time exception.
   *
   * <p>A missing value in the receiver is converted to a missing value in the result
   */
  @Override
  public FloatColumn asFloatColumn() {
    FloatColumn result = FloatColumn.create(name());
    for (double d : data) {
      if (DoubleColumnType.valueIsMissing(d)) {
        result.appendMissing();
      } else {
        result.append((float) d);
      }
    }
    return result;
  }
}
