# A pull parser allows parsing a YAML document by events.
#
# When creating an instance, the parser is positioned in
# the first event. To get the event kind invoke `kind`.
# If the event is a scalar you can invoke `value` to get
# its **string** value. Other methods like `tag`, `anchor`
# and `scalar_style` let you inspect other information from events.
#
# Invoking `read_next` reads the next event.
class YAML::PullParser
  protected getter content

  # :nodoc:
  #
  # Maximum structural depth (nested sequences and mappings) allowed.
  property max_nesting = 512

  # :nodoc:
  #
  # If true, evaluates the alias/anchor ratio to avoid excessive expansion of
  # aliases.
  property enforce_alias_anchor_ratio = true

  # :nodoc:
  #
  # Minimum number of aliases required before starting to evaluate the
  # alias/anchor ratio. This avoids affecting smaller documents.
  property alias_anchor_min_aliases = 100

  # :nodoc:
  #
  # The multiplier for the alias/anchor ratio evaluation. An error will be
  # raised when `aliases > multiplier * anchors` once `min_aliases` has been
  # reached.
  property alias_anchor_multiplier = 10

  def initialize(@content : String | IO)
    @parser = Pointer(Void).malloc(LibYAML::PARSER_SIZE).as(LibYAML::Parser*)
    @event = LibYAML::Event.new
    @closed = false

    @nesting = 0
    @anchors = 0
    @aliases = 0
    @anchor_scope = Hash(Int32, {String, Int32}).new
    @anchor_costs = Hash(String, Int32).new

    LibYAML.yaml_parser_initialize(@parser)

    if content.is_a?(String)
      LibYAML.yaml_parser_set_input_string(@parser, content, content.bytesize)
    else
      LibYAML.yaml_parser_set_input(@parser, ->(data, buffer, size, size_read) {
        parser = data.as(YAML::PullParser)
        io = parser.content.as(IO)
        slice = Slice.new(buffer, size)
        actual_read_bytes = io.read(slice)
        size_read.value = LibC::SizeT.new(actual_read_bytes)
        LibC::Int.new(1)
      }, self.as(Void*))
    end

    read_next
    raise "Expected STREAM_START" unless kind.stream_start?
  end

  # Creates a parser, yields it to the block, and closes
  # the parser at the end of it.
  def self.new(content, &)
    parser = new(content)
    yield parser ensure parser.close
  end

  # The current event kind.
  def kind : EventKind
    @event.type
  end

  # Returns the tag associated to the current event, or `nil`
  # if there's no tag.
  def tag : String?
    case kind
    when .mapping_start?
      ptr = @event.data.mapping_start.tag
    when .sequence_start?
      ptr = @event.data.sequence_start.tag
    when .scalar?
      ptr = @event.data.scalar.tag
    else
      # no tag
    end
    ptr ? String.new(ptr) : nil
  end

  # Returns the scalar value, assuming the pull parser
  # is located at a scalar. Raises otherwise.
  def value : String
    expect_kind EventKind::SCALAR

    ptr = @event.data.scalar.value
    ptr ? String.new(ptr, @event.data.scalar.length) : ""
  end

  # Returns the anchor associated to the current event, or `nil`
  # if there's no anchor.
  getter anchor : String?

  # Returns the sequence style, assuming the pull parser is located
  # at a sequence begin event. Raises otherwise.
  def sequence_style : SequenceStyle
    expect_kind EventKind::SEQUENCE_START
    @event.data.sequence_start.style
  end

  # Returns the mapping style, assuming the pull parser is located
  # at a mapping begin event. Raises otherwise.
  def mapping_style : MappingStyle
    expect_kind EventKind::MAPPING_START
    @event.data.mapping_start.style
  end

  # Returns the scalar style, assuming the pull parser is located
  # at a scalar event. Raises otherwise.
  def scalar_style : ScalarStyle
    expect_kind EventKind::SCALAR
    @event.data.scalar.style
  end

  # Reads the next event.
  def read_next : EventKind
    LibYAML.yaml_event_delete(pointerof(@event))
    LibYAML.yaml_parser_parse(@parser, pointerof(@event))
    if problem = problem?
      msg = String.new(problem)
      location = {problem_line_number, problem_column_number}
      if context = context?
        context_info = {String.new(context), context_line_number, context_column_number}
      end
      raise msg, *location, context_info
    end

    read_anchor
    @anchors += 1 if @anchor

    case kind
    when EventKind::SEQUENCE_START, EventKind::MAPPING_START
      increase_nesting
    when EventKind::SEQUENCE_END, EventKind::MAPPING_END
      decrease_nesting
    when EventKind::ALIAS
      increase_alias
    end

    kind
  end

  # Reads a "stream start" event, yields to the block,
  # and then reads a "stream end" event.
  def read_stream(&)
    read_stream_start
    value = yield
    read_stream_end
    value
  end

  # Reads a "document start" event, yields to the block,
  # and then reads a "document end" event.
  def read_document(&)
    read_document_start
    value = yield
    read_document_end
    value
  end

  # Reads a "sequence start" event, yields to the block,
  # and then reads a "sequence end" event.
  def read_sequence(&)
    read_sequence_start
    value = yield
    read_sequence_end
    value
  end

  # Reads a "mapping start" event, yields to the block,
  # and then reads a "mapping end" event.
  def read_mapping(&)
    read_mapping_start
    value = yield
    read_mapping_end
    value
  end

  # Reads an alias event, returning its anchor.
  def read_alias : String?
    expect_kind EventKind::ALIAS
    anchor = @anchor
    read_next
    anchor
  end

  # Reads a scalar, returning its value.
  def read_scalar : String
    expect_kind EventKind::SCALAR
    value = self.value
    read_next
    value
  end

  # Reads a "stream start" event.
  def read_stream_start
    read EventKind::STREAM_START
  end

  # Reads a "stream end" event.
  def read_stream_end
    read EventKind::STREAM_END
  end

  # Reads a "document start" event.
  def read_document_start
    read EventKind::DOCUMENT_START
  end

  # Reads a "document end" event.
  def read_document_end
    read EventKind::DOCUMENT_END
  end

  # Reads a "sequence start" event.
  def read_sequence_start
    read EventKind::SEQUENCE_START
  end

  # Reads a "sequence end" event.
  def read_sequence_end
    read EventKind::SEQUENCE_END
  end

  # Reads a "mapping start" event.
  def read_mapping_start
    read EventKind::MAPPING_START
  end

  # Reads a "mapping end" event.
  def read_mapping_end
    read EventKind::MAPPING_END
  end

  # Reads an expected event kind.
  def read(expected_kind : EventKind) : EventKind
    expect_kind expected_kind
    read_next
  end

  def skip : YAML::EventKind
    case kind
    when .scalar?
      read_next
    when .alias?
      read_next
    when .sequence_start?
      read_next
      until kind.sequence_end?
        skip
      end
      read_next
    when .mapping_start?
      read_next
      until kind.mapping_end?
        skip
        skip
      end
      read_next
    when .document_start?
      read_next
      until kind.document_end?
        skip
      end
      read_next
    when .stream_start?
      read_next
      until kind.stream_end?
        skip
      end
      read_next
    else
      read_next
    end
  end

  # Note: YAML starts counting from 0, we want to count from 1

  def location : {Int32, Int32}
    {start_line, start_column}
  end

  def start_line : Int32
    @event.start_mark.line.to_i32 + 1
  end

  def start_column : Int32
    @event.start_mark.column.to_i32 + 1
  end

  def end_line : Int32
    @event.end_mark.line.to_i32 + 1
  end

  def end_column : Int32
    @event.end_mark.column.to_i32 + 1
  end

  private def problem_line_number
    (problem? ? problem_mark?.line : start_line) + 1
  end

  private def problem_column_number
    (problem? ? problem_mark?.column : start_column) + 1
  end

  private def problem_mark?
    @parser.value.problem_mark
  end

  private def problem?
    @parser.value.problem
  end

  private def context?
    @parser.value.context
  end

  private def context_mark?
    @parser.value.context_mark
  end

  private def context_line_number
    # YAML starts counting from 0, we want to count from 1
    context_mark?.line + 1
  end

  private def context_column_number
    # YAML starts counting from 0, we want to count from 1
    context_mark?.column + 1
  end

  def finalize
    return if @closed

    LibYAML.yaml_parser_delete(@parser)
    LibYAML.yaml_event_delete(pointerof(@event))
  end

  def close : Nil
    finalize
    @closed = true
  end

  # Raises if the current kind is not the expected one.
  def expect_kind(kind : EventKind) : Nil
    raise "Expected #{kind} but was #{self.kind}" unless kind == self.kind
  end

  private def read_anchor
    anchor =
      case kind
      when .scalar?
        @event.data.scalar.anchor
      when .sequence_start?
        @event.data.sequence_start.anchor
      when .mapping_start?
        @event.data.mapping_start.anchor
      when .alias?
        @event.data.alias.anchor
      else
        Pointer(UInt8).null
      end
    @anchor = anchor ? String.new(anchor) : nil
  end

  def raise(msg : String, line_number = self.start_line, column_number = self.start_column, context_info = nil) : NoReturn
    ::raise ParseException.new(msg, line_number, column_number, context_info)
  end

  private def increase_nesting
    @nesting += 1

    if @nesting > @max_nesting
      raise "Nesting of #{@nesting} is too deep"
    end

    if anchor = @anchor
      @anchor_scope[@nesting] = {anchor, @aliases}
    end
  end

  private def decrease_nesting
    if scope = @anchor_scope.delete(@nesting)
      anchor, aliases = scope
      @anchor_costs[anchor] = @aliases - aliases
    end

    @nesting -= 1
  end

  private def increase_alias
    aliases = @anchor_costs[@anchor]? || 0
    @aliases += aliases + 1

    if @enforce_alias_anchor_ratio &&
       @aliases > @alias_anchor_min_aliases &&
       @aliases > @alias_anchor_multiplier * @anchors
      raise "Document contains excessive aliasing"
    end
  end
end
