require "weak_ref"

class XML::Document < XML::Node
  # :nodoc:
  #
  # The constructors allocate a XML::Node for a libxml node once, so we don't
  # finalize a document twice for example.
  #
  # We store the reference into the libxml struct (_private) for documents
  # because a document's XML::Node lives as long as its libxml doc. However we
  # can lose references to subtree XML::Node, so using _private would leave
  # dangling pointers. We thus keep a cache of weak references to all nodes in
  # the document, so we can still collect lost references, and at worst
  # reinstantiate a XML::Node if needed.
  #
  # NOTE: when a XML::Node is moved to another document, the XML::Node and any
  # instantiated descendant XML::Node shall be cleaned from the original
  # document's cache, and must be added to the new document's cache.
  protected getter cache : Hash(LibXML::Node*, WeakRef(Node))

  # :nodoc:
  #
  # Unlinked libxml nodes, and all their descendant nodes, don't appear in the
  # document's tree anymore, and must be manually freed, yet we can't merely
  # free the libxml node in a finalizer, because it would free the whole
  # subtree, while we may still have live XML::Node instances.
  #
  # We keep an explicit list of unlinked libxml nodes. We can't rely on the
  # cache because it uses weak references and the XML::Node could be collected,
  # leaking the libxml node and its subtree.
  #
  # NOTE: the libxml node, along with any descendant shall be removed from the
  # list when relinked into a tree, be it the same document or another.
  protected getter unlinked_nodes : Set(LibXML::Node*)

  # :nodoc:
  def self.new(doc : LibXML::Doc*, errors : Array(Error)? = nil) : Document
    if ptr = doc.value._private
      ptr.as(Document)
    else
      new(doc_: doc, errors_: errors)
    end
  end

  # Must never be called directly, use the constructors above.
  private def initialize(*, doc_ : LibXML::Doc*, errors_ : Array(Error)?)
    @node = doc_.as(LibXML::Node*)
    @errors = errors_
    @cache = Hash(LibXML::Node*, WeakRef(Node)).new
    @unlinked_nodes = Set(LibXML::Node*).new
    @document = self
    doc_.value._private = self.as(Void*)
  end

  # :nodoc:
  def finalize
    # free unlinked nodes and their subtrees
    @unlinked_nodes.each do |node|
      if node.value.doc == @node
        LibXML.xmlFreeNode(node)
      else
        # the node has been adopted into another document, don't free!
      end
    end

    # free the doc and its subtree
    LibXML.xmlFreeDoc(@node.as(LibXML::Doc*))
  end

  # Returns the encoding of this node's document.
  def encoding : String?
    if encoding = @node.as(LibXML::Doc*).value.encoding
      String.new(encoding)
    end
  end

  # Returns the version of this node's document.
  def version : String?
    if version = @node.as(LibXML::Doc*).value.version
      String.new(version)
    end
  end

  # :nodoc:
  def document : Document
    self
  end

  # Returns the list of `XML::Error` found when parsing this document.
  # Returns `nil` if no errors were found.
  def errors : Array(XML::Error)?
    @errors unless @errors.try &.empty?
  end
end
