/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.datastax.oss.driver.api.core.data;

import com.datastax.oss.driver.api.core.type.codec.TypeCodec;
import com.datastax.oss.driver.internal.core.type.codec.ParseUtils;
import com.datastax.oss.driver.shaded.guava.common.base.Preconditions;
import com.datastax.oss.driver.shaded.guava.common.base.Predicates;
import com.datastax.oss.driver.shaded.guava.common.collect.Iterables;
import edu.umd.cs.findbugs.annotations.NonNull;
import java.io.IOException;
import java.io.InvalidObjectException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.ObjectStreamException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.stream.Stream;

/**
 * Representation of a vector as defined in CQL.
 *
 * <p>A CQL vector is a fixed-length array of non-null numeric values. These properties don't map
 * cleanly to an existing class in the standard JDK Collections hierarchy so we provide this value
 * object instead. Like other value object collections returned by the driver instances of this
 * class are not immutable; think of these value objects as a representation of a vector stored in
 * the database as an initial step in some additional computation.
 *
 * <p>While we don't implement any Collection APIs we do implement Iterable. We also attempt to play
 * nice with the Streams API in order to better facilitate integration with data pipelines. Finally,
 * where possible we've tried to make the API of this class similar to the equivalent methods on
 * {@link List}.
 */
public class CqlVector<T> implements Iterable<T>, Serializable {

  /**
   * Create a new CqlVector containing the specified values.
   *
   * @param vals the collection of values to wrap.
   * @return a CqlVector wrapping those values
   */
  public static <V> CqlVector<V> newInstance(V... vals) {

    // Note that Array.asList() guarantees the return of an array which implements RandomAccess
    return new CqlVector(Arrays.asList(vals));
  }

  /**
   * Create a new CqlVector that "wraps" an existing ArrayList. Modifications to the passed
   * ArrayList will also be reflected in the returned CqlVector.
   *
   * @param list the collection of values to wrap.
   * @return a CqlVector wrapping those values
   */
  public static <V> CqlVector<V> newInstance(List<V> list) {
    Preconditions.checkArgument(list != null, "Input list should not be null");
    return new CqlVector(list);
  }

  /**
   * Create a new CqlVector instance from the specified string representation.
   *
   * @param str a String representation of a CqlVector
   * @param subtypeCodec the codec to use to parse the individual elements
   * @return a new CqlVector built from the String representation
   */
  public static <V> CqlVector<V> from(@NonNull String str, @NonNull TypeCodec<V> subtypeCodec) {
    Preconditions.checkArgument(str != null, "Cannot create CqlVector from null string");
    Preconditions.checkArgument(!str.isEmpty(), "Cannot create CqlVector from empty string");
    if (str.equalsIgnoreCase("NULL")) return null;

    int idx = ParseUtils.skipSpaces(str, 0);
    if (str.charAt(idx++) != '[')
      throw new IllegalArgumentException(
          String.format(
              "Cannot parse vector value from \"%s\", at character %d expecting '[' but got '%c'",
              str, idx, str.charAt(idx)));

    idx = ParseUtils.skipSpaces(str, idx);

    if (str.charAt(idx) == ']') {
      return new CqlVector<>(new ArrayList<>());
    }

    List<V> list = new ArrayList<>();
    while (idx < str.length()) {
      int n;
      try {
        n = ParseUtils.skipCQLValue(str, idx);
      } catch (IllegalArgumentException e) {
        throw new IllegalArgumentException(
            String.format(
                "Cannot parse vector value from \"%s\", invalid CQL value at character %d",
                str, idx),
            e);
      }

      list.add(subtypeCodec.parse(str.substring(idx, n)));
      idx = n;

      idx = ParseUtils.skipSpaces(str, idx);
      if (str.charAt(idx) == ']') return new CqlVector<>(list);
      if (str.charAt(idx++) != ',')
        throw new IllegalArgumentException(
            String.format(
                "Cannot parse vector value from \"%s\", at character %d expecting ',' but got '%c'",
                str, idx, str.charAt(idx)));

      idx = ParseUtils.skipSpaces(str, idx);
    }
    throw new IllegalArgumentException(
        String.format("Malformed vector value \"%s\", missing closing ']'", str));
  }

  private final List<T> list;

  private CqlVector(@NonNull List<T> list) {

    Preconditions.checkArgument(
        Iterables.all(list, Predicates.notNull()), "CqlVectors cannot contain null values");
    this.list = list;
  }

  /**
   * Retrieve the value at the specified index. Modelled after {@link List#get(int)}
   *
   * @param idx the index to retrieve
   * @return the value at the specified index
   */
  public T get(int idx) {
    return list.get(idx);
  }

  /**
   * Update the value at the specified index. Modelled after {@link List#set(int, Object)}
   *
   * @param idx the index to set
   * @param val the new value for the specified index
   * @return the old value for the specified index
   */
  public T set(int idx, T val) {
    return list.set(idx, val);
  }

  /**
   * Return the size of this vector. Modelled after {@link List#size()}
   *
   * @return the vector size
   */
  public int size() {
    return this.list.size();
  }

  /**
   * Return a CqlVector consisting of the contents of a portion of this vector. Modelled after
   * {@link List#subList(int, int)}
   *
   * @param from the index to start from (inclusive)
   * @param to the index to end on (exclusive)
   * @return a new CqlVector wrapping the sublist
   */
  public CqlVector<T> subVector(int from, int to) {
    return new CqlVector<T>(this.list.subList(from, to));
  }

  /**
   * Return a boolean indicating whether the vector is empty. Modelled after {@link List#isEmpty()}
   *
   * @return true if the list is empty, false otherwise
   */
  public boolean isEmpty() {
    return this.list.isEmpty();
  }

  /**
   * Create an {@link Iterator} for this vector
   *
   * @return the generated iterator
   */
  @Override
  public Iterator<T> iterator() {
    return this.list.iterator();
  }

  /**
   * Create a {@link Stream} of the values in this vector
   *
   * @return the Stream instance
   */
  public Stream<T> stream() {
    return this.list.stream();
  }

  @Override
  public boolean equals(Object o) {
    if (o == this) {
      return true;
    } else if (o instanceof CqlVector) {
      CqlVector that = (CqlVector) o;
      return this.list.equals(that.list);
    } else {
      return false;
    }
  }

  @Override
  public int hashCode() {
    return Objects.hash(list);
  }

  /**
   * The string representation of the vector. Elements, like strings, may not be properly quoted.
   *
   * @return the string representation
   */
  @Override
  public String toString() {
    return Iterables.toString(this.list);
  }

  /**
   * Serialization proxy for CqlVector. Allows serialization regardless of implementation of list
   * field.
   *
   * @param <T> inner type of CqlVector, assume Number is always Serializable.
   */
  private static class SerializationProxy<T> implements Serializable {

    private static final long serialVersionUID = 1;

    private transient List<T> list;

    SerializationProxy(CqlVector<T> vector) {
      this.list = vector.list;
    }

    // Reconstruct CqlVector's list of elements.
    private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException {
      stream.defaultReadObject();

      int size = stream.readInt();
      list = new ArrayList<>();
      for (int i = 0; i < size; i++) {
        list.add((T) stream.readObject());
      }
    }

    // Return deserialized proxy object as CqlVector.
    private Object readResolve() throws ObjectStreamException {
      return new CqlVector(list);
    }

    // Write size of CqlVector followed by items in order.
    private void writeObject(ObjectOutputStream stream) throws IOException {
      stream.defaultWriteObject();

      stream.writeInt(list.size());
      for (T item : list) {
        stream.writeObject(item);
      }
    }
  }

  /** @serialData The number of elements in the vector, followed by each element in-order. */
  private Object writeReplace() {
    return new SerializationProxy(this);
  }

  private void readObject(@SuppressWarnings("unused") ObjectInputStream stream)
      throws InvalidObjectException {
    // Should never be called since we serialized a proxy
    throw new InvalidObjectException("Proxy required");
  }
}
