/*
 * Copyright 2018 ConsenSys AG.
 *
 * Licensed 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 net.consensys.cava.config;

import static java.util.Objects.requireNonNull;
import static net.consensys.cava.toml.Toml.canonicalDottedKey;

import net.consensys.cava.toml.Toml;

import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.io.Writer;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nullable;

/**
 * Represents collection of configuration properties, optionally validated against a schema.
 */
public interface Configuration {

  /**
   * Read a configuration from a TOML-formatted string.
   *
   * @param toml A TOML-formatted string.
   * @return A Configuration loaded from the TOML file.
   */
  static Configuration fromToml(String toml) {
    return fromToml(toml, null);
  }

  /**
   * Read a configuration from a TOML-formatted string, associated with a validation schema.
   *
   * @param toml A TOML-formatted string.
   * @param schema The validation schema for the configuration.
   * @return A Configuration loaded from the TOML file.
   */
  static Configuration fromToml(String toml, @Nullable Schema schema) {
    requireNonNull(toml);
    return new TomlBackedConfiguration(Toml.parse(toml), schema);
  }

  /**
   * Loads a configuration from a TOML-formatted file.
   *
   * @param file The path of the TOML-formatted configuration file.
   * @return A Configuration loaded from the TOML file.
   * @throws NoSuchFileException If the file could not be found.
   * @throws IOException If an IO error occurs.
   */
  static Configuration fromToml(Path file) throws IOException {
    return fromToml(file, null);
  }

  /**
   * Loads a configuration from a file, associated with a validation schema.
   *
   * @param file The path of the TOML-formatted configuration file.
   * @param schema The validation schema for the configuration.
   * @return A Configuration loaded from the TOML file.
   * @throws NoSuchFileException If the file could not be found.
   * @throws IOException If an IO error occurs.
   */
  static Configuration fromToml(Path file, @Nullable Schema schema) throws IOException {
    requireNonNull(file);
    return new TomlBackedConfiguration(Toml.parse(file), schema);
  }

  /**
   * Loads a configuration from a TOML-formatted file.
   *
   * @param is An input stream providing TOML-formatted configuration.
   * @return A Configuration loaded from the TOML file.
   * @throws IOException If an IO error occurs.
   */
  static Configuration fromToml(InputStream is) throws IOException {
    return fromToml(is, null);
  }

  /**
   * Loads a configuration from a file, associated with a validation schema.
   *
   * @param is An input stream providing TOML-formatted configuration.
   * @param schema The validation schema for the configuration.
   * @return A Configuration loaded from the TOML file.
   * @throws IOException If an IO error occurs.
   */
  static Configuration fromToml(InputStream is, @Nullable Schema schema) throws IOException {
    requireNonNull(is);
    return new TomlBackedConfiguration(Toml.parse(is), schema);
  }

  /**
   * Get an empty configuration, with no values.
   *
   * @return An empty configuration, with no values.
   */
  static Configuration empty() {
    return EmptyConfiguration.EMPTY;
  }

  /**
   * Get an empty configuration, associated with a validation schema.
   *
   * @param schema The validation schema for the configuration.
   * @return An empty configuration.
   */
  static Configuration empty(@Nullable Schema schema) {
    return new EmptyConfiguration(schema);
  }

  /**
   * @return {@code true} if the TOML document contained errors.
   */
  default boolean hasErrors() {
    return !(errors().isEmpty());
  }

  /**
   * The errors that occurred during parsing.
   *
   * @return A list of errors.
   */
  List<ConfigurationError> errors();

  /**
   * Get a TOML-formatted representation of this configuration.
   *
   * @return A TOML-formatted representation.
   */
  default String toToml() {
    StringBuilder builder = new StringBuilder();
    try {
      toToml(builder);
    } catch (IOException e) {
      // Not reachable
      throw new UncheckedIOException(e);
    }
    return builder.toString();
  }

  /**
   * Save a configuration to a TOML-formatted file.
   *
   * <p>
   * If necessary, parent directories for the output file will be created.
   *
   * @param path The file path to write the TOML-formatted output to.
   * @throws IOException If the file cannot be written.
   */
  default void toToml(Path path) throws IOException {
    Files.createDirectories(path.getParent());
    try (Writer writer = Files.newBufferedWriter(path)) {
      toToml(writer);
    }
  }

  /**
   * Writes a configuration in TOML format.
   *
   * @param appendable The output to write to.
   * @throws IOException If the file cannot be written.
   */
  void toToml(Appendable appendable) throws IOException;

  /**
   * The keys of all entries present in this configuration.
   *
   * @return The keys of all entries in this configuration.
   */
  Set<String> keySet();

  /**
   * Check if a key is set in this configuration.
   *
   * @param key A configuration key (e.g. {@code "server.address.hostname"}).
   * @return {@code true} if the entry is present in this configuration.
   * @throws IllegalArgumentException If the key cannot be parsed.
   */
  boolean contains(String key);

  /**
   * Get an object from this configuration.
   *
   * @param key A configuration key (e.g. {@code "server.address.hostname"}).
   * @return The value, or {@code null} if no value was set in the configuration.
   * @throws IllegalArgumentException If the key cannot be parsed.
   */
  @Nullable
  Object get(String key);

  /**
   * Get the position where a key is defined in the TOML document.
   *
   * @param key A configuration key (e.g. {@code "server.address.port"}).
   * @return The input position, or {@code null} if the key was not set in this configuration.
   * @throws IllegalArgumentException If the key cannot be parsed.
   */
  @Nullable
  DocumentPosition inputPositionOf(String key);

  /**
   * Get a string from this configuration.
   *
   * @param key A configuration key (e.g. {@code "server.address.hostname"}).
   * @return The value.
   * @throws IllegalArgumentException If the key cannot be parsed.
   * @throws InvalidConfigurationPropertyTypeException If the value is not a string.
   * @throws NoConfigurationPropertyException If the key was not set in the configuration.
   */
  String getString(String key);

  /**
   * Get an integer from this configuration.
   *
   * @param key A configuration key (e.g. {@code "server.address.port"}).
   * @return The value.
   * @throws IllegalArgumentException If the key cannot be parsed.
   * @throws InvalidConfigurationPropertyTypeException If the value is not an integer.
   * @throws NoConfigurationPropertyException If the key was not set in the configuration.
   */
  int getInteger(String key);

  /**
   * Get a long from this configuration.
   *
   * @param key A configuration key (e.g. {@code "server.address.port"}).
   * @return The value.
   * @throws IllegalArgumentException If the key cannot be parsed.
   * @throws InvalidConfigurationPropertyTypeException If the value is not a long.
   * @throws NoConfigurationPropertyException If the key was not set in the configuration.
   */
  long getLong(String key);

  /**
   * Get a double from this configuration.
   *
   * @param key A configuration key (e.g. {@code "server.priority"}).
   * @return The value.
   * @throws IllegalArgumentException If the key cannot be parsed.
   * @throws InvalidConfigurationPropertyTypeException If the value is not a double.
   * @throws NoConfigurationPropertyException If the key was not set in the configuration.
   */
  double getDouble(String key);

  /**
   * Get a boolean from this configuration.
   *
   * @param key A configuration key (e.g. {@code "server.active"}).
   * @return The value.
   * @throws IllegalArgumentException If the key cannot be parsed.
   * @throws InvalidConfigurationPropertyTypeException If the value is not a boolean.
   * @throws NoConfigurationPropertyException If the key was not set in the configuration.
   */
  boolean getBoolean(String key);

  /**
   * Get a map from this configuration.
   *
   * @param key A configuration key (e.g. {@code "server.active"}).
   * @return The value.
   * @throws IllegalArgumentException If the key cannot be parsed.
   * @throws InvalidConfigurationPropertyTypeException If the value is not a map.
   * @throws NoConfigurationPropertyException If the key was not set in the configuration.
   */
  Map<String, Object> getMap(String key);

  /**
   * Get a list from this configuration.
   *
   * @param key A configuration key (e.g. {@code "server.common_names"}).
   * @return The value.
   * @throws IllegalArgumentException If the key cannot be parsed.
   * @throws InvalidConfigurationPropertyTypeException If the value is not a list.
   * @throws NoConfigurationPropertyException If the key was not set in the configuration.
   */
  List<Object> getList(String key);

  /**
   * Get a list of strings from this configuration.
   *
   * @param key A configuration key (e.g. {@code "server.common_names"}).
   * @return The value.
   * @throws IllegalArgumentException If the key cannot be parsed.
   * @throws InvalidConfigurationPropertyTypeException If the value is not a list of strings.
   * @throws NoConfigurationPropertyException If the key was not set in the configuration.
   */
  List<String> getListOfString(String key);

  /**
   * Get a list of integers from this configuration.
   *
   * @param key A configuration key (e.g. {@code "server.address.ports"}).
   * @return The value.
   * @throws IllegalArgumentException If the key cannot be parsed.
   * @throws InvalidConfigurationPropertyTypeException If the value is not a list of integers.
   * @throws NoConfigurationPropertyException If the key was not set in the configuration.
   */
  List<Integer> getListOfInteger(String key);

  /**
   * Get a list of longs from this configuration.
   *
   * @param key A configuration key (e.g. {@code "server.address.ports"}).
   * @return The value.
   * @throws IllegalArgumentException If the key cannot be parsed.
   * @throws InvalidConfigurationPropertyTypeException If the value is not a list of longs.
   * @throws NoConfigurationPropertyException If the key was not set in the configuration.
   */
  List<Long> getListOfLong(String key);

  /**
   * Get a list of doubles from this configuration.
   *
   * @param key A configuration key (e.g. {@code "server.priorities"}).
   * @return The value.
   * @throws IllegalArgumentException If the key cannot be parsed.
   * @throws InvalidConfigurationPropertyTypeException If the value is not a list of doubles.
   * @throws NoConfigurationPropertyException If the key was not set in the configuration.
   */
  List<Double> getListOfDouble(String key);

  /**
   * Get a list of booleans from this configuration.
   *
   * @param key A configuration key (e.g. {@code "server.flags"}).
   * @return The value.
   * @throws IllegalArgumentException If the key cannot be parsed.
   * @throws InvalidConfigurationPropertyTypeException If the value is not a list of booleans.
   * @throws NoConfigurationPropertyException If the key was not set in the configuration.
   */
  List<Boolean> getListOfBoolean(String key);

  /**
   * Get a list of maps from this configuration.
   *
   * @param key A configuration key (e.g. {@code "mainnet.servers"}).
   * @return The value.
   * @throws IllegalArgumentException If the key cannot be parsed.
   * @throws InvalidConfigurationPropertyTypeException If the value is not a list of maps.
   * @throws NoConfigurationPropertyException If the key was not set in the configuration.
   */
  List<Map<String, Object>> getListOfMap(String key);

  /**
   * Get the canonical form of a configuration key.
   *
   * @param key A configuration key (e.g. {@code "server.flags"}).
   * @return The canonical form of the key.
   * @throws IllegalArgumentException If the key cannot be parsed.
   */
  static String canonicalKey(String key) {
    return canonicalDottedKey(key);
  }
}
