/*
 * 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.policies.Policies;
import com.google.common.base.Joiner;
import java.util.ArrayList;
import java.util.List;

/**
 * The configuration of the cluster. It configures the following:
 *
 * <ul>
 *   <li>Cassandra protocol level configuration (compression).
 *   <li>Connection pooling configurations.
 *   <li>low-level TCP configuration options (tcpNoDelay, keepAlive, ...).
 *   <li>Metrics related options.
 *   <li>Query related options (default consistency level, fetchSize, ...).
 *   <li>Netty layer customization options.
 * </ul>
 *
 * This is also where you get the configured policies, though those cannot be changed (they are set
 * during the built of the Cluster object).
 */
public class Configuration {

  /**
   * Returns a builder to create a new {@code Configuration} object.
   *
   * <p>You only need this if you are building the configuration yourself. If you use {@link
   * Cluster#builder()}, it will be done under the hood for you.
   *
   * @return the builder.
   */
  public static Builder builder() {
    return new Builder();
  }

  private final Policies policies;
  private final ProtocolOptions protocolOptions;
  private final PoolingOptions poolingOptions;
  private final SocketOptions socketOptions;
  private final MetricsOptions metricsOptions;
  private final QueryOptions queryOptions;
  private final ThreadingOptions threadingOptions;
  private final NettyOptions nettyOptions;
  private final CodecRegistry codecRegistry;
  private final String defaultKeyspace;

  private Configuration(
      Policies policies,
      ProtocolOptions protocolOptions,
      PoolingOptions poolingOptions,
      SocketOptions socketOptions,
      MetricsOptions metricsOptions,
      QueryOptions queryOptions,
      ThreadingOptions threadingOptions,
      NettyOptions nettyOptions,
      CodecRegistry codecRegistry,
      String defaultKeyspace) {
    this.policies = policies;
    this.protocolOptions = protocolOptions;
    this.poolingOptions = poolingOptions;
    this.socketOptions = socketOptions;
    this.metricsOptions = metricsOptions;
    this.queryOptions = queryOptions;
    this.threadingOptions = threadingOptions;
    this.nettyOptions = nettyOptions;
    this.codecRegistry = codecRegistry;
    this.defaultKeyspace = defaultKeyspace;
  }

  /**
   * Copy constructor.
   *
   * @param toCopy the object to copy from.
   */
  protected Configuration(Configuration toCopy) {
    this(
        toCopy.getPolicies(),
        toCopy.getProtocolOptions(),
        toCopy.getPoolingOptions(),
        toCopy.getSocketOptions(),
        toCopy.getMetricsOptions(),
        toCopy.getQueryOptions(),
        toCopy.getThreadingOptions(),
        toCopy.getNettyOptions(),
        toCopy.getCodecRegistry(),
        toCopy.getDefaultKeyspace());
  }

  void register(Cluster.Manager manager) {
    protocolOptions.register(manager);
    poolingOptions.register(manager);
    queryOptions.register(manager);
    policies.getEndPointFactory().init(manager.getCluster());

    checkPoliciesIfSni();
  }

  // If using SNI endpoints, the SSL options and auth provider MUST be the "extended" versions, the
  // base versions work with IP addresses that might not be unique to a node.
  // Throw now since that's probably a configuration error.
  private void checkPoliciesIfSni() {
    if (policies.getEndPointFactory() instanceof SniEndPointFactory) {
      SSLOptions sslOptions = protocolOptions.getSSLOptions();
      List<String> errors = new ArrayList<String>();
      if (sslOptions != null && !(sslOptions instanceof ExtendedRemoteEndpointAwareSslOptions)) {
        errors.add(
            String.format(
                "the configured %s must implement %s",
                SSLOptions.class.getSimpleName(),
                ExtendedRemoteEndpointAwareSslOptions.class.getSimpleName()));
      }
      AuthProvider authProvider = protocolOptions.getAuthProvider();
      if (authProvider != null && !(authProvider instanceof ExtendedAuthProvider)) {
        errors.add(
            String.format(
                "the configured %s must implement %s",
                AuthProvider.class.getSimpleName(), ExtendedAuthProvider.class.getSimpleName()));
      }
      if (!errors.isEmpty()) {
        throw new IllegalStateException(
            "Configuration error: if SNI endpoints are in use, " + Joiner.on(',').join(errors));
      }
    }
  }

  /**
   * Returns the policies set for the cluster.
   *
   * @return the policies set for the cluster.
   */
  public Policies getPolicies() {
    return policies;
  }

  /**
   * Returns the low-level TCP configuration options used (tcpNoDelay, keepAlive, ...).
   *
   * @return the socket options.
   */
  public SocketOptions getSocketOptions() {
    return socketOptions;
  }

  /**
   * Returns the Cassandra binary protocol level configuration (compression).
   *
   * @return the protocol options.
   */
  public ProtocolOptions getProtocolOptions() {
    return protocolOptions;
  }

  /**
   * Returns the connection pooling configuration.
   *
   * @return the pooling options.
   */
  public PoolingOptions getPoolingOptions() {
    return poolingOptions;
  }

  /**
   * Returns the metrics configuration, if metrics are enabled.
   *
   * <p>Metrics collection is enabled by default but can be disabled at cluster construction time
   * through {@link Cluster.Builder#withoutMetrics}.
   *
   * @return the metrics options or {@code null} if metrics are not enabled.
   */
  public MetricsOptions getMetricsOptions() {
    return metricsOptions;
  }

  /**
   * Returns the queries configuration.
   *
   * @return the queries options.
   */
  public QueryOptions getQueryOptions() {
    return queryOptions;
  }

  /** @return the threading options for this configuration. */
  public ThreadingOptions getThreadingOptions() {
    return threadingOptions;
  }

  /**
   * Returns the {@link NettyOptions} instance for this configuration.
   *
   * @return the {@link NettyOptions} instance for this configuration.
   */
  public NettyOptions getNettyOptions() {
    return nettyOptions;
  }

  public String getDefaultKeyspace() {
    return defaultKeyspace;
  }
  /**
   * Returns the {@link CodecRegistry} instance for this configuration.
   *
   * <p>Note that this method could return {@link CodecRegistry#DEFAULT_INSTANCE} if no specific
   * codec registry has been set on the {@link Cluster}. In this case, care should be taken when
   * registering new codecs as they would be immediately available to other {@link Cluster}
   * instances sharing the same default instance.
   *
   * @return the {@link CodecRegistry} instance for this configuration.
   */
  public CodecRegistry getCodecRegistry() {
    return codecRegistry;
  }

  /** A builder to create a new {@code Configuration} object. */
  public static class Builder {
    private Policies policies;
    private ProtocolOptions protocolOptions;
    private PoolingOptions poolingOptions;
    private SocketOptions socketOptions;
    private MetricsOptions metricsOptions;
    private QueryOptions queryOptions;
    private ThreadingOptions threadingOptions;
    private NettyOptions nettyOptions;
    private CodecRegistry codecRegistry;
    private String defaultKeyspace;

    /**
     * Sets the policies for this cluster.
     *
     * @param policies the policies.
     * @return this builder.
     */
    public Builder withPolicies(Policies policies) {
      this.policies = policies;
      return this;
    }

    /**
     * Sets the protocol options for this cluster.
     *
     * @param protocolOptions the protocol options.
     * @return this builder.
     */
    public Builder withProtocolOptions(ProtocolOptions protocolOptions) {
      this.protocolOptions = protocolOptions;
      return this;
    }

    /**
     * Sets the pooling options for this cluster.
     *
     * @param poolingOptions the pooling options.
     * @return this builder.
     */
    public Builder withPoolingOptions(PoolingOptions poolingOptions) {
      this.poolingOptions = poolingOptions;
      return this;
    }

    /**
     * Sets the socket options for this cluster.
     *
     * @param socketOptions the socket options.
     * @return this builder.
     */
    public Builder withSocketOptions(SocketOptions socketOptions) {
      this.socketOptions = socketOptions;
      return this;
    }

    /**
     * Sets the metrics options for this cluster.
     *
     * <p>If this method doesn't get called, the configuration will use the defaults: metrics
     * enabled with JMX reporting enabled. To disable metrics, call this method with an instance
     * where {@link MetricsOptions#isEnabled() isEnabled()} returns false.
     *
     * @param metricsOptions the metrics options.
     * @return this builder.
     */
    public Builder withMetricsOptions(MetricsOptions metricsOptions) {
      this.metricsOptions = metricsOptions;
      return this;
    }

    /**
     * Sets the query options for this cluster.
     *
     * @param queryOptions the query options.
     * @return this builder.
     */
    public Builder withQueryOptions(QueryOptions queryOptions) {
      this.queryOptions = queryOptions;
      return this;
    }

    /**
     * Sets the threading options for this cluster.
     *
     * @param threadingOptions the threading options to set.
     * @return this builder.
     */
    public Builder withThreadingOptions(ThreadingOptions threadingOptions) {
      this.threadingOptions = threadingOptions;
      return this;
    }

    /**
     * Sets the Netty options for this cluster.
     *
     * @param nettyOptions the Netty options.
     * @return this builder.
     */
    public Builder withNettyOptions(NettyOptions nettyOptions) {
      this.nettyOptions = nettyOptions;
      return this;
    }

    /**
     * Sets the codec registry for this cluster.
     *
     * @param codecRegistry the codec registry.
     * @return this builder.
     */
    public Builder withCodecRegistry(CodecRegistry codecRegistry) {
      this.codecRegistry = codecRegistry;
      return this;
    }

    public Builder withDefaultKeyspace(String keyspace) {
      this.defaultKeyspace = keyspace;
      return this;
    }

    /**
     * Builds the final object from this builder.
     *
     * <p>Any field that hasn't been set explicitly will get its default value.
     *
     * @return the object.
     */
    public Configuration build() {
      return new Configuration(
          policies != null ? policies : Policies.builder().build(),
          protocolOptions != null ? protocolOptions : new ProtocolOptions(),
          poolingOptions != null ? poolingOptions : new PoolingOptions(),
          socketOptions != null ? socketOptions : new SocketOptions(),
          metricsOptions != null ? metricsOptions : new MetricsOptions(),
          queryOptions != null ? queryOptions : new QueryOptions(),
          threadingOptions != null ? threadingOptions : new ThreadingOptions(),
          nettyOptions != null ? nettyOptions : NettyOptions.DEFAULT_INSTANCE,
          codecRegistry != null ? codecRegistry : CodecRegistry.DEFAULT_INSTANCE,
          defaultKeyspace);
    }
  }
}
