/*
 * 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 net.consensys.cava.config.Configuration.canonicalKey;

import net.consensys.cava.toml.Toml;

import java.io.IOException;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import com.google.common.base.Splitter;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ListMultimap;

final class TomlSerializer {

  private static final Splitter LINE_SPLITTER = Splitter.onPattern("\\r?\\n");

  private final Configuration configuration;
  private final Schema schema;
  // tableKey -> configKey
  private final ListMultimap<String, String> tableMap;
  // configKey -> leafName
  private final Map<String, String> keyMap;

  TomlSerializer(Configuration configuration, Schema schema) {
    ListMultimap<String, String> tableMap = ArrayListMultimap.create();
    Map<String, String> keyMap = new HashMap<>();

    configuration.keySet().forEach(configKey -> {
      List<String> keyPath;
      try {
        keyPath = Toml.parseDottedKey(configKey);
      } catch (IllegalArgumentException e) {
        throw new IllegalStateException("Configuration key '" + configKey + "' is not valid in TOML");
      }
      String tableKey = Toml.joinKeyPath(keyPath.subList(0, keyPath.size() - 1));
      tableMap.put(tableKey, configKey);
      keyMap.put(configKey, keyPath.get(keyPath.size() - 1));
    });

    this.configuration = configuration;
    this.schema = schema;
    this.tableMap = tableMap;
    this.keyMap = keyMap;
  }

  void writeTo(Appendable appendable) throws IOException {
    List<String> tableKeys = tableMap.keySet().stream().sorted().collect(Collectors.toList());
    for (Iterator<String> iterator = tableKeys.iterator(); iterator.hasNext();) {
      String tableKey = iterator.next();
      if (!tableKey.isEmpty()) {
        writeDocumentation(tableKey, appendable);
      }
      if (!tableKey.isEmpty()) {
        appendable.append('[');
        appendable.append(tableKey);
        appendable.append(']');
        appendable.append(System.lineSeparator());
      }

      List<String> configKeys = tableMap.get(tableKey);
      configKeys.sort(Comparator.naturalOrder());
      for (String configKey : configKeys) {
        String leafName = keyMap.get(configKey);
        Object obj = configuration.get(configKey);
        if (obj == null) {
          throw new IllegalStateException("Configuration key '" + configKey + "' was unexpectedly null");
        }
        Object defaultValue = schema.getDefault(configKey);
        if (obj instanceof Integer) {
          obj = ((Integer) obj).longValue();
        }
        if (defaultValue instanceof Integer) {
          defaultValue = ((Integer) defaultValue).longValue();
        }

        writeDocumentation(configKey, appendable);

        String leafKey = Toml.joinKeyPath(Collections.singletonList(leafName));
        if (defaultValue != null) {
          appendable.append('#');
          appendable.append(leafKey);
          appendable.append(" = ");
          writeValue(defaultValue, appendable);
          appendable.append(System.lineSeparator());
        }
        if (!obj.equals(defaultValue)) {
          appendable.append(leafKey);
          appendable.append(" = ");
          writeValue(obj, appendable);
          appendable.append(System.lineSeparator());
        }
      }

      if (iterator.hasNext()) {
        appendable.append(System.lineSeparator());
      }
    }
  }

  private void writeValue(Object obj, Appendable appendable) throws IOException {
    if (obj instanceof String) {
      appendable.append('\"');
      appendable.append(Toml.tomlEscape((String) obj));
      appendable.append('\"');
    } else if (obj instanceof List) {
      @SuppressWarnings("unchecked")
      List<Object> list = (List<Object>) obj;
      writeList(list, appendable);
    } else if (obj instanceof Map) {
      @SuppressWarnings("unchecked")
      Map<String, Object> map = (Map<String, Object>) obj;
      writeMap(map, appendable);
    } else {
      appendable.append(obj.toString());
    }
  }

  private void writeList(List<Object> list, Appendable appendable) throws IOException {
    appendable.append('[');
    for (Iterator<Object> iterator = list.iterator(); iterator.hasNext();) {
      Object obj = iterator.next();
      if (obj == null) {
        throw new IllegalStateException("Unexpected null in list property");
      }
      writeValue(obj, appendable);
      if (iterator.hasNext()) {
        appendable.append(", ");
      }
    }
    appendable.append(']');
  }

  private void writeMap(Map<String, Object> map, Appendable appendable) throws IOException {
    appendable.append('{');
    for (Iterator<String> iterator = map.keySet().stream().sorted().iterator(); iterator.hasNext();) {
      String key = iterator.next();
      Object obj = map.get(key);
      if (obj == null) {
        throw new IllegalStateException("Unexpected null in map property");
      }

      appendable.append(Toml.joinKeyPath(Collections.singletonList(key)));
      appendable.append(" = ");
      writeValue(obj, appendable);
      if (iterator.hasNext()) {
        appendable.append(", ");
      }
    }
    appendable.append('}');
  }

  private void writeDocumentation(String key, Appendable appendable) throws IOException {
    String description = schema.description(canonicalKey(key));
    if (description == null) {
      return;
    }
    for (String line : LINE_SPLITTER.split(description)) {
      appendable.append("## ");
      appendable.append(line);
      appendable.append(System.lineSeparator());
    }
  }
}
