# Parsing context that holds anchors and what they refer to.
#
# When implementing `new(ctx : YAML::ParseContext, node : YAML::Nodes::Node)`
# to deserialize an object from a node, `Reference` types must invoke
# both `read_alias` and `record_anchor` in order to support parsing
# recursive data structures.
#
# - `read_alias` must be invoked before an instance is created
# - `record_anchor` must be invoked after an instance is created and
#   before its members are deserialized.
class YAML::ParseContext
  def initialize
    # Recorded anchors: anchor => {boxed object, crystal_type_id}
    @anchors = {} of String => {Void*, Int32}
  end

  # Associates an object with an anchor.
  def record_anchor(node, object : T) : Nil forall T
    if anchor = node.anchor
      @anchors[anchor] = {Box(T).box(object), object.crystal_type_id}
    end
  end

  # Tries to read an alias from `node` of type `T`. Invokes
  # the block if successful, and invokers must return this object
  # instead of deserializing their members.
  def read_alias(node, type : T.class, &) forall T
    if ptr = read_alias_impl(node, T.crystal_instance_type_id, raise_on_alias: true)
      yield Box(T).unbox(ptr)
    end
  end

  # Similar to `read_alias` but doesn't raise if an alias exists
  # but an instance of type T isn't associated with the current anchor.
  def read_alias?(node, type : T.class, &) forall T
    if ptr = read_alias_impl(node, T.crystal_instance_type_id, raise_on_alias: false)
      yield Box(T).unbox(ptr)
    end
  end

  private def read_alias_impl(node, expected_crystal_type_id, raise_on_alias)
    if node.is_a?(YAML::Nodes::Alias)
      value = @anchors[node.anchor]?

      if value
        box, crystal_type_id = value
        if crystal_type_id == expected_crystal_type_id
          return box
        end
      end

      raise("Error deserializing alias") if raise_on_alias
    end

    nil
  end
end
