module Nokogiri
  module XML
    class Node

      attr_accessor :cstruct # :nodoc:

      def pointer_id # :nodoc:
        cstruct.pointer
      end

      def encode_special_chars(string) # :nodoc:
        char_ptr = LibXML.xmlEncodeSpecialChars(self[:doc], string)
        encoded = char_ptr.read_string
        # TODO: encoding?
        LibXML.xmlFree(char_ptr)
        encoded
      end

      def internal_subset # :nodoc:
        return nil if cstruct[:doc].null?
        doc = cstruct.document
        dtd = LibXML.xmlGetIntSubset(doc)
        return nil if dtd.null?
        Node.wrap(dtd)
      end

      def dup(deep = 1) # :nodoc:
        dup_ptr = LibXML.xmlDocCopyNode(cstruct, cstruct.document, deep)
        return nil if dup_ptr.null?
        Node.wrap(dup_ptr, self.class)
      end

      def unlink # :nodoc:
        LibXML.xmlUnlinkNode(cstruct)
        cstruct.keep_reference_from_document!
        self
      end

      def blank? # :nodoc:
        LibXML.xmlIsBlankNode(cstruct) == 1
      end

      def next_sibling # :nodoc:
        cstruct_node_from :next
      end

      def previous_sibling # :nodoc:
        cstruct_node_from :prev
      end

      def replace_with_node(new_node) # :nodoc:
        LibXML.xmlReplaceNode(cstruct, new_node.cstruct)
        Node.send(:relink_namespace, new_node.cstruct)
        self
      end

      def children # :nodoc:
        return NodeSet.new(nil) if cstruct[:children].null?
        child = Node.wrap(cstruct[:children])

        set = NodeSet.new child.document
        set_ptr = LibXML.xmlXPathNodeSetCreate(child.cstruct)
        
        set.cstruct = LibXML::XmlNodeSet.new(set_ptr)
        return set unless child

        child_ptr = child.cstruct[:next]
        while ! child_ptr.null?
          child = Node.wrap(child_ptr)
          LibXML.xmlXPathNodeSetAdd(set.cstruct, child.cstruct)
          child_ptr = child.cstruct[:next]
        end

        return set
      end

      def child # :nodoc:
        (val = cstruct[:children]).null? ? nil : Node.wrap(val)
      end

      def key?(attribute) # :nodoc:
        ! (prop = LibXML.xmlHasProp(cstruct, attribute.to_s)).null?
      end

      def namespaced_key?(attribute, namespace) # :nodoc:
        prop = LibXML.xmlHasNsProp(cstruct, attribute.to_s,
          namespace.nil? ? nil : namespace.to_s)
        prop.null? ? false : true
      end

      def []=(property, value) # :nodoc:
        LibXML.xmlSetProp(cstruct, property, value)
        value
      end

      def get(attribute) # :nodoc:
        return nil unless attribute
        propstr = LibXML.xmlGetProp(cstruct, attribute.to_s)
        return nil if propstr.null?
        rval = propstr.read_string # TODO: encoding?
        LibXML.xmlFree(propstr)
        rval
      end

      def set_namespace(namespace)
        LibXML.xmlSetNs(cstruct, namespace.cstruct)
        self
      end

      def attribute(name) # :nodoc:
        raise "Node#attribute not implemented yet"
      end

      def attribute_with_ns(name, namespace) # :nodoc:
        prop = LibXML.xmlHasNsProp(cstruct, name.to_s,
          namespace.nil? ? NULL : namespace.to_s)
        return prop if prop.null?
        Node.wrap(prop)
      end

      def attribute_nodes # :nodoc:
        attr = []
        prop_cstruct = cstruct[:properties]
        while ! prop_cstruct.null?
          prop = Node.wrap(prop_cstruct)
          attr << prop
          prop_cstruct = prop.cstruct[:next]
        end
        attr
      end

      def namespace # :nodoc:
        cstruct[:ns].null? ? nil : Namespace.wrap(cstruct.document, cstruct[:ns])
      end

      def namespace_definitions # :nodoc:
        list = []
        ns_ptr = cstruct[:nsDef]
        return list if ns_ptr.null?
        while ! ns_ptr.null?
          ns = Namespace.wrap(cstruct.document, ns_ptr)
          list << ns
          ns_ptr = ns.cstruct[:next]
        end
        list
      end

      def node_type # :nodoc:
        cstruct[:type]
      end

      def native_content=(content) # :nodoc:
        LibXML.xmlNodeSetContent(cstruct, content)
        content
      end

      def content # :nodoc:
        content_ptr = LibXML.xmlNodeGetContent(cstruct)
        return nil if content_ptr.null?
        content = content_ptr.read_string # TODO: encoding?
        LibXML.xmlFree(content_ptr)
        content
      end

      def add_child(child) # :nodoc:
        Node.reparent_node_with(child, self) do |child_cstruct, my_cstruct|
          LibXML.xmlAddChild(my_cstruct, child_cstruct)
        end
      end

      def parent # :nodoc:
        cstruct_node_from :parent
      end
      
      def node_name=(string) # :nodoc:
        LibXML.xmlNodeSetName(cstruct, string)
        string
      end

      def node_name # :nodoc:
        cstruct[:name] # TODO: encoding?
      end

      def path # :nodoc:
        path_ptr = LibXML.xmlGetNodePath(cstruct)
        val = path_ptr.null? ? nil : path_ptr.read_string # TODO: encoding?
        LibXML.xmlFree(path_ptr)
        val
      end

      def add_next_sibling(next_sibling) # :nodoc:
        Node.reparent_node_with(next_sibling, self) do |sibling_cstruct, my_cstruct|
          LibXML.xmlAddNextSibling(my_cstruct, sibling_cstruct)
        end
      end

      def add_previous_sibling(prev_sibling) # :nodoc:
        Node.reparent_node_with(prev_sibling, self) do |sibling_cstruct, my_cstruct|
          LibXML.xmlAddPrevSibling(my_cstruct, sibling_cstruct)
        end
      end

      def native_write_to(io, encoding, indent_string, options) # :nodoc:
        set_xml_indent_tree_output 1
        set_xml_tree_indent_string indent_string
        savectx = LibXML.xmlSaveToIO(IoCallbacks.writer(io), nil, nil, encoding, options)
        LibXML.xmlSaveTree(savectx, cstruct)
        LibXML.xmlSaveClose(savectx)
        io
      end

      def line # :nodoc:
        cstruct[:line]
      end

      def add_namespace_definition(prefix, href) # :nodoc:
        ns = LibXML.xmlNewNs(cstruct, href, prefix)
        if ns.null?
          ns = LibXML.xmlSearchNs(cstruct.document, cstruct,
            prefix.nil? ? nil : prefix.to_s)
        end
        LibXML.xmlSetNs(cstruct, ns) if prefix.nil?
        Namespace.wrap(cstruct.document, ns)
      end

      def self.new(name, document, *rest) # :nodoc:
        ptr = LibXML.xmlNewNode(nil, name.to_s)

        node_cstruct = LibXML::XmlNode.new(ptr)
        node_cstruct[:doc] = document.cstruct[:doc]
        node_cstruct.keep_reference_from_document!

        node = Node.wrap(node_cstruct, self)
        node.send :initialize, name, document, *rest
        yield node if block_given?
        node
      end

      def dump_html # :nodoc:
        return to_xml if type == DOCUMENT_NODE
        buffer = LibXML::XmlBuffer.new(LibXML.xmlBufferCreate())
        LibXML.htmlNodeDump(buffer, cstruct[:doc], cstruct)
        buffer[:content] # TODO: encoding?
      end

      def compare(other) # :nodoc:
        LibXML.xmlXPathCmpNodes(other.cstruct, self.cstruct)
      end

      def self.wrap(node_struct, klass=nil) # :nodoc:
        if node_struct.is_a?(FFI::Pointer)
          # cast native pointers up into a node cstruct
          return nil if node_struct.null?
          node_struct = LibXML::XmlNode.new(node_struct) 
        end

        raise "wrapping a node without a document" unless node_struct.document

        document_struct = node_struct.document
        document = document_struct.nil? ? nil : document_struct.ruby_doc
        if node_struct[:type] == DOCUMENT_NODE || node_struct[:type] == HTML_DOCUMENT_NODE
          return document
        end

        ruby_node = node_struct.ruby_node
        return ruby_node unless ruby_node.nil?

        klasses = case node_struct[:type]
                  when ELEMENT_NODE then [XML::Element]
                  when TEXT_NODE then [XML::Text]
                  when ENTITY_REF_NODE then [XML::EntityReference]
                  when COMMENT_NODE then [XML::Comment]
                  when DOCUMENT_FRAG_NODE then [XML::DocumentFragment]
                  when PI_NODE then [XML::ProcessingInstruction]
                  when ATTRIBUTE_NODE then [XML::Attr]
                  when ENTITY_DECL then [XML::EntityDeclaration]
                  when CDATA_SECTION_NODE then [XML::CDATA]
                  when DTD_NODE then [XML::DTD, LibXML::XmlDtd]
                  else [XML::Node]
                  end

        if klass
          node = klass.allocate
        else
          node = klasses.first.allocate
        end
        node.cstruct = klasses[1] ? klasses[1].new(node_struct.pointer) : node_struct

        node.cstruct.ruby_node = node

        cache = document.instance_variable_get(:@node_cache)
        cache << node

        node.instance_variable_set(:@document, document)
        document.decorate(node)
        node
      end

      def document
        cstruct.document.ruby_doc
      end

      private

      def self.reparent_node_with(node, other, &block) # :nodoc:
        raise(ArgumentError, "node must be a Nokogiri::XML::Node") unless node.is_a?(Nokogiri::XML::Node)

        if node.cstruct[:doc] == other.cstruct[:doc]
          LibXML.xmlUnlinkNode(node.cstruct)
          if node.type == TEXT_NODE && other.type == TEXT_NODE && Nokogiri.is_2_6_16?
            other.cstruct.pointer.put_pointer(other.cstruct.offset_of(:content), LibXML.xmlStrdup(other.cstruct[:content]))
          end
          reparented_struct = block.call(node.cstruct, other.cstruct)
          raise(RuntimeError, "Could not reparent node (1)") unless reparented_struct
        else
          duped_node = LibXML.xmlDocCopyNode(node.cstruct, other.cstruct.document, 1)
          raise(RuntimeError, "Could not reparent node (xmlDocCopyNode)") unless duped_node
          reparented_struct = block.call(duped_node, other.cstruct)
          raise(RuntimeError, "Could not reparent node (2)") unless reparented_struct
          LibXML.xmlUnlinkNode(node.cstruct)
          node.cstruct.keep_reference_from_document!
        end
        
        reparented_struct = LibXML::XmlNode.new(reparented_struct)

        # the child was a text node that was coalesced. we need to have the object
        # point at SOMETHING, or we'll totally bomb out.
        if reparented_struct != node.cstruct
          node.cstruct = reparented_struct
        end

        relink_namespace reparented_struct

        reparented = Node.wrap(reparented_struct)
        reparented.decorate!
        reparented
      end

      def self.relink_namespace(reparented_struct) # :nodoc:
        # Make sure that our reparented node has the correct namespaces
        if reparented_struct[:doc] != reparented_struct[:parent]
          LibXML.xmlSetNs(reparented_struct, LibXML::XmlNode.new(reparented_struct[:parent])[:ns])
        end

        # Search our parents for an existing definition
        if ! reparented_struct[:nsDef].null?
          ns = LibXML.xmlSearchNsByHref(
            reparented_struct[:doc],
            reparented_struct[:parent],
            LibXML::XmlNs.new(reparented_struct[:nsDef])[:href]
            )
          reparented_struct[:nsDef] = nil unless ns.null?
        end

        # Only walk all children if there actually is a namespace we need to reparent.
        return if reparented_struct[:ns].null?

        # When a node gets reparented, walk it's children to make sure that
        # their namespaces are reparented as well.
        child_ptr = reparented_struct[:children]
        while ! child_ptr.null?
          child_struct = LibXML::XmlNode.new(child_ptr) 
          relink_namespace child_struct
          child_ptr = child_struct[:next]
        end
      end

      def cstruct_node_from(sym) # :nodoc:
        (val = cstruct[sym]).null? ? nil : Node.wrap(val)
      end

      def set_xml_indent_tree_output(value) # :nodoc:
        LibXML.__xmlIndentTreeOutput.write_int(value)
      end

      def set_xml_tree_indent_string(value) # :nodoc:
        LibXML.__xmlTreeIndentString.write_pointer(LibXML.xmlStrdup(value.to_s))
      end

    end
  end
end
