/*
 * Copyright (c) 2022 Airbyte, Inc., all rights reserved.
 */

package io.airbyte.commons.yaml;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SequenceWriter;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator;
import com.fasterxml.jackson.dataformat.yaml.YAMLParser;
import com.google.common.collect.AbstractIterator;
import io.airbyte.commons.jackson.MoreMappers;
import io.airbyte.commons.lang.CloseableConsumer;
import io.airbyte.commons.lang.Exceptions;
import io.airbyte.commons.util.AutoCloseableIterator;
import io.airbyte.commons.util.AutoCloseableIterators;
import java.io.IOException;
import java.io.InputStream;
import java.io.Writer;
import java.util.Iterator;

@SuppressWarnings("PMD.AvoidBranchingStatementAsLastInLoop")
public class Yamls {

  private static final YAMLFactory YAML_FACTORY = new YAMLFactory();
  private static final ObjectMapper OBJECT_MAPPER = MoreMappers.initYamlMapper(YAML_FACTORY);

  private static final YAMLFactory YAML_FACTORY_WITHOUT_QUOTES = new YAMLFactory().enable(YAMLGenerator.Feature.MINIMIZE_QUOTES);
  private static final ObjectMapper OBJECT_MAPPER_WITHOUT_QUOTES = MoreMappers.initYamlMapper(YAML_FACTORY_WITHOUT_QUOTES);

  /**
   * Serialize object to YAML string. String values WILL be wrapped in double quotes.
   *
   * @param object - object to serialize
   * @return YAML string version of object
   */
  public static <T> String serialize(final T object) {
    try {
      return OBJECT_MAPPER.writeValueAsString(object);
    } catch (final JsonProcessingException e) {
      throw new RuntimeException(e);
    }
  }

  /**
   * Serialize object to YAML string. String values will NOT be wrapped in double quotes.
   *
   * @param object - object to serialize
   * @return YAML string version of object
   */
  public static String serializeWithoutQuotes(final Object object) {
    try {
      return OBJECT_MAPPER_WITHOUT_QUOTES.writeValueAsString(object);
    } catch (final JsonProcessingException e) {
      throw new RuntimeException(e);
    }
  }

  public static <T> T deserialize(final String yamlString, final Class<T> klass) {
    try {
      return OBJECT_MAPPER.readValue(yamlString, klass);
    } catch (final IOException e) {
      throw new RuntimeException(e);
    }
  }

  public static JsonNode deserialize(final String yamlString) {
    try {
      return OBJECT_MAPPER.readTree(yamlString);
    } catch (final IOException e) {
      throw new RuntimeException(e);
    }
  }

  public static AutoCloseableIterator<JsonNode> deserializeArray(final InputStream stream) {
    try {
      final YAMLParser parser = YAML_FACTORY.createParser(stream);

      // Check the first token
      if (parser.nextToken() != JsonToken.START_ARRAY) {
        throw new IllegalStateException("Expected content to be an array");
      }

      final Iterator<JsonNode> iterator = new AbstractIterator<>() {

        @Override
        protected JsonNode computeNext() {
          try {
            while (parser.nextToken() != JsonToken.END_ARRAY) {
              return parser.readValueAsTree();
            }
          } catch (final IOException e) {
            throw new RuntimeException(e);
          }
          return endOfData();
        }

      };

      return AutoCloseableIterators.fromIterator(iterator, parser::close);

    } catch (final IOException e) {
      throw new RuntimeException(e);
    }
  }

  // todo (cgardens) - share this with Jsons if ever needed.

  /**
   * Creates a consumer that writes list items to the writer in a streaming fashion.
   *
   * @param writer writer to write to
   * @param <T> type of items being written
   * @return consumer that is able to write element to a list element by element. must be closed!
   */
  public static <T> CloseableConsumer<T> listWriter(final Writer writer) {
    return new YamlConsumer<>(writer, OBJECT_MAPPER);
  }

  public static class YamlConsumer<T> implements CloseableConsumer<T> {

    private final SequenceWriter sequenceWriter;

    public YamlConsumer(final Writer writer, final ObjectMapper objectMapper) {
      this.sequenceWriter = Exceptions.toRuntime(() -> objectMapper.writer().writeValuesAsArray(writer));

    }

    @Override
    public void accept(final T t) {
      try {
        sequenceWriter.write(t);
      } catch (final IOException e) {
        throw new RuntimeException(e);
      }
    }

    @Override
    public void close() throws Exception {
      // closing the SequenceWriter closes the Writer that it wraps.
      sequenceWriter.close();
    }

  }

}
