/*
 * 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 org.apache.drill.test;

import java.util.Collection;
import java.util.Properties;
import java.util.Map.Entry;

import com.typesafe.config.ConfigValue;
import org.apache.drill.common.config.DrillConfig;

import com.typesafe.config.Config;
import com.typesafe.config.ConfigValueFactory;
import org.apache.drill.common.map.CaseInsensitiveMap;
import org.apache.drill.exec.ExecConstants;
import org.apache.drill.exec.server.options.OptionDefinition;
import org.apache.drill.exec.server.options.SystemOptionManager;

/**
 * Builds a {@link DrillConfig} for use in tests. Use this when a config
 * is needed by itself, separate from an embedded Drillbit.
 */
public class ConfigBuilder {

  protected String configResource;
  protected Properties configProps = new Properties();
  protected CaseInsensitiveMap<OptionDefinition> definitions = SystemOptionManager.createDefaultOptionDefinitions();

  /**
   * Use the given configuration properties as overrides.
   * @param configProps a collection of config properties
   * @return this builder
   * @see {@link #put(String, Object)}
   */
  public ConfigBuilder configProps(Properties configProps) {
    if (hasResource()) {
      // Drill provides no constructor for this use case.
      throw new IllegalArgumentException( "Cannot provide both a config resource and config properties.");
    }

    if (this.configProps == null) {
      this.configProps = createDefaultProperties();
    }

    this.configProps.putAll(configProps);

    return this;
  }

  public ConfigBuilder configProps(DrillConfig drillConfig) {
    if (hasResource()) {
      // Drill provides no constructor for this use case.
      throw new IllegalArgumentException( "Cannot provide both a config resource and config properties.");
    }

    if (configProps == null) {
      configProps = createDefaultProperties();
    }

    for (Entry<String, ConfigValue> entry: drillConfig.entrySet()) {
      final Object key = entry.getKey();
      final Object value = entry.getValue().unwrapped();

      configProps.put(key, value);
    }

    return this;
  }

  /**
   * Use the given configuration file, stored as a resource, to initialize
   * the Drill config. Note that the resource file should have the two
   * following settings to work as a config for an embedded Drillbit:
   * <pre><code>
   * drill.exec.sys.store.provider.local.write : false,
   * drill.exec.http.enabled : false
   * </code></pre>
   * It may be more convenient to add your settings to the default
   * config settings with {@link #put(String, Object)}.
   * @param configResource path to the file that contains the
   * config file to be read
   * @return this builder
   * @see {@link #put(String, Object)}
   */

  public ConfigBuilder resource(String configResource) {

    if (configProps != null) {
      // Drill provides no constructor for this use case.
      throw new IllegalArgumentException( "Cannot provide both a config resource and config properties.");
    }

    // TypeSafe gets unhappy about a leading slash, but other functions
    // require it. Silently discard the leading slash if given to
    // preserve the test writer's sanity.

    this.configResource = ClusterFixture.trimSlash(configResource);
    return this;
  }

  /**
   * Add an additional boot-time property for the embedded Drillbit.
   * @param key config property name
   * @param value property value
   * @return this builder
   */

  public ConfigBuilder put(String key, Object value) {
    if (hasResource()) {
      // Drill provides no constructor for this use case.
      throw new IllegalArgumentException( "Cannot provide both a config resource and config properties.");
    }

    if (configProps == null) {
      configProps = createDefaultProperties();
    }

    if (value instanceof Collection) {
      configProps.put(key, value);
    } else {
      configProps.put(key, value.toString());
    }

    return this;
  }

  private static Properties createDefaultProperties()
  {
    Properties properties = new Properties();
    properties.put(ExecConstants.CAST_EMPTY_STRING_TO_NULL, "false");
    properties.put(ExecConstants.USE_DYNAMIC_UDFS_KEY, "false");
    properties.put(ExecConstants.SYS_STORE_PROVIDER_LOCAL_ENABLE_WRITE, "false");

    return properties;
  }

  public ConfigBuilder putDefinition(OptionDefinition definition) {
    definitions.put(definition.getValidator().getOptionName(), definition);
    return this;
  }

  public CaseInsensitiveMap<OptionDefinition> getDefinitions() {
    return CaseInsensitiveMap.newImmutableMap(definitions);
  }

  public DrillConfig build() {

    // Create a config
    // Because of the way DrillConfig works, we can set the ZK
    // connection string only if a property set is provided.

    if (hasResource()) {
      return DrillConfig.create(configResource);
    } else if (configProps != null) {
      return constructConfig();
    } else {
      return DrillConfig.create();
    }
  }

  private DrillConfig constructConfig() {
    Properties stringProps = new Properties();
    Properties collectionProps = new Properties();

    // Filter out the collection type configs and other configs which can be converted to string.
    for(Entry<Object, Object> entry : configProps.entrySet()) {
      if(entry.getValue() instanceof Collection<?>) {
        collectionProps.put(entry.getKey(), entry.getValue());
      } else {
        stringProps.setProperty(entry.getKey().toString(), entry.getValue().toString());
      }
    }

    // First create a DrillConfig based on string properties.
    Config drillConfig = DrillConfig.create(stringProps);

    // Then add the collection properties inside the DrillConfig. Below call to withValue returns
    // a new reference. Considering mostly properties will be of string type, doing this
    // later will be less expensive as compared to doing it for all the properties.
    for(Entry<Object, Object> entry : collectionProps.entrySet()) {
      drillConfig = drillConfig.withValue(entry.getKey().toString(),
        ConfigValueFactory.fromAnyRef(entry.getValue()));
    }

    return new DrillConfig(drillConfig);
  }

  public boolean hasResource() {
    return configResource != null;
  }
}
