/*
 * 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.driver.core;

import com.datastax.driver.core.exceptions.InvalidTypeException;
import com.google.common.collect.ImmutableList;
import java.util.Arrays;
import java.util.List;

/**
 * A tuple type.
 *
 * <p>A tuple type is a essentially a list of types.
 */
public class TupleType extends DataType {

  private final List<DataType> types;
  private final ProtocolVersion protocolVersion;
  private volatile CodecRegistry codecRegistry;

  TupleType(List<DataType> types, ProtocolVersion protocolVersion, CodecRegistry codecRegistry) {
    super(DataType.Name.TUPLE);
    this.types = ImmutableList.copyOf(types);
    this.protocolVersion = protocolVersion;
    this.codecRegistry = codecRegistry;
  }

  /**
   * Creates a "disconnected" tuple type (<b>you should prefer {@link
   * Metadata#newTupleType(DataType...) cluster.getMetadata().newTupleType(...)} whenever
   * possible</b>).
   *
   * <p>This method is only exposed for situations where you don't have a {@code Cluster} instance
   * available. If you create a type with this method and use it with a {@code Cluster} later, you
   * won't be able to set tuple fields with custom codecs registered against the cluster, or you
   * might get errors if the protocol versions don't match.
   *
   * @param protocolVersion the protocol version to use.
   * @param codecRegistry the codec registry to use.
   * @param types the types for the tuple type.
   * @return the newly created tuple type.
   */
  public static TupleType of(
      ProtocolVersion protocolVersion, CodecRegistry codecRegistry, DataType... types) {
    return new TupleType(Arrays.asList(types), protocolVersion, codecRegistry);
  }

  /**
   * The (immutable) list of types composing this tuple type.
   *
   * @return the (immutable) list of types composing this tuple type.
   */
  public List<DataType> getComponentTypes() {
    return types;
  }

  /**
   * Returns a new empty value for this tuple type.
   *
   * @return an empty (with all component to {@code null}) value for this user type definition.
   */
  public TupleValue newValue() {
    return new TupleValue(this);
  }

  /**
   * Returns a new value for this tuple type that uses the provided values for the components.
   *
   * <p>The numbers of values passed to this method must correspond to the number of components in
   * this tuple type. The {@code i}th parameter value will then be assigned to the {@code i}th
   * component of the resulting tuple value.
   *
   * @param values the values to use for the component of the resulting tuple.
   * @return a new tuple values based on the provided values.
   * @throws IllegalArgumentException if the number of {@code values} provided does not correspond
   *     to the number of components in this tuple type.
   * @throws InvalidTypeException if any of the provided value is not of the correct type for the
   *     component.
   */
  public TupleValue newValue(Object... values) {
    if (values.length != types.size())
      throw new IllegalArgumentException(
          String.format(
              "Invalid number of values. Expecting %d but got %d", types.size(), values.length));

    TupleValue t = newValue();
    for (int i = 0; i < values.length; i++) {
      DataType dataType = types.get(i);
      if (values[i] == null) t.setValue(i, null);
      else
        t.setValue(
            i, codecRegistry.codecFor(dataType, values[i]).serialize(values[i], protocolVersion));
    }
    return t;
  }

  @Override
  public boolean isFrozen() {
    return true;
  }

  /**
   * Return the protocol version that has been used to deserialize this tuple type, or that will be
   * used to serialize it. In most cases this should be the version currently in use by the cluster
   * instance that this tuple type belongs to, as reported by {@link
   * ProtocolOptions#getProtocolVersion()}.
   *
   * @return the protocol version that has been used to deserialize this tuple type, or that will be
   *     used to serialize it.
   */
  ProtocolVersion getProtocolVersion() {
    return protocolVersion;
  }

  CodecRegistry getCodecRegistry() {
    return codecRegistry;
  }

  void setCodecRegistry(CodecRegistry codecRegistry) {
    this.codecRegistry = codecRegistry;
  }

  @Override
  public int hashCode() {
    return Arrays.hashCode(new Object[] {name, types});
  }

  @Override
  public boolean equals(Object o) {
    if (!(o instanceof TupleType)) return false;

    TupleType d = (TupleType) o;
    return name == d.name && types.equals(d.types);
  }

  /**
   * Return {@code true} if this tuple type contains the given tuple type, and {@code false}
   * otherwise.
   *
   * <p>A tuple type is said to contain another one if the latter has fewer components than the
   * former, but all of them are of the same type. E.g. the type {@code tuple<int, text>} is
   * contained by the type {@code tuple<int, text, double>}.
   *
   * <p>A contained type can be seen as a "partial" view of a containing type, where the missing
   * components are supposed to be {@code null}.
   *
   * @param other the tuple type to compare against the current one
   * @return {@code true} if this tuple type contains the given tuple type, and {@code false}
   *     otherwise.
   */
  public boolean contains(TupleType other) {
    if (this.equals(other)) return true;
    if (other.types.size() > this.types.size()) return false;
    return types.subList(0, other.types.size()).equals(other.types);
  }

  @Override
  public String toString() {
    return "frozen<" + asFunctionParameterString() + ">";
  }

  @Override
  public String asFunctionParameterString() {
    StringBuilder sb = new StringBuilder();
    for (DataType type : types) {
      sb.append(sb.length() == 0 ? "tuple<" : ", ");
      sb.append(type.asFunctionParameterString());
    }
    return sb.append(">").toString();
  }
}
