# MWDOM core
# Copyright (c)2001, MoonWolf. All right reserved.
require "observer"
require "singleton"

module MWDOM
  
  def escape(text)
    text = text.gsub(/&/, "&amp;")
    text.gsub!(/</, "&lt;")
    text.gsub!(/>/, "&gt;")
    text.gsub!(/'/, "&#39;")
    text.gsub!(/"/, "&#34;")
    text
  end
  module_function :escape
  
  def localName(qualifiedName)
    if qualifiedName=~/:/
      $'
    else
      qualifiedName
    end
  end
  module_function :localName
  
  #class DOMString < String; end
  #class DOMTimeStamp < Bignum; end

  INDEX_SIZE_ERR              = 1
  DOMSTRING_SIZE_ERR          = 2
  HIERARCHY_REQUEST_ERR       = 3
  WRONG_DOCUMENT_ERR          = 4
  INVALID_CHARACTER_ERR       = 5
  NO_DATA_ALLOWED_ERR         = 6
  NO_MODIFICATION_ALLOWED_ERR = 7
  NOT_FOUND_ERR               = 8
  NOT_SUPPORTED_ERR           = 9
  INUSE_ATTRIBUTE_ERR         = 10
  INVALID_STATE_ERR           = 11
  SYNTAX_ERR                  = 12
  INVALID_MODIFICATION_ERR    = 13
  NAMESPACE_ERR               = 14
  INVALID_ACCESS_ERR          = 15
  
  class DOMException < Exception
    MSG = [
      "INDEX_SIZE_ERR",
      "DOMSTRING_SIZE_ERR",
      "HIERARCHY_REQUEST_ERR",
      "WRONG_DOCUMENT_ERR",
      "INVALID_CHARACTER_ERR",
      "NO_DATA_ALLOWED_ERR",
      "NO_MODIFICATION_ALLOWED_ERR",
      "NOT_FOUND_ERR",
      "NOT_SUPPORTED_ERR",
      "INUSE_ATTRIBUTE_ERR",
      "INVALID_STATE_ERR",
      "SYNTAX_ERR",
      "INVALID_MODIFICATION_ERR",
      "NAMESPACE_ERR",
      "INVALID_ACCESS_ERR",
    ]
    def initialize(code)
      @code = code
    end
    attr_reader :code
    
    def to_s
      MSG[@code-1]
    end
  end



  class DOMImplementation
    include Singleton
    IMPLEMENT = {
      "Core" => ["2.0"],
      "XML"  => ["2.0"],
    }
    #
    def hasFeature(feature, version)
      ver=IMPLEMENT[feature]
      if ver and ver.include?(version)
        true
      else
        false
      end
    end
    
    def createDocumentType(qualifiedName, publicId, systemId)
      DocumentType.new(qualifiedName, publicId, systemId)
    end
    
    def createDocument(namespaceURI, qualifiedName, doctype)
      document = Document.new(self)
      if doctype
        document.appendChild(doctype)
      end
      
      document
    end
  end



  class NodeList
    def initialize
      @nodelist = Array.new
    end
    attr_accessor :nodelist
    
    def to_s
      @nodelist.to_s
    end
    
    #Observer
    def update(node)
      @nodelist.delete(node)
      node.delete_observer(self)
    end
    
    #DOM
    def item(index)
      if index>=0 and index<@nodelist.size
        @nodelist[index]
      else
        nil
      end
    end
    
    def length
      @nodelist.length
    end
    
  end



  class NamedNodeMap
    def initialize(doc)
      @ownerDocument = doc
      @hash = {}
    end
    attr_reader :hash
    
    def to_s
      s = ""
      @hash.each {|k,v|
        s << v.to_s
      }
      s
    end
    
    private
    def nameNS(ns,name)
      if ns
        name=ns+":"+name
      else
        name
      end
    end
    
    #DOM
    public
    attr_reader :ownerDocument
    
    def getNamedItem(name)
      @hash[name]
    end
    
    def setNamedItem(arg)
      if not @ownerDocument.equal?(arg.ownerDocument)
        raise DOMException.new(INUSE_ATTRIBUTE_ERR)
      end
      
      name = nameNS(arg.namespaceURI,arg.nodeName.dup)
      if old=@hash[name]
        @hash[name]=arg
        old
      else
        @hash[name]=arg
        nil
      end
    end
    
    def removeNamedItem(name)
      @hash.delete(name)
    end
    
    def item(index)
      attr=@hash.to_a[index]
      if attr
        attr[1]
      else
        nil
      end
    end
    
    def length
      @hash.length
    end
    
    def getNamedItemNS(namespaceURI, localName)
      name = nameNS(namespaceURI, localName)
      @hash[name]
    end
    
    alias setNamedItemNS setNamedItem
    
    def removeNamedItemNS(namespaceURI, localName)
      name = nameNS(namespaceURI, localName)
      @hash.delete(name)
    end
    
  end



  # NodeType
  ELEMENT_Node                = 1
  ATTRIBUTE_Node              = 2
  TEXT_Node                   = 3
  CDATA_SECTION_Node          = 4
  ENTITY_REFERENCE_Node       = 5
  ENTITY_Node                 = 6
  PROCESSING_INSTRUCTION_Node = 7
  COMMENT_Node                = 8
  DOCUMENT_Node               = 9
  DOCUMENT_TYPE_Node          = 10
  DOCUMENT_FRAGMENT_Node      = 11
  NOTATION_Node               = 12
  
  module Node
    include Observable
    
    def initialize(ownerDocument)
      @ownerDocument = ownerDocument
      @nodeName      = nil
      @nodeValue     = nil
      @parentNode    = nil
      @childNodes    = NodeList.new
      @attributes    = NamedNodeMap.new(ownerDocument)
      @prefix        = nil
    end
    
    
    #DOM
    def nodeName
      @nodeName
    end
    
    def nodeValue
      @nodeValue
    end
    
    def nodeValue=(val)
      @nodeValue=val
    end
    
    def nodeType
      raise "Attr#nodeType"
    end
    
    attr_accessor :parentNode
    attr_reader   :childNodes
    
    def firstChild
      @childNodes.nodelist.first
    end
    
    def lastChild
      @childNodes.nodelist.last
    end
    
    def previousSibling
      if @parentNode and idx=@parentNode.childNodes.nodelist.index(self)
        @parentNode.childNodes.item(idx-1)
      end
    end
    
    def nextSibling
      if @parentNode and idx=@parentNode.childNodes.nodelist.index(self)
        @parentNode.childNodes.item(idx+1)
      end
    end
    
    def attributes
      nil
    end
    
    def ownerDocument
      @ownerDocument
    end
    
    def insertBefore(newChild, refChild)
      if refChild==nil
        return appendChild(newChild)
      end
      idx=@childNodes.nodelist.index(refChild)
      raise DOMException.new(NOT_FOUND_ERR) unless idx
      if newChild.nodeType==DOCUMENT_FRAGMENT_Node
        newChild.childNodes.nodelist.each {|node|
          @childNodes.nodelist[idx,0] = node;
          node.parentNode=self;
          idx += 1
        }
      else
        if parent=newChild.parentNode
          parent.removeChild(newChild)
        end
        @childNodes.nodelist[idx,0] = newChild
        newChild.parentNode=self
      end
    end
    
    def replaceChild(newChild, oldChild)
      idx=@childNodes.nodelist.index(oldChild)
      removeChild(oldChild)
      if parent=newChild.parentNode
        parent.removeChild(newChild)
      end
      @childNodes.nodelist[idx,0]=newChild
      @childNodes.nodelist << newChild
      newChild.parentNode=self
    end
    
    def removeChild(oldChild)
      retval=@childNodes.nodelist.delete(oldChild)
      oldChild.changed
      oldChild.notify_observers(oldChild)
      oldChild.parentNode=nil
      return retval if retval
      raise DOMException.new(NOT_FOUND_ERR)
    end
    
    def appendChild(newChild)
      if newChild.nodeType==DOCUMENT_FRAGMENT_Node
        newChild.childNodes.nodelist.each {|node|
          @childNodes.nodelist << node;
          node.parentNode=self;
        }
      else
        if parent=newChild.parentNode
          parent.removeChild(newChild)
        end
        @childNodes.nodelist << newChild
        newChild.parentNode=self
      end
    end
    
    def hasChildNodes
      if @childNodes and @childNodes.length>0
        true
      else
        false
      end
    end
    
    def cloneNode(deep)
      raise "Not Implemented " + self.type.to_s
    end
    
    def normalize
      nodelist = @childNodes.nodelist
      idx=0
      while idx<nodelist.size
        node=nodelist[idx]
        case node.nodeType
        when ELEMENT_Node
          node.normalize
        when TEXT_Node
          if nextnode=nodelist[idx+1] and nextnode.nodeType==TEXT_Node
            node.appendData(nextnode.nodeValue)
            removeChild(nextnode)
            idx -= 1
          end
        end
        idx += 1
      end
    end
    
    def isSupported(feature, version)
      ownerDocument.implementation.hasFeature(feature, version)
    end
    
    def namespaceURI
      nil
    end
    
    def prefix
      nil
    end
    
    def prefix=(val)
      nil
    end
    
    def localName
      self.nodeName
    end
    
    def hasAttributes
      raise "Not Implemented"
    end
  end




  class Attr
    include Node
    def initialize(doc,name,value=nil,namespaceURI=nil)
      super(doc)
      @namespaceURI = namespaceURI
      @nodeName     = name
      @nodeValue    = nil
      @ownerElement = nil
      if value
        text=Text.new(@ownerDocument,value)
        @childNodes.nodelist=[text]
      end
    end
    attr_accessor :ownerElement
    
    def to_s
      "{#{@namespaceURI}}:#{@nodeName}=\"#{MWDOM::escape(nodeValue())}\""
    end
    
    #DOM Node
    attr_reader :namespaceURI
    
    def prefix
      @nodeName=~/:/
      $`
    end
    
    def prefix=(val)
      if @nodeName=~/:/
        @nodeName=val+":"+$'
      else
        @nodeName=val+":"+@nodeName
      end
    end
    
    def localName
      @nodeName=~/:/
      $' || @nodeName
    end
    
    
    #DOM
    def nodeType
      ATTRIBUTE_Node
    end
    
    def nodeValue
      @childNodes.to_s
    end
    
    def nodeValue=(val)
      text=Text.new(@ownerDocument,val)
      @childNodes.nodelist=[text]
    end
    
    def name
      @nodeName
    end
    
    def specified
      true
    end
    
    alias value nodeValue
    alias value nodeValue=
    
    def cloneNode(deep)
      attr=self.type.new(@ownerDocument,@nodeName.dup)
      if deep
        @childNodes.nodelist.each {|node|
          attr.appendChild(node.cloneNode(true))
        }
      end
      attr
    end
  end



  class Document
    include Node
    def initialize(impl)
      super(nil)
      @implementation = impl
    end
    
    def to_s
      "<?xml version=\"1.0\" encoding=\"utf-8\" ?>"+
      @childNodes.to_s
    end
    
    #DOM Node
    def nodeName
      "#document"
    end
    
    def cloneNode(deep)
      retnode = self.type.new(@implementation)
      if deep
        @childNodes.nodelist.each {|node|
          retnode.appendChild(node.cloneNode(true))
        }
      end
      retnode
    end
    
    #DOM
    def nodeType
      DOCUMENT_Node
    end
    
    def doctype
      @childNodes.nodelist.each {|node|
        return node if node.nodeType==DOCUMENT_TYPE_Node
      }
      return nil
    end
    
    def implementation
      @implementation
    end
    
    def documentElement
      @childNodes.nodelist.each {|node|
        return node if node.nodeType==ELEMENT_Node
      }
      return nil
    end
    
    def createElement(tagName)
      Element.new(self,tagName,nil)
    end
    
    def createDocumentFragment()
      DocumentFragment.new()
    end
    
    def createTextNode(data)
      Text.new(self,data)
    end
    
    def createComment(data)
      Comment.new(self,data)
    end
    
    def createCDATASection(data)
      CDATASection.new(self,data)
    end
    
    def createProcessingInstruction(target, data)
      ProcessingInstruction.new(self,target,data)
    end
    
    def createAttribute(name)
      Attr.new(self,name,nil,nil)
    end
    
    def createEntityReference(name)
      EntityReference.new(self,name)
    end
    
    def getElementsByTagName(tagname)
      retval = NodeList.new
      nodelist = retval.nodelist
      @childNodes.nodelist.each {|node|
        if node.nodeType==ELEMENT_Node
          if tagname=='*' or node.nodeName==tagname
            nodelist << node
            node.add_observer(retval)
          end
          node._getElementsByTagName(tagname).each {|node|
            nodelist << node
            node.add_observer(retval)
          }
        end
      }
      retval
    end
    
    def importNode(importedNode, deep)
      raise "Not Implemented"
    end
    
    def createElementNS(namespaceURI, qualifiedName)
      Element.new(self,qualifiedName,namespaceURI)
    end
    
    def createAttributeNS(namespaceURI, qualifiedName)
      Attr.new(self,qualifiedName,nil,namespaceURI)
    end
    
    def getElementsByTagNameNS(namespaceURI, localName)
      raise "Not Implemented"
    end
    
    def getElementById(elementId)
      raise "Not Implemented"
    end
  end



  class Element
    include Node
    def initialize(doc,qualifiedName,namespaceURI)
      super(doc)
      @namespaceURI  = namespaceURI
      @nodeName      = qualifiedName
      @ownerDocument = doc
    end
    
    def to_s
      "<{#{@namespaceURI}}:#{@nodeName}" + (
        if @attributes.length==0
          ""
        else
          @attributes.hash.collect {|key,attr|
            " "+attr.to_s
          }.to_s
        end
      ) + ">" +
      @childNodes.to_s +
      "</#{@nodeName}>"
    end
    
    def attributes=(attrs)
      @attributes=attrs
    end
    
    #DOM Nodes
    attr_reader :attributes
    
    def cloneNode(deep)
      retnode = self.type.new(@ownerDocument,@nodeName.dup)
      @attributes.hash.each {|key,attr|
        retnode.setAttributeNode(attr.cloneNode(true))
      }
      if deep
        @childNodes.nodelist.each {|node|
          retnode.appendChild(node.cloneNode(true))
        }
      end
      retnode
    end
    
    attr_reader :namespaceURI
    
    def prefix
      @nodeName=~/:/
      $`
    end
    
    def prefix=(val)
      if @nodeName=~/:/
        @nodeName=val+":"+$'
      else
        @nodeName=val+":"+@nodeName
      end
    end
    
    def localName
      @nodeName=~/:/
      $' || @nodeName
    end
    
    #DOM
    def nodeType
      ELEMENT_Node
    end
    
    def tagName
      @nodeName
    end
    
    def getAttribute(name)
      if attr = @attributes.getNamedItem(name)
        attr.nodeValue
      else
        ""
      end
    end
    
    def setAttribute(name, value)
      if attr=@attributes.getNamedItem(name)
        attr.value = value
      else
        @attributes.setNamedItem(Attr.new(@ownerDocument,name,value))
      end
    end
    
    def removeAttribute(name)
      @attributes.removeNamedItem(name)
    end
    
    def getAttributeNode(name)
      @attrbitues.getNamedItem(name) or ""
    end
    
    def setAttributeNode(newAttr)
      attr=@attributes.getNamedItem(newAttr.nodeName)
      @attributes.setNamedItem(newAttr)
      attr
    end
    
    def removeAttributeNode(oldAttr)
      name=oldAttr.name
      if oldAttr.id==@attributes.getNamedItem(name).id
        @attributes.removeNamedItem(name)
      else
        raise DOMException.new(NOT_FOUND_ERR)
      end
    end
    
    def getElementsByTagName(tagname)
      retval = NodeList.new
      nodelist = retval.nodelist
      @childNodes.nodelist.each {|node|
        if node.nodeType==ELEMENT_Node
          if tagname=='*' or node.nodeName==tagname
            nodelist << node
            node.add_observer(retval)
          end
          node._getElementsByTagName(tagname).each {|node|
            nodelist << node
            node.add_observer(retval)
          }
        end
      }
      retval
    end
    
    def _getElementsByTagName(tagname)
      nodelist = []
      @childNodes.nodelist.each {|node|
        if node.nodeType==ELEMENT_Node
          if tagname=='*' or node.nodeName==tagname
            nodelist << node
          end
          nodelist.concat node._getElementsByTagName(tagname)
        end
      }
      nodelist
    end
    
    def getAttributeNS(namespaceURI, localName)
      raise "Not Implemented"
    end
    
    def setAttributeNS(namespaceURI, qualifiedName, value)
      localName = MWDOM::localName(qualifiedName)
      if attr=@attributes.getNamedItemNS(namespaceURI,localName)
        attr.value = value
      else
        @attributes.setNamedItemNS(Attr.new(@ownerDocument,qualifiedName,value,namespaceURI))
      end
    end
    
    def removeAttributeNS(namespaceURI, localName)
      raise "Not Implemented"
    end
    
    def getAttributeNodeNS(namespaceURI, localName)
      raise "Not Implemented"
    end
    
    def setAttributeNodeNS(newAttr)
      raise "Not Implemented"
    end
    
    def getElementsByTagNameNS(namespaceURI, localName)
      raise "Not Implemented"
    end
    
    def hasAttribute(name)
      raise "Not Implemented"
    end
    
    def hasAttributeNS(namespaceURI, localName)
      raise "Not Implemented"
    end
  end



  class CharacterData
    include Node
    def initialize(doc,data)
      super(doc)
      @nodeValue = data
    end
    
    #DOM Node
    def cloneNode(deep)
      self.type.new(@ownerDocument,@nodeValue.dup)
    end
    
    #DOM
    def data
      @nodeValue
    end
    
    def data=(val)
      @nodeValue=val
    end
    
    def length
      @nodeValue.length
    end
    
    def substringData(offset, count)
      if offset>=0 and offset<@nodeValue.length and count>=0
        return @nodeValue[offset,count]
      end
      raise DOMException.new(INDEX_SIZE_ERR)
    end
    
    def appendData(arg)
      @nodeValue << arg
    end
    
    def insertData(offset, arg)
      if offset>=0 and offset<@nodeValue.length
        @nodeValue[offset,0]=arg
        return
      end
      raise DOMException.new(INDEX_SIZE_ERR)
    end
    
    def deleteData(offset, count)
      if offset>=0 and offset<@nodeValue.length and count>=0
        @nodeValue[offset,count]=''
        return
      end
      raise DOMException.new(INDEX_SIZE_ERR)
    end
    
    def replaceData(offset, count, arg)
      if offset>=0 and offset<@nodeValue.length and count>=0
        @nodeValue[offset,count]=arg
        return
      end
      raise DOMException.new(INDEX_SIZE_ERR)
    end
  end



  class Text < CharacterData
    def to_s
      MWDOM::escape(@nodeValue)
    end
    
    #DOM Node
    def nodeName
      "#text"
    end
    
    #DOM
    def nodeType
      TEXT_Node
    end
    
    def splitText(offset)
      raise "Not Implemented"
    end
  end



  class Comment < CharacterData
    def to_s
      "<!--" + MWDOM::escape(@nodeValue) + "-->"
    end
    
    #DOM Node
    def nodeName
      "#comment"
    end
    
    #DOM
    def nodeType
      COMMENT_Node
    end
    
  end



  class CDATASection < Text
    def to_s
      "<![CDATA[" + @nodeValue + "]]>"
    end
    
    #DOM Node
    def nodeName
      "#cdata-section"
    end
    
    #DOM
    def nodeType
      CDATA_SECTION_Node
    end
    
  end



  class DocumentType
    include Node
    def initialize(qualifiedName, publicId, systemId)
      super(nil)
      @nodeName = qualifiedName
      @publicId = publicId
      @systemId = systemId
    end
    
    def to_s
      s = "<!DOCTYPE #{@nodeName}"
      if @publicId
        s << " PUBLIC \"#{@publicId}\""
        s << " \"#{@systemId}\"" if @systemId
      elsif @systemId
        s << " SYSTEM \"#{@systemId}\""
      end
      s+">"
    end
    
    
    #DOM Node
    def cloneNode(deep)
      self.type.new(@nodeName,@publicId,@systemId)
    end
    
    #DOM
    def nodeType
      DOCUMENT_TYPE_Node
    end
    
    def name
      @nodeName
    end
    
    def entities
      raise "Not Implemented"
    end
    
    def notations
      raise "Not Implemented"
    end
    
    attr_reader :publicId
    attr_reader :systemId
    
    def internalSubset
      raise "Not Implemented"
    end
  end



  class Notation
    include Node
    
    #DOM
    def nodeType
      NOTATION_Node
    end
    
    def publicId
      raise "Not Implemented"
    end
    
    def systemId
      raise "Not Implemented"
    end
  end



  class Entity
    include Node
    
    #DOM
    def nodeType
      ENTITY_Node
    end
    
    def publicId
      raise "Not Implemented"
    end
    
    def systemId
      raise "Not Implemented"
    end
    
    def notationName
      raise "Not Implemented"
    end
  end



  class EntityReference
    include Node
    def initialize(doc,name)
      super(doc)
      @nodeName = name
    end
    
    def to_s
      "&#{@nodeName};"
    end
    
    
    #DOM Node
    def cloneNode(deep)
      self.type.new(@ownerDocument,@nodeName.dup)
    end
    
    #DOM
    def nodeType
      ENTITY_REFERENCE_Node
    end
    
  end



  class ProcessingInstruction
    include Node
    def initialize(doc,target,data)
      super(doc)
      @nodeName = target
      @nodeValue = data
    end
    
    def to_s
      "<?#{@nodeName} #{@nodeValue}?>"
    end
    
    
    #DOM Node
    def cloneNode(deep)
      self.type.new(@ownerDocument,@nodeName,@nodeValue)
    end
    
    #DOM
    def nodeType
      PROCESSING_INSTRUCTION_Node
    end
    
    def target
      @nodeName
    end
    
    def data
      @nodeValue
    end
    
    def data=(val)
      @nodeValue = val
    end
  end



  class DocumentFragment
    include Node
    def initialize()
      super(nil)
      @childNodes = NodeList.new
    end
    
    def to_s
      @childNodes.to_s
    end
    
    
    #DOM Node
    def nodeName
      "#document-fragment"
    end
    
    def cloneNode(deep)
      retnode = self.type.new()
      if deep
        @childNodes.nodelist.each {|node|
          retnode.appendChild(node.cloneNode(true))
        }
      end
      retnode
    end
    
    #DOM
    def nodeType
      DOCUMENT_FRAGMENT_Node
    end
    
  end
end
