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

import com.datastax.driver.core.CodecRegistry;
import com.datastax.driver.core.ColumnMetadata;
import com.datastax.driver.core.ConsistencyLevel;
import com.datastax.driver.core.Metadata;
import com.datastax.driver.core.ProtocolVersion;
import com.datastax.driver.core.RegularStatement;
import com.datastax.driver.core.Statement;
import com.datastax.driver.core.TypeCodec;
import com.datastax.driver.core.exceptions.CodecNotFoundException;
import com.datastax.driver.core.policies.RetryPolicy;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * Common ancestor to statements generated with the {@link QueryBuilder}.
 *
 * <p>The actual query string will be generated and cached the first time it is requested, which is
 * either when the driver tries to execute the query, or when you call certain public methods (for
 * example {@link RegularStatement#getQueryString(CodecRegistry)}, {@link #getObject(int,
 * CodecRegistry)}).
 *
 * <p>Whenever possible (and unless you call {@link #setForceNoValues(boolean)}, the builder will
 * try to handle values passed to its methods as standalone values bound to the query string with
 * placeholders. For instance:
 *
 * <pre>
 *     select().all().from("foo").where(eq("k", "the key"));
 *     // Is equivalent to:
 *     new SimpleStatement("SELECT * FROM foo WHERE k=?", "the key");
 * </pre>
 *
 * There are a few exceptions to this rule:
 *
 * <ul>
 *   <li>for fixed-size number types, the builder can't guess what the actual CQL type is.
 *       Standalone values are sent to Cassandra in their serialized form, and number types aren't
 *       all serialized in the same way, so picking the wrong type could lead to a query error;
 *   <li>if the value is a "special" element like a function call, it can't be serialized. This also
 *       applies to collections mixing special elements and regular objects.
 * </ul>
 *
 * In these cases, the builder will inline the value in the query string:
 *
 * <pre>
 *     select().all().from("foo").where(eq("k", 1));
 *     // Is equivalent to:
 *     new SimpleStatement("SELECT * FROM foo WHERE k=1");
 * </pre>
 *
 * One final thing to consider is {@link CodecRegistry custom codecs}. If you've registered codecs
 * to handle your own Java types against the cluster, then you can pass instances of those types to
 * query builder methods. But should the builder have to inline those values, it needs your codecs
 * to {@link TypeCodec#format(Object) convert them to string form}. That is why some of the public
 * methods of this class take a {@code CodecRegistry} as a parameter:
 *
 * <pre>
 *     BuiltStatement s = select().all().from("foo").where(eq("k", myCustomObject));
 *     // if we do this codecs will definitely be needed:
 *     s.forceNoValues(true);
 *     s.getQueryString(myCodecRegistry);
 * </pre>
 *
 * For convenience, there are no-arg versions of those methods that use {@link
 * CodecRegistry#DEFAULT_INSTANCE}. But you should only use them if you are sure that no custom
 * values will need to be inlined while building the statement, or if you have registered your
 * custom codecs with the default registry instance. Otherwise, you will get a {@link
 * CodecNotFoundException}.
 */
public abstract class BuiltStatement extends RegularStatement {

  private final List<ColumnMetadata> partitionKey;
  private final List<Object> routingKeyValues;
  final String keyspace;

  private boolean dirty;
  private String cache;
  private List<Object> values;
  Boolean isCounterOp;
  boolean hasNonIdempotentOps;

  // Whether the user has inputted bind markers. If that's the case, we never generate values as
  // it means the user meant for the statement to be prepared and we shouldn't add our own markers.
  boolean hasBindMarkers;
  private boolean forceNoValues;

  BuiltStatement(
      String keyspace, List<ColumnMetadata> partitionKey, List<Object> routingKeyValues) {
    this.partitionKey = partitionKey;
    this.routingKeyValues = routingKeyValues;
    this.keyspace = keyspace;
  }

  /**
   * @deprecated preserved for backward compatibility, use {@link Metadata#quoteIfNecessary(String)}
   *     instead.
   */
  @Deprecated
  protected static String escapeId(String ident) {
    return Metadata.quoteIfNecessary(ident);
  }

  @Override
  public String getQueryString(CodecRegistry codecRegistry) {
    maybeRebuildCache(codecRegistry);
    return cache;
  }

  /**
   * Returns the {@code i}th value as the Java type matching its CQL type.
   *
   * @param i the index to retrieve.
   * @param codecRegistry the codec registry that will be used if the statement must be rebuilt in
   *     order to determine if it has values, and Java objects must be inlined in the process (see
   *     {@link BuiltStatement} for more explanations on why this is so).
   * @return the value of the {@code i}th value of this statement.
   * @throws IllegalStateException if this statement does not have values.
   * @throws IndexOutOfBoundsException if {@code i} is not a valid index for this object.
   * @see #getObject(int)
   */
  public Object getObject(int i, CodecRegistry codecRegistry) {
    maybeRebuildCache(codecRegistry);
    if (values == null || values.isEmpty())
      throw new IllegalStateException("This statement does not have values");
    if (i < 0 || i >= values.size()) throw new ArrayIndexOutOfBoundsException(i);
    return values.get(i);
  }

  /**
   * Returns the {@code i}th value as the Java type matching its CQL type.
   *
   * <p>This method calls {@link #getObject(int, CodecRegistry)} with {@link
   * CodecRegistry#DEFAULT_INSTANCE}. It's safe to use if you don't use any custom codecs, or if
   * your custom codecs are in the default registry; otherwise, use the other method and provide the
   * registry that contains your codecs.
   *
   * @param i the index to retrieve.
   * @return the value of the {@code i}th value of this statement.
   * @throws IllegalStateException if this statement does not have values.
   * @throws IndexOutOfBoundsException if {@code i} is not a valid index for this object.
   */
  public Object getObject(int i) {
    return getObject(i, CodecRegistry.DEFAULT_INSTANCE);
  }

  private void maybeRebuildCache(CodecRegistry codecRegistry) {
    if (!dirty && cache != null) return;

    StringBuilder sb;
    values = null;

    if (hasBindMarkers || forceNoValues) {
      sb = buildQueryString(null, codecRegistry);
    } else {
      values = new ArrayList<Object>();
      sb = buildQueryString(values, codecRegistry);

      if (values.size() > 65535)
        throw new IllegalArgumentException(
            "Too many values for built statement, the maximum allowed is 65535");

      if (values.isEmpty()) values = null;
    }

    maybeAddSemicolon(sb);

    cache = sb.toString();
    dirty = false;
  }

  static StringBuilder maybeAddSemicolon(StringBuilder sb) {
    // Use the same test that String#trim() uses to determine
    // if a character is a whitespace character.
    int l = sb.length();
    while (l > 0 && sb.charAt(l - 1) <= ' ') l -= 1;
    if (l != sb.length()) sb.setLength(l);

    if (l == 0 || sb.charAt(l - 1) != ';') sb.append(';');
    return sb;
  }

  abstract StringBuilder buildQueryString(List<Object> variables, CodecRegistry codecRegistry);

  boolean isCounterOp() {
    return isCounterOp == null ? false : isCounterOp;
  }

  void setCounterOp(boolean isCounterOp) {
    this.isCounterOp = isCounterOp;
  }

  boolean hasNonIdempotentOps() {
    return hasNonIdempotentOps;
  }

  void setNonIdempotentOps() {
    hasNonIdempotentOps = true;
  }

  void setDirty() {
    dirty = true;
  }

  void checkForBindMarkers(Object value) {
    dirty = true;
    if (Utils.containsBindMarker(value)) hasBindMarkers = true;
  }

  void checkForBindMarkers(Utils.Appendeable value) {
    dirty = true;
    if (value != null && value.containsBindMarker()) hasBindMarkers = true;
  }

  // TODO: Correctly document the InvalidTypeException
  void maybeAddRoutingKey(String name, Object value) {
    if (routingKeyValues == null
        || name == null
        || value == null
        || Utils.containsSpecialValue(value)) return;

    for (int i = 0; i < partitionKey.size(); i++) {
      if (Utils.handleId(name).equals(partitionKey.get(i).getName())) {
        routingKeyValues.set(i, value);
        return;
      }
    }
  }

  @Override
  public ByteBuffer getRoutingKey(ProtocolVersion protocolVersion, CodecRegistry codecRegistry) {
    if (routingKeyValues == null) return null;
    ByteBuffer[] routingKeyParts = new ByteBuffer[partitionKey.size()];
    for (int i = 0; i < partitionKey.size(); i++) {
      Object value = routingKeyValues.get(i);
      if (value == null) return null;
      TypeCodec<Object> codec = codecRegistry.codecFor(partitionKey.get(i).getType(), value);
      routingKeyParts[i] = codec.serialize(value, protocolVersion);
    }
    return routingKeyParts.length == 1 ? routingKeyParts[0] : Utils.compose(routingKeyParts);
  }

  @Override
  public String getKeyspace() {
    return keyspace;
  }

  @Override
  public ByteBuffer[] getValues(ProtocolVersion protocolVersion, CodecRegistry codecRegistry) {
    maybeRebuildCache(codecRegistry);
    return values == null ? null : Utils.convert(values.toArray(), protocolVersion, codecRegistry);
  }

  @Override
  public boolean hasValues(CodecRegistry codecRegistry) {
    maybeRebuildCache(codecRegistry);
    return values != null;
  }

  @Override
  public Map<String, ByteBuffer> getNamedValues(
      ProtocolVersion protocolVersion, CodecRegistry codecRegistry) {
    // Built statements never return named values
    return null;
  }

  @Override
  public boolean usesNamedValues() {
    return false;
  }

  @Override
  public Boolean isIdempotent() {
    // If a value was forced with setIdempotent, it takes priority
    if (idempotent != null) return idempotent;

    // Otherwise return the computed value
    return !hasNonIdempotentOps();
  }

  @Override
  public String toString() {
    try {
      if (forceNoValues) return getQueryString();
      // 1) try first with all values inlined (will not work if some values require custom codecs,
      // or if the required codecs are registered in a different CodecRegistry instance than the
      // default one)
      return maybeAddSemicolon(buildQueryString(null, CodecRegistry.DEFAULT_INSTANCE)).toString();
    } catch (RuntimeException e1) {
      // 2) try next with bind markers for all values to avoid usage of custom codecs
      try {
        return maybeAddSemicolon(
                buildQueryString(new ArrayList<Object>(), CodecRegistry.DEFAULT_INSTANCE))
            .toString();
      } catch (RuntimeException e2) {
        // Ugly but we have absolutely no context to get the registry from
        return String.format(
            "built query (could not generate with default codec registry: %s)", e2.getMessage());
      }
    }
  }

  /**
   * Allows to force this builder to not generate values (through its {@code getValues()} method).
   *
   * <p>By default (and unless the protocol version 1 is in use, see below) and for performance
   * reasons, the query builder will not serialize all values provided to strings. This means that
   * {@link #getQueryString(CodecRegistry)} may return a query string with bind markers (where and
   * when is at the discretion of the builder) and {@link #getValues} will return the binary values
   * for those markers. This method allows to force the builder to not generate binary values but
   * rather to inline them all in the query string. In practice, this means that if you call {@code
   * setForceNoValues(true)}, you are guaranteed that {@code getValues()} will return {@code null}
   * and that the string returned by {@code getQueryString()} will contain no other bind markers
   * than the ones specified by the user.
   *
   * <p>If the native protocol version 1 is in use, the driver will default to not generating values
   * since those are not supported by that version of the protocol. In practice, the driver will
   * automatically call this method with {@code true} as argument prior to execution. Hence, calling
   * this method when the protocol version 1 is in use is basically a no-op.
   *
   * <p>Note that this method is mainly useful for debugging purpose. In general, the default
   * behavior should be the correct and most efficient one.
   *
   * @param forceNoValues whether or not this builder may generate values.
   * @return this statement.
   */
  public RegularStatement setForceNoValues(boolean forceNoValues) {
    this.forceNoValues = forceNoValues;
    this.dirty = true;
    return this;
  }

  /** An utility class to create a BuiltStatement that encapsulate another one. */
  abstract static class ForwardingStatement<T extends BuiltStatement> extends BuiltStatement {

    T statement;

    ForwardingStatement(T statement) {
      super(null, null, null);
      this.statement = statement;
    }

    @Override
    public String getQueryString(CodecRegistry codecRegistry) {
      return statement.getQueryString(codecRegistry);
    }

    @Override
    StringBuilder buildQueryString(List<Object> values, CodecRegistry codecRegistry) {
      return statement.buildQueryString(values, codecRegistry);
    }

    @Override
    public ByteBuffer getRoutingKey(ProtocolVersion protocolVersion, CodecRegistry codecRegistry) {
      return statement.getRoutingKey(protocolVersion, codecRegistry);
    }

    @Override
    public String getKeyspace() {
      return statement.getKeyspace();
    }

    @Override
    boolean isCounterOp() {
      return statement.isCounterOp();
    }

    @Override
    boolean hasNonIdempotentOps() {
      return statement.hasNonIdempotentOps();
    }

    @Override
    public RegularStatement setForceNoValues(boolean forceNoValues) {
      statement.setForceNoValues(forceNoValues);
      return this;
    }

    @Override
    public Statement setConsistencyLevel(ConsistencyLevel consistency) {
      statement.setConsistencyLevel(consistency);
      return this;
    }

    @Override
    public ConsistencyLevel getConsistencyLevel() {
      return statement.getConsistencyLevel();
    }

    @Override
    public Statement enableTracing() {
      statement.enableTracing();
      return this;
    }

    @Override
    public Statement disableTracing() {
      statement.disableTracing();
      return this;
    }

    @Override
    public boolean isTracing() {
      return statement.isTracing();
    }

    @Override
    public Statement setRetryPolicy(RetryPolicy policy) {
      statement.setRetryPolicy(policy);
      return this;
    }

    @Override
    public RetryPolicy getRetryPolicy() {
      return statement.getRetryPolicy();
    }

    @Override
    public ByteBuffer[] getValues(ProtocolVersion protocolVersion, CodecRegistry codecRegistry) {
      return statement.getValues(protocolVersion, codecRegistry);
    }

    @Override
    public boolean hasValues() {
      return statement.hasValues();
    }

    @Override
    void checkForBindMarkers(Object value) {
      statement.checkForBindMarkers(value);
    }

    @Override
    void checkForBindMarkers(Utils.Appendeable value) {
      statement.checkForBindMarkers(value);
    }

    @Override
    public String toString() {
      return statement.toString();
    }
  }
}
