require "./yaml/*"
require "./yaml/schema/*"
require "./yaml/schema/core/*"
require "./yaml/nodes/*"
require "semantic_version"

require "base64"

# The YAML module provides serialization and deserialization of YAML
# version 1.1 to/from native Crystal data structures, with the additional
# independent types specified in http://yaml.org/type/
#
# NOTE: To use `YAML`, you must explicitly import it with `require "yaml"`
#
# ### Parsing with `#parse` and `#parse_all`
#
# `YAML.parse` will return an `Any`, which is a convenient wrapper around all possible
# YAML core types, making it easy to traverse a complex YAML structure but requires
# some casts from time to time, mostly via some method invocations.
#
# ```
# require "yaml"
#
# data = YAML.parse <<-YAML
#          ---
#          foo:
#            bar:
#              baz:
#                - qux
#                - fox
#          YAML
# data["foo"]["bar"]["baz"][1].as_s # => "fox"
# ```
#
# `YAML.parse` can read from an `IO` directly (such as a file) which saves
# allocating a string:
#
# ```
# require "yaml"
#
# yaml = File.open("path/to/file.yml") do |file|
#   YAML.parse(file)
# end
# ```
#
# ### Parsing with `from_yaml`
#
# A type `T` can be deserialized from YAML by invoking `T.from_yaml(string_or_io)`.
# For this to work, `T` must implement
# `new(ctx : YAML::PullParser, node : YAML::Nodes::Node)` and decode
# a value from the given *node*, using *ctx* to store and retrieve
# anchored values (see `YAML::PullParser` for an explanation of this).
#
# Crystal primitive types, `Time`, `Bytes` and `Union` implement
# this method. `YAML::Serializable` can be used to implement this method
# for user types.
#
# ### Dumping with `YAML.dump` or `#to_yaml`
#
# `YAML.dump` generates the YAML representation for an object.
# An `IO` can be passed and it will be written there,
# otherwise it will be returned as a string. Similarly, `#to_yaml`
# (with or without an `IO`) on any object does the same.
#
# For this to work, the type given to `YAML.dump` must implement
# `to_yaml(builder : YAML::Nodes::Builder`).
#
# Crystal primitive types, `Time` and `Bytes` implement
# this method. `YAML::Serializable` can be used to implement this method
# for user types.
#
# ```
# yaml = YAML.dump({hello: "world"})                               # => "---\nhello: world\n"
# File.open("foo.yml", "w") { |f| YAML.dump({hello: "world"}, f) } # writes it to the file
# # or:
# yaml = {hello: "world"}.to_yaml                               # => "---\nhello: world\n"
# File.open("foo.yml", "w") { |f| {hello: "world"}.to_yaml(f) } # writes it to the file
# ```
module YAML
  class Error < Exception
  end

  # Exception thrown on a YAML parse error.
  class ParseException < Error
    getter line_number : Int32
    getter column_number : Int32

    def initialize(message, line_number, column_number, context_info = nil)
      @line_number = line_number.to_i
      @column_number = column_number.to_i
      if context_info
        context_msg, context_line, context_column = context_info
        super("#{message} at line #{line_number}, column #{column_number}, #{context_msg} at line #{context_line}, column #{context_column}")
      else
        super("#{message} at line #{line_number}, column #{column_number}")
      end
    end

    def location : {Int32, Int32}
      {line_number, column_number}
    end
  end

  # Deserializes a YAML document according to the core schema.
  #
  # ```yaml
  # # ./foo.yml
  # data:
  #   string: "foobar"
  #   array:
  #     - John
  #     - Sarah
  #   hash: {key: value}
  #   paragraph: |
  #     foo
  #     bar
  # ```
  #
  # ```
  # require "yaml"
  #
  # YAML.parse(File.read("./foo.yml"))
  # # => {
  # # => "data" => {
  # # => "string" => "foobar",
  # # => "array" => ["John", "Sarah"],
  # # => "hash" => {"key" => "value"},
  # # => "paragraph" => "foo\nbar\n"
  # # => }
  # ```
  def self.parse(data : String | IO) : Any
    YAML::Schema::Core.parse(data)
  end

  # Deserializes multiple YAML documents according to the core schema.
  #
  # ```yaml
  # # ./foo.yml
  # foo: bar
  # ---
  # hello: world
  # ```
  #
  # ```
  # require "yaml"
  #
  # YAML.parse_all(File.read("./foo.yml"))
  # # => [{"foo" => "bar"}, {"hello" => "world"}]
  # ```
  def self.parse_all(data : String) : Array(Any)
    YAML::Schema::Core.parse_all(data)
  end

  # Serializes an object to YAML, returning it as a `String`.
  def self.dump(object) : String
    object.to_yaml
  end

  # Serializes an object to YAML, writing it to *io*.
  def self.dump(object, io : IO) : Nil
    object.to_yaml(io)
  end

  # Returns the used version of `libyaml`.
  def self.libyaml_version : SemanticVersion
    LibYAML.yaml_get_version(out major, out minor, out patch)

    SemanticVersion.new(major, minor, patch)
  end
end
