# A YAML builder generates valid YAML.
#
# A `YAML::Error` is raised if attempting to generate
# an invalid YAML (for example, if invoking `end_sequence`
# without a matching `start_sequence`)
#
# ```
# require "yaml"
#
# string = YAML.build do |yaml|
#   yaml.mapping do
#     yaml.scalar "foo"
#     yaml.sequence do
#       yaml.scalar 1
#       yaml.scalar 2
#     end
#     yaml.scalar "bar"
#     yaml.mapping do
#       yaml.scalar "baz"
#       yaml.scalar "qux"
#     end
#   end
# end
# string # => "---\nfoo:\n- 1\n- 2\nbar:\n  baz: qux\n"
# ```
class YAML::Builder
  @box : Void*

  # By default the maximum nesting of sequences/mappings is 99. Nesting more
  # than this will result in a YAML::Error. Changing the value of this property
  # allows more/less nesting.
  property max_nesting = 99

  # Creates a `YAML::Builder` that will write to the given `IO`.
  def initialize(@io : IO)
    @box = Box.box(io)
    @emitter = Pointer(Void).malloc(LibYAML::EMITTER_SIZE).as(LibYAML::Emitter*)
    @event = LibYAML::Event.new
    @closed = false
    @nesting = 0
    LibYAML.yaml_emitter_initialize(@emitter)
    LibYAML.yaml_emitter_set_unicode(@emitter, 1)
    LibYAML.yaml_emitter_set_output(@emitter, ->(data, buffer, size) {
      data_io = Box(IO).unbox(data)
      data_io.write_string(Slice.new(buffer, size))
      1
    }, @box)
  end

  # Creates a `YAML::Builder` that writes to *io* and yields it to the block.
  #
  # After returning from the block the builder is closed.
  def self.build(io : IO, & : self ->) : Nil
    builder = new(io)
    yield builder ensure builder.close
  end

  # Starts a YAML stream.
  def start_stream
    emit stream_start, LibYAML::Encoding::UTF8
  end

  # Ends a YAML stream.
  def end_stream : Nil
    emit stream_end
    flush
  end

  # Starts a YAML stream, invokes the block, and ends it.
  def stream(&)
    start_stream
    yield.tap { end_stream }
  end

  # Starts a document.
  #
  # If *implicit_start_indicator* is true, skips printing the document start
  # indicator (`---`) if this is the first document in the stream.
  def start_document(*, implicit_start_indicator : Bool = false)
    emit document_start, nil, nil, nil, implicit_start_indicator.to_unsafe
  end

  # Ends a document.
  def end_document
    emit document_end, 1
  end

  # Starts a document, invokes the block, and then ends it.
  #
  # If *implicit_start_indicator* is true, skips printing the document start
  # indicator (`---`) if this is the first document in the stream.
  def document(*, implicit_start_indicator : Bool = false, &)
    start_document(implicit_start_indicator: implicit_start_indicator)
    yield.tap { end_document }
  end

  # Emits a scalar value.
  def scalar(value, anchor : String? = nil, tag : String? = nil, style : YAML::ScalarStyle = YAML::ScalarStyle::ANY)
    string = value.to_s
    implicit = tag ? 0 : 1
    emit scalar, get_anchor(anchor), string_to_unsafe(tag), string, string.bytesize, implicit, implicit, style
  end

  # Starts a sequence.
  def start_sequence(anchor : String? = nil, tag : String? = nil, style : YAML::SequenceStyle = YAML::SequenceStyle::ANY) : Nil
    implicit = tag ? 0 : 1
    emit sequence_start, get_anchor(anchor), string_to_unsafe(tag), implicit, style
    increase_nesting
  end

  # Ends a sequence.
  def end_sequence : Nil
    emit sequence_end
    decrease_nesting
  end

  # Starts a sequence, invokes the block, and the ends it.
  def sequence(anchor : String? = nil, tag : String? = nil, style : YAML::SequenceStyle = YAML::SequenceStyle::ANY, &)
    start_sequence(anchor, tag, style)
    yield.tap { end_sequence }
  end

  # Starts a mapping.
  def start_mapping(anchor : String? = nil, tag : String? = nil, style : YAML::MappingStyle = YAML::MappingStyle::ANY) : Nil
    implicit = tag ? 0 : 1
    emit mapping_start, get_anchor(anchor), string_to_unsafe(tag), implicit, style
    increase_nesting
  end

  # Ends a mapping.
  def end_mapping : Nil
    emit mapping_end
    decrease_nesting
  end

  # Starts a mapping, invokes the block, and then ends it.
  def mapping(anchor : String? = nil, tag : String? = nil, style : YAML::MappingStyle = YAML::MappingStyle::ANY, &)
    start_mapping(anchor, tag, style)
    yield.tap { end_mapping }
  end

  # Emits an alias to the given *anchor*.
  #
  # ```
  # require "yaml"
  #
  # yaml = YAML.build do |builder|
  #   builder.mapping do
  #     builder.scalar "key"
  #     builder.alias "example"
  #   end
  # end
  #
  # yaml # => "---\nkey: *example\n"
  # ```
  def alias(anchor : String) : Nil
    LibYAML.yaml_alias_event_initialize(pointerof(@event), anchor)
    yaml_emit("alias")
  end

  # Emits the scalar `"<<"` followed by an alias to the given *anchor*.
  #
  # See [YAML Merge](https://yaml.org/type/merge.html).
  #
  # ```
  # require "yaml"
  #
  # yaml = YAML.build do |builder|
  #   builder.mapping do
  #     builder.merge "development"
  #   end
  # end
  #
  # yaml # => "---\n<<: *development\n"
  # ```
  def merge(anchor : String) : Nil
    self.scalar "<<"
    self.alias anchor
  end

  # Flushes any pending data to the underlying `IO`.
  def flush
    LibYAML.yaml_emitter_flush(@emitter)

    @io.flush
  end

  def finalize
    return if @closed
    LibYAML.yaml_emitter_delete(@emitter)
  end

  # Closes the builder, freeing up resources.
  def close : Nil
    finalize
    @closed = true
  end

  private def get_anchor(anchor)
    string_to_unsafe(anchor)
  end

  private def string_to_unsafe(tag)
    tag.try(&.to_unsafe) || Pointer(UInt8).null
  end

  private macro emit(event_name, *args)
    LibYAML.yaml_{{event_name}}_event_initialize(pointerof(@event), {{args.splat}})
    yaml_emit({{event_name.stringify}})
  end

  private def yaml_emit(event_name)
    ret = LibYAML.yaml_emitter_emit(@emitter, pointerof(@event))
    if ret != 1
      raise YAML::Error.new(build_libyaml_message(event_name))
    end
  end

  private def build_libyaml_message(event_name)
    problem = @emitter.value.problem
    strlen = LibC.strlen(problem)
    String.build(event_name.bytesize + strlen + 16) do |io|
      io << "Error emitting " << event_name << ": "
      io.write Slice.new(problem, strlen)
    end
  end

  private def increase_nesting
    @nesting += 1
    if @nesting > @max_nesting
      raise YAML::Error.new("Nesting of #{@nesting} is too deep")
    end
  end

  private def decrease_nesting
    @nesting -= 1
  end
end

module YAML
  # Returns the resulting String of writing YAML to the yielded `YAML::Builder`.
  #
  # ```
  # require "yaml"
  #
  # string = YAML.build do |yaml|
  #   yaml.mapping do
  #     yaml.scalar "foo"
  #     yaml.sequence do
  #       yaml.scalar 1
  #       yaml.scalar 2
  #     end
  #   end
  # end
  # string # => "---\nfoo:\n- 1\n- 2\n"
  # ```
  def self.build(&)
    String.build do |str|
      build(str) do |yaml|
        yield yaml
      end
    end
  end

  # Writes YAML into the given `IO`. A `YAML::Builder` is yielded to the block.
  def self.build(io : IO, &) : Nil
    YAML::Builder.build(io) do |yaml|
      yaml.stream do
        yaml.document do
          yield yaml
        end
      end
    end
  end
end
