require "rexml/parent"
require "rexml/namespace"
require "rexml/attribute"
require "rexml/cdata"
require "rexml/xpath"
require "rexml/parseexception"

module REXML
	# Represents a tagged XML element.  Elements are characterized by
	# having children, attributes, and names, and can themselves be
	# children.
	class Element < Parent
		include Namespace

		UNDEFINED = "UNDEFINED";		# The default name

		# Mechanisms for accessing attributes and child elements of this
		# element.
		attr_reader :attributes, :elements
		attr_accessor :context

		# Constructor
		# @param parent if supplied, must be a Parent, and will be used as
		# the parent of this object.
		# @param arg if not supplied, will be set to the default value.
		# If a String, the name of this object will be set to the argument.
		# If an Element, the object will be shallowly cloned; name, attributes,
		# and namespaces will be copied.
		# If a Source, the source will be scanned and parsed for an Element,
		# and all child elements will be recursively parsed as well.
		def initialize( arg = UNDEFINED, parent=nil, context=nil )
			super(parent)

			@elements = Elements.new self
			@attributes = Attributes.new self
			@context = context

			if arg.kind_of? Source
				parse arg
			elsif arg.kind_of? String
				self.name = arg
			elsif arg.kind_of? Element
				self.name = arg.expanded_name
				arg.attributes.each_attribute{ |attribute|
					@attributes << Attribute.new( attribute )
				}
				@context = arg.context
			end
		end

		def clone
			Element.new self
		end

		# @return the root of the document that this element belongs to.
		# If this element doesn't belong to a document, but does belong to
		# another Element, the parent's root will be returned, until the
		# earliest ancestor is found.
		def root
			parent.nil? ? self : parent.root
		end

		def whitespace
			@whitespace = (@context and @context[:respect_whitespace] and
				(@context[:respect_whitespace] == :all or
				 @context[:respect_whitespace].include? expanded_name)
			)
			@whitespace
		end

		def raw
			@raw = (@context and @context[:raw] and
			(@context[:raw] == :all or
			@context[:raw].include? expanded_name))
			@raw
		end

		once :whitespace, :raw

		#################################################
		# Namespaces                                    #
		#################################################

		# @return an Array of all defined namespaces
		def prefixes
			prefixes = []
			prefixes = parent.prefixes if parent
			prefixes += attributes.prefixes
			return prefixes
		end

		# @return the URI for a namespace, or the empty string if no such 
		# namespace is declared
		def namespace prefix=nil
			if prefix.nil?
				prefix = prefix()
			end
			prefix = "xmlns" if prefix == ""
			ns = attributes[ prefix ]
			ns = parent.namespace(prefix) if ns.nil? and parent
			return ns
		end

		# Adds a namespace to this element
		# @param prefix the namespace.  If the second argument is not supplied,
		# this should be the URI of the default namespace.
		# @param value (optional) the URI of the namespace.  If "", then unsets
		# the default namespace
		def add_namespace( prefix, uri=nil )
			unless uri
				@attributes["xmlns"] = prefix
			else
				@attributes[prefix, "xmlns"] = uri
			end
		end

		# NEEDS DOCUMENTATION
		def delete_namespace namespace="xmlns"
			attribute = attributes[name, namespace]
			attribute.remove unless attribute.nil?
		end

		#################################################
		# Elements                                      #
		#################################################

		# Adds a child to this element, optionally setting attributes in
		# the element.
		# @param element optional.  If Element, the element is added.
		# Otherwise, a new Element is constructed with the argument (see
		# Element.initialize).
		# @param attrs If supplied, must be a hash of String name,value 
		# pairs, which will be used to set the attributes of the new Element.
		# @return the Element that was added
		def add_element element=nil, attrs=nil
			el = @elements.add element
			attrs.each_pair do |key, value|
				el.attributes[key]=value
			end if attrs.kind_of? Hash
			el
		end

		# Deletes a child element.
		# @param element Must be an Element, String, or Integer.  If Element, 
		# the element is removed.  If String, the element is found (via XPath) 
		# and removed.  NOTE that this means that any parent can remove any
		# descendant.  If Integer, the Element indexed by that number will be
		# removed.
		# @return the element that was removed.
		def delete_element element
			@elements.delete element
		end

		# @return true if this element has at least one child Element
		def has_elements?
			!@elements.empty?
		end

		# Iterates through the children, yielding for each Element that
		# has a particular attribute set.
		# @param key the name of the attribute to search for
		# @param value the value of the attribute
		# @param max (optional) causes this method to return after yielding
		# for this number of matching children
		# @param name (optional) if supplied, this is an XPath that filters
		# the children to check.
		def each_element_with_attribute( key, value, max=0, name=nil, &block )
			each_with_something( proc {|child| 
				child.attributes[key]==value
			}, max, name, &block )
		end

		# Iterates through the children, yielding for each Element that
		# has a particular text set.
		# @param key the name of the attribute to search for
		# @param value the value of the attribute
		# @param max (optional) causes this method to return after yielding
		# for this number of matching children
		# @param name (optional) if supplied, this is an XPath that filters
		# the children to check.
		# @see text
		def each_element_with_text( text, max=0, name=nil, &block )
			each_with_something( proc {|child| child.text == text}, 
			max, name, &block )
		end

		# Synonym for elements.each
		def each_element( xpath=nil, &block )
			@elements.each( xpath, &block )
		end

		# This is a little slower than calling elements.each directly.
		# @return an array of Elements that match the supplied path
		# @param xpath any XPath by which to search for elements in the tree
		def get_elements( xpath )
			rv = []
			@elements.each( xpath ) {|child| rv<<child }
			rv
		end

		# Returns the next sibling that is an element, or nil if there is
		# no Element sibling after this one
		def next_element
			element = next_sibling
			element = element.next_sibling until element.nil? or element.kind_of? Element 
			return element
		end

		# Returns the previous sibling that is an element, or nil if there is
		# no Element sibling prior to this one
		def previous_element
			element = previous_sibling
			element = element.previous_sibling until element.nil? or element.kind_of? Element
			return element
		end


		#################################################
		# Text                                          #
		#################################################

		# @return true if this element has at least one Text child
		def has_text?
			not text().nil?
		end

		# A convenience method which returns the first child text element.
		# NOTE that an element may have multiple Text elements, perhaps
		# separated by other children; consider:
		# "<p>some text <b>this is bold!</b> more text<p>"
		# The element <p> has two text elements, "some text " and " more text".
		# This method would return only the first, "some text ".
		# @return the String content of the first child text element 
		# encountered.  
		def text( path = nil )
			rv = get_text path
			return rv.to_s unless rv.nil?
			nil
		end

		# This is the same method as text(), only this method returns the
		# actual Text object, rather than the String content.
		# @return the first Text child encountered.
		def get_text path = nil
			rv = nil
			if path
				element = @elements[ path ]
				rv = element.get_text unless element.nil?
			else
				rv = find { |node| node.kind_of? Text }
			end
			return rv
		end

		# Sets the first Text child of this object.  See text() for a
		# discussion about Text children.
		# If a Text child already exists, the child is replaced by this
		# content.  This means that Text content can be deleted by calling
		# this method with a nil argument.  In this case, the next Text
		# child becomes the first Text child.  In no case is the order of
		# any siblings disturbed.
		# @param text If a String, a new Text child is created and added to
		# this Element as the first Text child.  If Text, the text is set
		# as the first Child element.  If nil, then any existing first Text
		# child is removed.
		# @return self.  NOTE that contrary to most REXML methods, the replaced
		# content is not returned.
		def text=( text )
			text = Text.new( text, whitespace(), raw() ) if text.kind_of? String
			old_text = get_text
			if text.nil?
				old_text.remove unless old_text.nil?
			else
				if old_text.nil?
					self << text
				else
					old_text.replace_with( text )
				end
			end
			return self
		end

		# A helper method to add a Text child.  Actual Text instances can
		# be added with regular Parent methods, such as add() and <<()
		# @param text if a String, a new Text instance is created and added
		# to the parent.  If Text, the object is added directly.
		# @return self.  NOTE that contrary to most REXML methods, the object
		# added to the parent is not returned.
		def add_text( text )
			text = Text.new( text, whitespace(), raw() ) if text.kind_of? String
			self << text unless text.nil?
			return self
		end

		#################################################
		# Attributes                                    #
		#################################################

		# @return true if this element has any attributes set, false
		# otherwise.
		def has_attributes?
			return !@attributes.empty?
		end

		# Adds an attribute to this element, overwriting any existing attribute
		# by the same name.
		# @param key can be either an Attribute or a String.  If an Attribute,
		# the attribute is added to the list of Element attributes.  If String,
		# the argument is used as the name of the new attribute, and the value
		# parameter must be supplied.
		# @param value not required, and is ignored if the first argument is
		# an Attribute.  Otherwise, this is a String, and is used as the value
		# of the new Attribute.
		# @return the Attribute added
		def add_attribute( key, value=nil )
			if key.kind_of? Attribute
				@attributes << key
			else
				@attributes[key] = value
			end
		end

		# Add multiple attributes to this element.  EG:
		# el.add_attributes( {"name1"=>"value1", "name2"=>"value2"} )
		# el.add_attributes( [ ["name1","value1"], ["name2"=>"value2"] ] )
		# @p hash is either a hash, or array of arrays
		def add_attributes hash
			if hash.kind_of? Hash
				hash.each_pair {|key, value| @attributes[key] = value }
			elsif hash.kind_of? Array
				hash.each { |value| @attributes[ value[0] ] = value[1] }
			end
		end

		# Removes an attribute
		# @param key either an Attribute or a String.  In either case, the
		# attribute is found by matching the attribute name to the argument,
		# and then removed.  If no attribute is found, no action is taken.
		# @return the attribute removed, or nil if this Element did not contain
		# a matching attribute
		def delete_attribute(key)
			attr = @attributes.get_attribute(key)
			attr.remove unless attr.nil?
		end

		# Writes out this element, and recursively, all children.
		# @param writer A String or IO (or any object supporting <<(String))
		# @param indent if supplied, must be an Integer, which is then used
		# to determine the indentation of this element and its children.
		def write(writer, indent=0)
			indent writer, indent
			writer << "<#@expanded_name"

			@attributes.each_attribute do |attr|
				writer << " "
				attr.write( writer, indent )
			end unless @attributes.empty?

			if @children.empty?
				writer << "/" 
			else
				writer << ">"
				write_children writer, indent
				writer << "</#{expanded_name}"
			end
			writer << ">"
		end

		def Element.parse_stream source, listener
			begin
				# Get the next tag
				name, closed, attrs = base_parser(source)
				attrs.each { |ar| ar[1,1] = [] }
				listener.tag_start name, attrs
				unless closed
					while true
						md = source.match(/\A(\s*[^>]*>)/um)
						raise ParseException.new("malformed XML: bad content", source, 
						self) unless md
						line = md[1]
						return if line.nil?
						case line
						when /\A<\w/um
							Element.parse_stream source, listener
						when /\A<\//um
							break
						when CData::START_RE
							CData.parse_stream source, listener
						when Comment::START_RE
							Comment.parse_stream source, listener
						when Instruction::START_RE
							Instruction.parse_stream source, listener
						else
							Text.parse_stream source, listener
						end
					end
					if source.match(/^\s*<\/#{name}\s*>/um, true)
						listener.tag_end name
					else
						raise ParseException.new( "Missing end tag for #{name}", 
						source, self )
					end
				else
					listener.tag_end name
				end
			rescue ParseException
				$!.source = source
				$!.element = self
				raise
			rescue Exception
				raise
				old_ex = $!
				raise ParseException.new("unidentified error", source, self, old_ex)
			end
		end

		private
		# A private helper method
		def each_with_something( test, max=0, name=nil )
			num = 0
			child=nil
			@elements.each( name ){ |child|
				yield child if test.call(child) and num += 1
				return if max>0 and num == max
			}
		end

		# A helper method, to consolidate the common code between parse and
		# parse_stream.
		# @return an array with [tag_name_S, closed_B, attributes_A]
		def Element.base_parser source
			# Get the next tag
			md = source.match(/^<((?:[:\w_][-\.\w_]*:)?[-!\*\.\w_]*)\s*((((["']).*?\5)|[^\/'">]*)*?)(\/)?>/um, true)
			raise ParseException.new("malformed XML: missing tag start", source,
				self) unless md
				attrs = []
				if md[2]
					attrs = md[2].scan( Attribute::PATTERN )
					raise ParseException.new( "error parsing attributes: [#{attrs.join ', '}], excess = \"#$'\"", source, self ) if $' and $'.strip.size > 0
				end
				return [md[1], md[6], attrs]
			end

			def parse source
				begin
					# Get the next tag
					self.name, closed, attrs = Element.base_parser(source)
					attrs.each do |set|
						@attributes.add( Attribute.new( set[0], set[2] ) )
					end

					unless closed
						parse_children source
						raise ParseException.new( "Missing end tag for #{expanded_name}",
							source, self) unless source.match(/^\s*<\/#@expanded_name\s*>/um, 
							true)
						end
					rescue ParseException
						$!.source = source
						$!.element = self
						raise
					rescue Exception
						old_ex = $!
						raise ParseException.new("unidentified error", source, self, old_ex)
					end
				end

				# A private helper method
				def parse_children source
					while true
						md = source.match(/\A(\s*[^>]*>)/um)
						raise ParseException.new( "missing tag start", source, self ) unless md
						case md[1]
						when nil
							return
						when /\A<[\w_:]/um
							Element.new(source, self, @context)
						when /\A<\//um
							return
						when CData::START_RE
							add(CData.new(source))
						when Comment::START_RE
							add(Comment.new(source))
						when Instruction::START_RE
							add(Instruction.new(source))
						else
							add(Text.new(source, whitespace(), raw()))
						end
					end
				end

				# A private helper method
				def write_children( writer, indent )
					cr = indent < 0 ? "" : "\n"
					#if size == 1 and @children[0].kind_of?(Text)
					#	self[0].write( writer, -1 )
					if indent == -1
						each { |child| child.write( writer, indent ) }
					else
						next_indent = indent+2
						each { |child|
							writer << cr
							child.write( writer, next_indent )
						}
						writer << cr
						indent( writer, indent )
					end
				end
			end

			########################################################################
			# ELEMENTS                                                             #
			########################################################################

			# A class which provides filtering of children for Elements, and
			# XPath search support.
	class Elements
		include Enumerable
		# Constructor
		# @param parent the parent Element
		def initialize parent
			@element = parent
		end

		# Fetches a child element
		# @param index the search parameter.  This is either an Integer, which
		# will be used to find the index'th child Element, or an XPath,
		# which will be used to search for the Element.  NOTE that because
		# of the nature of XPath searches, any element in the connected XML
		# document can be fetched through any other element.
		# @param name optional, and only used in the first argument is an
		# Integer.  In that case, the index'th child Element that has the
		# supplied name will be returned.  Note that the indexes start at 1.
		# @return the first matching Element, or nil if no child matched
		def []( index, name=nil)
			if index.kind_of? Integer
				raise "index (#{index}) must be >= 1" if index < 1
				literal, name = literalize name
				num = 0
				child = nil
				@element.find { |child|
					child.kind_of? Element and
					(name.nil? ? true : child.has_name?( name, literal )) and 
					(num += 1) == index
				}
			else
				XPath::each(@element, index ) { |element| return element }
				return nil
			end
		end

		# Sets an element, replacing any previous matching element.  If no
		# existing element is found ,the element is added.
		# @param index is used to find a matching element to replace.  See []().
		# @param element the element to replace the existing element with
		# @return the previous element; nil if no previous element was found.
		def []=( index, element )
			previous = self[index]
			if previous.nil?
				@element.add element
			else
				previous.replace_with element
			end
			return previous
		end

		# @return true if there are no Element children, false otherwise
		def empty?
			@element.find{ |child| child.kind_of? Element}.nil?
		end

		# @return the index of the supplied child (starting at 1), or -1 if 
		# the element is not a child
		# @param element a child of this Element
		def index element
			rv = 0
			found = @element.find do |child| 
				child.kind_of? Element and
				(rv += 1) and
				child == element
			end
			return rv if found == element
			return -1
		end

		# Deletes a child Element
		# @param element Either an Element, which is removed directly; an
		# xpath, where the first matching child is removed; or an Integer,
		# where the n'th Element is removed.
		# @return the removed child
		def delete element
			if element.kind_of? Element
				@element.delete element
			else
				el = self[element]
				el.remove if el
			end
		end

		# Removes multiple elements
		# @param xpath all elements matching this String path are removed.
		# @return an Array of Elements that have been removed
		def delete_all( xpath )
			rv = []
			XPath::each( @element, xpath) {|element| rv << element }
			rv.each do |element|
				@element.delete element
				element.remove
			end
			return rv
		end

		# Adds an element
		# @param element if supplied, is either an Element, String, or
		# Source (see Element.initialize).  If not supplied or nil, a
		# new, default Element will be constructed
		# @return the added Element
		def add element=nil
			if element.nil?
				Element.new "", self, @element.context
			elsif not element.kind_of?(Element)
				Element.new element, self, @element.context
			else
				@element << element
				element.context = @element.context
			end
		end

		alias :<< :add

		# Iterates through all of the child Elements, optionally filtering
		# them by a given XPath
		# Only supply the first parameter (and a block).  The second
		# parameter is used internally for recursion.
		# @param xpath optional.  If supplied, this is a String XPath,
		# and is used to filter the children, so that only matching children
		# are yielded
		def each( xpath=nil, &block)
			XPath::each( @element, xpath, &block )
		end

		def size
			count = 0
			@element.each {|child| count+=1 if child.kind_of? Element }
			count
		end

		def to_a( xpath=nil )
			rv = []
			each( xpath ) { |el| rv << el }
			rv
		end

		private
		# A private helper method
		def literalize name
			literal = !(name =~ /^'.*'$/u).nil?
			name = name[1..-2] if literal and name
			[literal, name]
		end
	end

	########################################################################
	# ATTRIBUTES                                                           #
	########################################################################

	# A class that holds the Attributes of an Element.  
	class Attributes < Hash
		# Constructor
		# @param element the Element of which this is an Attribute
		def initialize element
			@element = element
		end

		# Fetches an attribute value.  If you want to get the Attribute itself,
		# use get_attribute()
		# @param name an XPath attribute name
		# @return A String, which is the value of the first matching attribute,
		# or nil if there was none
		def [](name)
			attr = get_attribute(name)
			return attr.value unless attr.nil?
			return nil
		end

		def each_attribute
			each_value do |val|
				if val.kind_of? Attribute
					yield val
				else
					val.each_value { |atr| yield atr }
				end
			end
		end

		# Fetches an attribute
		# @param name the XPath by which to search for the attribute
		# @return The first matching Attribute, or nil if there was none
		def get_attribute( name )
			attr = fetch( name, nil )
			if attr.nil?
				return nil if name.nil?
				# Look for prefix
				if name.include? ':'
					name =~ /(\w+):(\w+)/u
					prefix, name = $1, $2
					attr = fetch( name, nil )
					# check prefix
					return nil if attr == nil
					if attr.kind_of? Attribute
						return attr if uri == attr.prefix
					else
						attr = attr[ prefix ]
						return attr
					end
				end
				return nil
			end
			if attr.kind_of? Hash
				attr = attr[ @element.prefix ]
			end
			return attr
		end

		# Sets an attribute, overwriting any existing attribute value by the
		# same name
		# @param name the name of the attribute
		# @param value (optional) If supplied, the value of the attribute.  If
		# nil, any existing matching attribute is deleted.
		# @return self NOTE that unlike most REXML methods, this does not return
		# the set Attribute.
		def []=( name, value = nil )
			value = Attribute.new(name, value) unless value.kind_of? Attribute
			value.element = @element
			old_attr = get_attribute name
			if old_attr.nil?
				store(value.name, value)
			elsif old_attr.kind_of? Hash
				old_attr[value.prefix] = value
			elsif old_attr.prefix != value.prefix
				hash = { old_attr.prefix	=> old_attr,
							value.prefix		=> value }
							store value.name, hash
						else
							store value.name, value
						end
						return @element
					end

					def prefixes
						ns = []
						each_attribute do |attribute|
							ns << attribute.name if attribute.prefix == "xmlns"
						end
						ns
					end

					# Removes an attribute
					# @return the removed attribute
					def delete( attribute )
						attr = super
						attr.remove unless attr.nil?
						attribute
					end

					# Adds an attribute, overriding any existing attribute by the
					# same name.
		# @param attribute must be an Attribute
		def add( attribute )
			self[attribute.name] = attribute
		end

		alias :<< :add

		# DO NOT USE THIS METHOD!  It WILL fail, and may be removed.  If you
		# absolutely need this method, write the author.
		# Deletes all attributes matching an xpath
		# @param xpath A String; all attributes that match this path will
		# be removed
		# @return an Array of the Attributes that were removed
		def delete_all( xpath )
			rv = []
			each_attribute_pair(xpath) { |name,attribute| rv << attribute }
			rv.each{ |attr| attr.remove }
			return rv
		end

		private
		# Private helper class.
		def literalize name
			literal = !(name =~ /^'.*'$/u).nil?
			name = name[1..-2] if literal
			[literal, name]
		end
	end
end
