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

import com.datastax.oss.driver.api.core.CqlIdentifier;
import com.datastax.oss.driver.api.core.context.DriverContext;
import com.datastax.oss.driver.api.core.metadata.token.Token;
import com.datastax.oss.driver.api.core.type.DataType;
import com.datastax.oss.driver.api.core.type.DataTypes;
import com.datastax.oss.driver.api.core.type.UserDefinedType;
import com.datastax.oss.driver.api.core.type.codec.CodecNotFoundException;
import com.datastax.oss.driver.api.core.type.codec.TypeCodec;
import com.datastax.oss.driver.api.core.type.codec.registry.CodecRegistry;
import com.datastax.oss.driver.api.querybuilder.delete.DeleteSelection;
import com.datastax.oss.driver.api.querybuilder.insert.InsertInto;
import com.datastax.oss.driver.api.querybuilder.relation.Relation;
import com.datastax.oss.driver.api.querybuilder.select.SelectFrom;
import com.datastax.oss.driver.api.querybuilder.select.Selector;
import com.datastax.oss.driver.api.querybuilder.term.Term;
import com.datastax.oss.driver.api.querybuilder.truncate.Truncate;
import com.datastax.oss.driver.api.querybuilder.update.UpdateStart;
import com.datastax.oss.driver.internal.core.metadata.schema.ShallowUserDefinedType;
import com.datastax.oss.driver.internal.core.metadata.token.ByteOrderedToken;
import com.datastax.oss.driver.internal.core.metadata.token.Murmur3Token;
import com.datastax.oss.driver.internal.core.metadata.token.RandomToken;
import com.datastax.oss.driver.internal.querybuilder.ArithmeticOperator;
import com.datastax.oss.driver.internal.querybuilder.DefaultLiteral;
import com.datastax.oss.driver.internal.querybuilder.DefaultRaw;
import com.datastax.oss.driver.internal.querybuilder.delete.DefaultDelete;
import com.datastax.oss.driver.internal.querybuilder.insert.DefaultInsert;
import com.datastax.oss.driver.internal.querybuilder.select.DefaultBindMarker;
import com.datastax.oss.driver.internal.querybuilder.select.DefaultSelect;
import com.datastax.oss.driver.internal.querybuilder.term.BinaryArithmeticTerm;
import com.datastax.oss.driver.internal.querybuilder.term.FunctionTerm;
import com.datastax.oss.driver.internal.querybuilder.term.OppositeTerm;
import com.datastax.oss.driver.internal.querybuilder.term.TupleTerm;
import com.datastax.oss.driver.internal.querybuilder.term.TypeHintTerm;
import com.datastax.oss.driver.internal.querybuilder.truncate.DefaultTruncate;
import com.datastax.oss.driver.internal.querybuilder.update.DefaultUpdate;
import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.Nullable;
import java.util.Arrays;

/** A Domain-Specific Language to build CQL queries using Java code. */
public class QueryBuilder {

  /** Starts a SELECT query for a qualified table. */
  @NonNull
  public static SelectFrom selectFrom(
      @Nullable CqlIdentifier keyspace, @NonNull CqlIdentifier table) {
    return new DefaultSelect(keyspace, table);
  }

  /**
   * Shortcut for {@link #selectFrom(CqlIdentifier, CqlIdentifier)
   * selectFrom(CqlIdentifier.fromCql(keyspace), CqlIdentifier.fromCql(table))}
   */
  @NonNull
  public static SelectFrom selectFrom(@Nullable String keyspace, @NonNull String table) {
    return selectFrom(
        keyspace == null ? null : CqlIdentifier.fromCql(keyspace), CqlIdentifier.fromCql(table));
  }

  /** Starts a SELECT query for an unqualified table. */
  @NonNull
  public static SelectFrom selectFrom(@NonNull CqlIdentifier table) {
    return selectFrom(null, table);
  }

  /** Shortcut for {@link #selectFrom(CqlIdentifier) selectFrom(CqlIdentifier.fromCql(table))} */
  @NonNull
  public static SelectFrom selectFrom(@NonNull String table) {
    return selectFrom(CqlIdentifier.fromCql(table));
  }

  /** Starts an INSERT query for a qualified table. */
  @NonNull
  public static InsertInto insertInto(
      @Nullable CqlIdentifier keyspace, @NonNull CqlIdentifier table) {
    return new DefaultInsert(keyspace, table);
  }

  /**
   * Shortcut for {@link #insertInto(CqlIdentifier, CqlIdentifier)
   * insertInto(CqlIdentifier.fromCql(keyspace), CqlIdentifier.fromCql(table))}.
   */
  @NonNull
  public static InsertInto insertInto(@Nullable String keyspace, @NonNull String table) {
    return insertInto(
        keyspace == null ? null : CqlIdentifier.fromCql(keyspace), CqlIdentifier.fromCql(table));
  }

  /** Starts an INSERT query for an unqualified table. */
  @NonNull
  public static InsertInto insertInto(@NonNull CqlIdentifier table) {
    return insertInto(null, table);
  }

  /** Shortcut for {@link #insertInto(CqlIdentifier) insertInto(CqlIdentifier.fromCql(table))}. */
  @NonNull
  public static InsertInto insertInto(@NonNull String table) {
    return insertInto(CqlIdentifier.fromCql(table));
  }

  /** Starts an UPDATE query for a qualified table. */
  @NonNull
  public static UpdateStart update(@Nullable CqlIdentifier keyspace, @NonNull CqlIdentifier table) {
    return new DefaultUpdate(keyspace, table);
  }

  /**
   * Shortcut for {@link #update(CqlIdentifier, CqlIdentifier)
   * update(CqlIdentifier.fromCql(keyspace), CqlIdentifier.fromCql(table))}
   */
  @NonNull
  public static UpdateStart update(@Nullable String keyspace, @NonNull String table) {
    return update(
        keyspace == null ? null : CqlIdentifier.fromCql(keyspace), CqlIdentifier.fromCql(table));
  }

  /** Starts an UPDATE query for an unqualified table. */
  @NonNull
  public static UpdateStart update(@NonNull CqlIdentifier table) {
    return update(null, table);
  }

  /** Shortcut for {@link #update(CqlIdentifier) update(CqlIdentifier.fromCql(table))} */
  @NonNull
  public static UpdateStart update(@NonNull String table) {
    return update(CqlIdentifier.fromCql(table));
  }

  /** Starts a DELETE query for a qualified table. */
  @NonNull
  public static DeleteSelection deleteFrom(
      @Nullable CqlIdentifier keyspace, @NonNull CqlIdentifier table) {
    return new DefaultDelete(keyspace, table);
  }

  /**
   * Shortcut for {@link #deleteFrom(CqlIdentifier, CqlIdentifier)
   * deleteFrom(CqlIdentifier.fromCql(keyspace), CqlIdentifier.fromCql(table))}
   */
  @NonNull
  public static DeleteSelection deleteFrom(@Nullable String keyspace, @NonNull String table) {
    return deleteFrom(
        keyspace == null ? null : CqlIdentifier.fromCql(keyspace), CqlIdentifier.fromCql(table));
  }

  /** Starts a DELETE query for an unqualified table. */
  @NonNull
  public static DeleteSelection deleteFrom(@NonNull CqlIdentifier table) {
    return deleteFrom(null, table);
  }

  /** Shortcut for {@link #deleteFrom(CqlIdentifier) deleteFrom(CqlIdentifier.fromCql(table))} */
  @NonNull
  public static DeleteSelection deleteFrom(@NonNull String table) {
    return deleteFrom(CqlIdentifier.fromCql(table));
  }

  /**
   * An ordered set of anonymous terms, as in {@code WHERE (a, b) = (1, 2)} (on the right-hand
   * side).
   *
   * <p>For example, this can be used as the right operand of {@link Relation#columns(String...)}.
   */
  @NonNull
  public static Term tuple(@NonNull Iterable<? extends Term> components) {
    return new TupleTerm(components);
  }

  /** Var-arg equivalent of {@link #tuple(Iterable)}. */
  @NonNull
  public static Term tuple(@NonNull Term... components) {
    return tuple(Arrays.asList(components));
  }

  /** The sum of two terms, as in {@code WHERE k = left + right}. */
  @NonNull
  public static Term add(@NonNull Term left, @NonNull Term right) {
    return new BinaryArithmeticTerm(ArithmeticOperator.SUM, left, right);
  }

  /** The difference of two terms, as in {@code WHERE k = left - right}. */
  @NonNull
  public static Term subtract(@NonNull Term left, @NonNull Term right) {
    return new BinaryArithmeticTerm(ArithmeticOperator.DIFFERENCE, left, right);
  }

  /** The product of two terms, as in {@code WHERE k = left * right}. */
  @NonNull
  public static Term multiply(@NonNull Term left, @NonNull Term right) {
    return new BinaryArithmeticTerm(ArithmeticOperator.PRODUCT, left, right);
  }

  /** The quotient of two terms, as in {@code WHERE k = left / right}. */
  @NonNull
  public static Term divide(@NonNull Term left, @NonNull Term right) {
    return new BinaryArithmeticTerm(ArithmeticOperator.QUOTIENT, left, right);
  }

  /** The remainder of two terms, as in {@code WHERE k = left % right}. */
  @NonNull
  public static Term remainder(@NonNull Term left, @NonNull Term right) {
    return new BinaryArithmeticTerm(ArithmeticOperator.REMAINDER, left, right);
  }

  /** The opposite of a term, as in {@code WHERE k = -argument}. */
  @NonNull
  public static Term negate(@NonNull Term argument) {
    return new OppositeTerm(argument);
  }

  /** A function call as a term, as in {@code WHERE k = f(arguments)}. */
  @NonNull
  public static Term function(
      @NonNull CqlIdentifier functionId, @NonNull Iterable<Term> arguments) {
    return function(null, functionId, arguments);
  }

  /** Var-arg equivalent of {@link #function(CqlIdentifier, Iterable)}. */
  @NonNull
  public static Term function(@NonNull CqlIdentifier functionId, @NonNull Term... arguments) {
    return function(functionId, Arrays.asList(arguments));
  }

  /**
   * Shortcut for {@link #function(CqlIdentifier, Iterable)
   * function(CqlIdentifier.fromCql(functionName), arguments)}.
   */
  @NonNull
  public static Term function(@NonNull String functionName, @NonNull Iterable<Term> arguments) {
    return function(CqlIdentifier.fromCql(functionName), arguments);
  }

  /**
   * Shortcut for {@link #function(CqlIdentifier, Term...)
   * function(CqlIdentifier.fromCql(functionName), arguments)}.
   */
  @NonNull
  public static Term function(@NonNull String functionName, @NonNull Term... arguments) {
    return function(CqlIdentifier.fromCql(functionName), arguments);
  }

  /** A function call as a term, as in {@code WHERE k = ks.f(arguments)}. */
  @NonNull
  public static Term function(
      @Nullable CqlIdentifier keyspaceId,
      @NonNull CqlIdentifier functionId,
      @NonNull Iterable<Term> arguments) {
    return new FunctionTerm(keyspaceId, functionId, arguments);
  }

  /** Var-arg equivalent of {@link #function(CqlIdentifier, CqlIdentifier, Iterable)}. */
  @NonNull
  public static Term function(
      @Nullable CqlIdentifier keyspaceId,
      @NonNull CqlIdentifier functionId,
      @NonNull Term... arguments) {
    return function(keyspaceId, functionId, Arrays.asList(arguments));
  }

  /**
   * Shortcut for {@link #function(CqlIdentifier, CqlIdentifier, Iterable)
   * function(CqlIdentifier.fromCql(keyspaceName), CqlIdentifier.fromCql(functionName), arguments)}.
   */
  @NonNull
  public static Term function(
      @Nullable String keyspaceName,
      @NonNull String functionName,
      @NonNull Iterable<Term> arguments) {
    return function(
        keyspaceName == null ? null : CqlIdentifier.fromCql(keyspaceName),
        CqlIdentifier.fromCql(functionName),
        arguments);
  }

  /**
   * Shortcut for {@link #function(CqlIdentifier, CqlIdentifier, Term...)
   * function(CqlIdentifier.fromCql(keyspaceName), CqlIdentifier.fromCql(functionName), arguments)}.
   */
  @NonNull
  public static Term function(
      @Nullable String keyspaceName, @NonNull String functionName, @NonNull Term... arguments) {
    return function(
        keyspaceName == null ? null : CqlIdentifier.fromCql(keyspaceName),
        CqlIdentifier.fromCql(functionName),
        arguments);
  }

  /**
   * Provides a type hint for an expression, as in {@code WHERE k = (double)1/3}.
   *
   * <p>To create the data type, use the constants and static methods in {@link DataTypes}, or
   * {@link #udt(CqlIdentifier)}.
   */
  @NonNull
  public static Term typeHint(@NonNull Term term, @NonNull DataType targetType) {
    return new TypeHintTerm(term, targetType);
  }

  /** A call to the built-in {@code now} function as a term. */
  @NonNull
  public static Term now() {
    return function("now");
  }

  /** A call to the built-in {@code currentTimestamp} function as a term. */
  @NonNull
  public static Term currentTimestamp() {
    return function("currenttimestamp");
  }

  /** A call to the built-in {@code currentDate} function as a term. */
  @NonNull
  public static Term currentDate() {
    return function("currentdate");
  }

  /** A call to the built-in {@code currentTime} function as a term. */
  @NonNull
  public static Term currentTime() {
    return function("currenttime");
  }

  /** A call to the built-in {@code currentTimeUuid} function as a term. */
  @NonNull
  public static Term currentTimeUuid() {
    return function("currenttimeuuid");
  }

  /** A call to the built-in {@code minTimeUuid} function as a term. */
  @NonNull
  public static Term minTimeUuid(@NonNull Term argument) {
    return function("mintimeuuid", argument);
  }

  /** A call to the built-in {@code maxTimeUuid} function as a term. */
  @NonNull
  public static Term maxTimeUuid(@NonNull Term argument) {
    return function("maxtimeuuid", argument);
  }

  /** A call to the built-in {@code toDate} function as a term. */
  @NonNull
  public static Term toDate(@NonNull Term argument) {
    return function("todate", argument);
  }

  /** A call to the built-in {@code toTimestamp} function as a term. */
  @NonNull
  public static Term toTimestamp(@NonNull Term argument) {
    return function("totimestamp", argument);
  }

  /** A call to the built-in {@code toUnixTimestamp} function as a term. */
  @NonNull
  public static Term toUnixTimestamp(@NonNull Term argument) {
    return function("tounixtimestamp", argument);
  }

  /**
   * A literal term, as in {@code WHERE k = 1}.
   *
   * <p>This method can process any type for which there is a default Java to CQL mapping, namely:
   * primitive types ({@code Integer=>int, Long=>bigint, String=>text, etc.}), and collections,
   * tuples, and user defined types thereof.
   *
   * <p>A null argument will be rendered as {@code NULL}.
   *
   * <p>For custom mappings, use {@link #literal(Object, CodecRegistry)} or {@link #literal(Object,
   * TypeCodec)}.
   *
   * @throws CodecNotFoundException if there is no default CQL mapping for the Java type of {@code
   *     value}.
   */
  @NonNull
  public static Literal literal(@Nullable Object value) {
    return literal(value, CodecRegistry.DEFAULT);
  }

  /**
   * A literal term, as in {@code WHERE k = 1}.
   *
   * <p>This is an alternative to {@link #literal(Object)} for custom type mappings. The provided
   * registry should contain a codec that can format the value. Typically, this will be your
   * session's registry, which is accessible via {@code session.getContext().getCodecRegistry()}.
   *
   * @see DriverContext#getCodecRegistry()
   * @throws CodecNotFoundException if {@code codecRegistry} does not contain any codec that can
   *     handle {@code value}.
   */
  @NonNull
  public static Literal literal(@Nullable Object value, @NonNull CodecRegistry codecRegistry) {
    if (value instanceof Murmur3Token) {
      value = ((Murmur3Token) value).getValue();
    } else if (value instanceof ByteOrderedToken) {
      value = ((ByteOrderedToken) value).getValue();
    } else if (value instanceof RandomToken) {
      value = ((RandomToken) value).getValue();
    } else if (value instanceof Token) {
      throw new IllegalArgumentException("Unsupported token type: " + value.getClass().getName());
    }
    try {
      return literal(value, (value == null) ? null : codecRegistry.codecFor(value));
    } catch (CodecNotFoundException e) {
      assert value != null;
      throw new IllegalArgumentException(
          String.format(
              "Could not inline literal of type %s. "
                  + "This happens because the driver doesn't know how to map it to a CQL type. "
                  + "Try passing a TypeCodec or CodecRegistry to literal().",
              value.getClass().getName()),
          e);
    }
  }

  /**
   * A literal term, as in {@code WHERE k = 1}.
   *
   * <p>This is an alternative to {@link #literal(Object)} for custom type mappings. The value will
   * be turned into a string with {@link TypeCodec#format(Object)}, and inlined in the query.
   */
  @NonNull
  public static <T> Literal literal(@Nullable T value, @Nullable TypeCodec<T> codec) {
    // Don't handle Token here, if the user calls this directly we assume they passed a codec that
    // can handle the value
    return new DefaultLiteral<>(value, codec);
  }

  /**
   * A raw CQL snippet.
   *
   * <p>The contents will be appended to the query as-is, without any syntax checking or escaping.
   * This method should be used with caution, as it's possible to generate invalid CQL that will
   * fail at execution time; on the other hand, it can be used as a workaround to handle new CQL
   * features that are not yet covered by the query builder.
   */
  @NonNull
  public static Raw raw(@NonNull String raw) {
    return new DefaultRaw(raw);
  }

  /** Creates an anonymous bind marker, which appears as {@code ?} in the generated CQL. */
  @NonNull
  public static BindMarker bindMarker() {
    return bindMarker((CqlIdentifier) null);
  }

  /** Creates a named bind marker, which appears as {@code :id} in the generated CQL. */
  @NonNull
  public static BindMarker bindMarker(@Nullable CqlIdentifier id) {
    return new DefaultBindMarker(id);
  }

  /** Shortcut for {@link #bindMarker(CqlIdentifier) bindMarker(CqlIdentifier.fromCql(name))} */
  @NonNull
  public static BindMarker bindMarker(@Nullable String name) {
    return bindMarker(name == null ? null : CqlIdentifier.fromCql(name));
  }

  /**
   * Shortcut to reference a UDT in methods that use a {@link DataType}, such as {@link
   * #typeHint(Term, DataType)} and {@link Selector#cast(Selector, DataType)}.
   */
  @NonNull
  public static UserDefinedType udt(@NonNull CqlIdentifier name) {
    return new ShallowUserDefinedType(null, name, false);
  }

  /** Shortcut for {@link #udt(CqlIdentifier) udt(CqlIdentifier.fromCql(name))}. */
  @NonNull
  public static UserDefinedType udt(@NonNull String name) {
    return udt(CqlIdentifier.fromCql(name));
  }

  /**
   * Creates a new {@code TRUNCATE} query.
   *
   * @param table the name of the table to truncate.
   * @return the truncation query.
   */
  public static Truncate truncate(@NonNull CqlIdentifier table) {
    return truncate(null, table);
  }

  /**
   * Creates a new {@code TRUNCATE} query.
   *
   * <p>This is a shortcut for {@link #truncate(CqlIdentifier)
   * truncate(CqlIdentifier.fromCql(table))}.
   *
   * @param table the name of the table to truncate.
   * @return the truncation query.
   */
  public static Truncate truncate(@NonNull String table) {
    return truncate(null, CqlIdentifier.fromCql(table));
  }

  /**
   * Creates a new {@code TRUNCATE} query.
   *
   * @param keyspace the name of the keyspace to use.
   * @param table the name of the table to truncate.
   * @return the truncation query.
   */
  public static Truncate truncate(@Nullable CqlIdentifier keyspace, @NonNull CqlIdentifier table) {
    return new DefaultTruncate(keyspace, table);
  }

  /**
   * Creates a new {@code TRUNCATE} query.
   *
   * <p>This is a shortcut for {@link #truncate(CqlIdentifier, CqlIdentifier)
   * truncate(CqlIdentifier.fromCql(keyspace), CqlIdentifier.fromCql(table))}.
   *
   * @param keyspace the name of the keyspace to use.
   * @param table the name of the table to truncate.
   * @return the truncation query.
   */
  public static Truncate truncate(@Nullable String keyspace, @NonNull String table) {
    return truncate(
        keyspace == null ? null : CqlIdentifier.fromCql(keyspace), CqlIdentifier.fromCql(table));
  }
}
