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

import static java.util.Objects.requireNonNull;
import static net.consensys.cava.toml.TomlType.typeFor;

import java.io.IOException;
import java.util.Collections;
import java.util.Iterator;
import java.util.Optional;

final class JsonSerializer {
  private JsonSerializer() {}

  static void toJson(TomlTable table, Appendable appendable) throws IOException {
    requireNonNull(table);
    requireNonNull(appendable);
    toJson(table, appendable, 0);
    appendable.append(System.lineSeparator());
  }

  private static void toJson(TomlTable table, Appendable appendable, int indent) throws IOException {
    if (table.isEmpty()) {
      appendable.append("{}");
      return;
    }
    appendLine(appendable, "{");
    for (Iterator<String> iterator = table.keySet().stream().sorted().iterator(); iterator.hasNext();) {
      String key = iterator.next();
      append(appendable, indent + 2, "\"" + escape(key) + "\" : ");
      Object value = table.get(Collections.singletonList(key));
      assert value != null;
      appendTomlValue(value, appendable, indent);
      if (iterator.hasNext()) {
        appendable.append(",");
        appendable.append(System.lineSeparator());
      }
    }
    appendable.append(System.lineSeparator());
    append(appendable, indent, "}");
  }

  static void toJson(TomlArray array, Appendable appendable) throws IOException {
    toJson(array, appendable, 0);
    appendable.append(System.lineSeparator());
  }

  private static void toJson(TomlArray array, Appendable appendable, int indent) throws IOException {
    if (array.isEmpty()) {
      appendable.append("[]");
      return;
    }
    if (array.containsTables()) {
      append(appendable, 0, "[");
      for (Iterator<Object> iterator = array.toList().iterator(); iterator.hasNext();) {
        toJson((TomlTable) iterator.next(), appendable, indent);
        if (iterator.hasNext()) {
          appendable.append(",");
        }
      }
      append(appendable, 0, "]");
    } else {
      appendLine(appendable, "[");
      for (Iterator<Object> iterator = array.toList().iterator(); iterator.hasNext();) {
        indentLine(appendable, indent + 2);
        appendTomlValue(iterator.next(), appendable, indent);
        if (iterator.hasNext()) {
          appendable.append(",");
          appendable.append(System.lineSeparator());
        }
      }
      appendable.append(System.lineSeparator());
      append(appendable, indent, "]");
    }
  }

  private static void appendTomlValue(Object value, Appendable appendable, int indent) throws IOException {
    Optional<TomlType> tomlType = typeFor(value);
    assert tomlType.isPresent();
    switch (tomlType.get()) {
      case STRING:
        append(appendable, 0, "\"" + escape((String) value) + "\"");
        break;
      case INTEGER:
      case FLOAT:
        append(appendable, 0, value.toString());
        break;
      case BOOLEAN:
        append(appendable, 0, ((Boolean) value) ? "true" : "false");
        break;
      case OFFSET_DATE_TIME:
      case LOCAL_DATE_TIME:
      case LOCAL_DATE:
      case LOCAL_TIME:
        append(appendable, 0, "\"" + value.toString() + "\"");
        break;
      case ARRAY:
        toJson((TomlArray) value, appendable, indent + 2);
        break;
      case TABLE:
        toJson((TomlTable) value, appendable, indent + 2);
        break;
    }
  }

  private static void append(Appendable appendable, int indent, String line) throws IOException {
    indentLine(appendable, indent);
    appendable.append(line);
  }

  private static void appendLine(Appendable appendable, String line) throws IOException {
    appendable.append(line);
    appendable.append(System.lineSeparator());
  }

  private static void indentLine(Appendable appendable, int indent) throws IOException {
    for (int i = 0; i < indent; ++i) {
      appendable.append(' ');
    }
  }

  private static StringBuilder escape(String text) {
    StringBuilder out = new StringBuilder(text.length());
    for (int i = 0; i < text.length(); i++) {
      char ch = text.charAt(i);
      if (ch == '\'') {
        out.append("\\'");
        continue;
      }
      if (ch >= 0x20) {
        out.append(ch);
        continue;
      }

      switch (ch) {
        case '\t':
          out.append("\\t");
          break;
        case '\b':
          out.append("\\b");
          break;
        case '\n':
          out.append("\\n");
          break;
        case '\r':
          out.append("\\r");
          break;
        case '\f':
          out.append("\\f");
          break;
        default:
          out.append("\\u").append(String.format("%04x", text.codePointAt(i)));
      }
    }
    return out;
  }
}
