require 'java'

include_class 'javax.xml.parsers.DocumentBuilderFactory'
include_class 'org.xml.sax.InputSource'
include_class 'java.io.StringReader'
include_class 'org.w3c.dom.Node'
include_class 'javax.xml.xpath.XPathFactory'
include_class 'javax.xml.xpath.XPathConstants'
include_class 'javax.xml.namespace.NamespaceContext'
include_class 'javax.xml.XMLConstants'

module JREXML

  class Parser

    @@dbf = nil

    def Parser.unparse(node, output)
      Unparser.new(output).unparse(node)
    end

    def Parser.parse(text)
      begin
        unless @@dbf
          @@dbf = DocumentBuilderFactory.newInstance
          @@dbf.setNamespaceAware(true)
        end

        db = @@dbf.newDocumentBuilder
        return db.parse(InputSource.new(StringReader.new(text)))

      rescue NativeException
        return $!.to_s
      end
    end

    def Parser.getAttribute(target, node)
      Nodes.each_node(node.getAttributes) do |attr|
        if attr.getName == target
          return attr
        end
      end
      return nil
    end

    def Parser.escape(text)
      text.gsub(/([&<'">])/) do
        case $1
        when '&' then '&amp;'
        when '<' then '&lt;'
        when "'" then '&apos;'
        when '"' then '&quot;'
        when '>' then '&gt;'
        end
      end
    end

    def Parser.unescape(text)
      text.gsub(/&([^;]*);/) do
        case $1
        when 'lt' then '<'
        when 'amp' then '&'
        when 'gt' then '>'
        when 'apos' then "'"
        when 'quot' then '"'
        end
      end
    end
  end

  class Unparser

    def initialize output
      @output = output
      @nspref = 1
      @namespaces = {}
      @namespaceDeclarations = []
    end

    def unparse doc
      # this is how it comes back from the Java APIs
      unparse1 doc
    end

    def unparse1 node
      case node.getNodeType
      when Node::CDATA_SECTION_NODE, Node::TEXT_NODE
        out Parser.escape(node.getNodeValue)
      when Node::COMMENT_NODE, Node::PROCESSING_INSTRUCTION_NODE
        # bah
      when Node::DOCUMENT_NODE
        node = node.getFirstChild
        unparse1 node
      when Node::DOCUMENT_TYPE_NODE
        node = node.getNextSibling
        unparse1 node
      when Node::ELEMENT_NODE
        unparseStartTag node
        Nodes.each_node(node.getChildNodes) { |child| unparse1(child) }
        unparseEndTag node
      when Node::ENTITY_NODE, Node::ENTITY_REFERENCE_NODE, Node::NOTATION_NODE
        raise(ArgumentError, "Floating XML goo, can't serialize")
      else
        raise(ArgumentError, "Unrecognized node type #{node.getNodeType}")
      end
    end

    def out str
      @output << str.to_s
    end

    def unparseStartTag node
      out '<'
      unparseName node
      Nodes.each_node(node.getAttributes) { |a| unparseAttribute(a) }
      declareNamespacesIfNecessary
      out '>'
    end

    def declareNamespacesIfNecessary
      @namespaceDeclarations.each do |ns|
        out " xmlns:#{@namespaces[ns]}='#{Parser.unescape(ns)}'"
      end
      @namespaceDeclarations = []
    end

    def unparseAttribute attr  
      out ' '
      unparseName attr
      out '="'
      out Parser.escape(attr.getNodeValue)
      out '"'
    end

    def unparseName node
      out node.getNodeName

      ## Hmm... DOM seems to come preloaded with all this stuff
      # ns = node.getNamespaceURI
      # if ns
      #  pref = @namespaces[ns]
      #  unless pref
      #    pref = 'ns' + @nspref.to_s
      #    @namespaces[ns] = pref
      #    @namespaceDeclarations << ns
      #    @nspref = @nspref +1
      #  end
      #  out "#{pref}:#{node.getNodeName}"
      #else
      #  out node.getNodeName
      #end
    end

    def unparseEndTag node
      out '</'
      unparseName node
      out '>'
    end
  end

  class Element

    attr_reader :dom, :attributes

    def initialize input

      if input.class == String
        @dom = Parser.parse input
        if @dom.class == String
          raise(ArgumentError, "Can't parse text input: #{@dom}")
        end

      elsif input.respond_to? :getChildNodes
        @dom = input
      else
        raise(ArgumentError,
              "Argument to Element.new must be text or a DOM node")
      end

      # This could screw up XPaths
      if @dom.getNodeType == Node::DOCUMENT_NODE
        @dom = @dom.getFirstChild
      end

      @attributes = Attributes.new @dom
    end

    def to_s
      Parser.unparse(@dom, '')
    end
    def to_str
      to_s
    end
  end

  class XPath 
    @@xpf = nil

    def XPath.match node, path, namespaces={}
      node = fixNode node
      xp = XPath.newXP namespaces
      collect xp.evaluate(path, node, XPathConstants::NODESET)    
    end

    def XPath.each node, path, namespaces = {}
      node = fixNode node
      xp = XPath.newXP namespaces
      list = xp.evaluate(path, node, XPathConstants::NODESET)
      collect(list).each { |node| yield node }
    end

    def XPath.first node, path, namespaces = {}
      node = fixNode node
      xp = XPath.newXP namespaces
      xp.evaluate(path, node, XPathConstants::NODE)
    end

    private

    def XPath.fixNode node
      if node.class == Element
        node.dom
      elsif node.respond_to? :getChildNodes
        node
      else
        raise "Invalid argument to xpath"
      end
    end

    def XPath.newXP namespaces
      raise "The namespaces argument, if supplied, must be a hash object." unless namespaces.kind_of? Hash
      @@xpf ||= XPathFactory.newInstance
      xp = @@xpf.newXPath

      # It turns out that if you have a constructor with args, Java blows chunks
      #  when you try to pass it to namespaceContext= - probably a JRuby bug
      n = NSCT.new
      n.namespaces= namespaces
      xp.namespaceContext= n
      return xp
    end

    def XPath.collect list
      len = list.getLength
      (0 ... len).collect { |i| list.item(i) }
    end
  end

  class NSCT < NamespaceContext

    def namespaces= namespaces
      @namespaces = namespaces
    end

    def getNamespaceURI prefix
      if prefix == 'xml' 
        XMLConstants::XML_NS_URI
      else
        @namespaces[prefix]
      end
    end
  end

  class Attributes

    # should only be called by Element
    def initialize node
      @attrsNode = node.getAttributes
    end

    def [] name
      if name =~ /^(.*):(.*)$/
        ns = node.getNamespaceURI $1
        anode = @attrsNode.getNamedItemNS(ns, $2)
      else
        anode = @attrsNode.getNamedItem(name)
      end
      anode.getNodeValue
    end
    
  end

  class Nodes

    def Nodes.each_node(list)
      len = list.getLength
      (0 ... len).each do |i|
        yield list.item(i)
      end
    end
  end
end

