require 'rexml/document'

module REXML
	# = Using the Pull Parser
	# <em>This API is experimental, and subject to change.</em>
	#  parser = PullParser.new( "<a>text<b att='val'/>txet</a>" )
	#  while parser.has_next?
	#    res = parser.next
	#    puts res[1]['att'] if res.start_tag? and res[0] == 'b'
	#  end
	# See the PullEvent class for information on the content of the results.
	# The data is identical to the arguments passed for the various events to
	# the StreamListener API.
	#
	# Notice that:
	#  parser = PullParser.new( "<a>BAD DOCUMENT" )
	#  while parser.has_next?
	#    res = parser.next
	#    raise res[1] if res.error?
	#  end
	class PullParser
		def initialize stream
			if stream.kind_of? String
				@source = Source.new stream
			elsif stream.kind_of? IO
				@source = IOSource.new stream
			elsif stream.kind_of? Source
				@source = stream
			else
				raise "#{stream.type} is not a valid input stream.  It must be \n"+
				"either a String, IO, or Source."
			end
			listener = PullListener.new(self)
			@main = Thread.current
			@thread = Thread.new {
				begin
					Document::parse_stream( @source, listener )
				rescue REXML::ParseException
					@value = PullEvent.new( :error, $!.message )
				end
			}
			Thread.pass
		end

		# Returns true if there are no more events
		def empty?
			Thread.pass while @thread.status == "run"
			return false if @value and @value.error?
			!@thread.alive?
		end

		# Returns true if there are more events.  Synonymous with !empty?
		def has_next?
			!empty?
		end

		# Returns the next event.  This is a +PullEvent+ object.
		def next
			Thread.pass while @thread.status == "run"
			value = @value
			@value = nil
			@thread.wakeup if @thread.alive?
			value
		end

		# Pushes an event onto the queue.  You don't need to use this.
		def push value
			@value = value
			Thread.stop
		end
	end

	# A parsing event.  The contents of the event are accessed as an +Array?,
	# and the type is given either by the ...? methods, or by accessing the
	# +type+ accessor.  The contents of this object vary from event to event,
	# but are identical to the arguments passed to +StreamListener+s for each
	# event.
	class PullEvent < Array
		# The type of this event.  Will be one of :tag_start, :tag_end, :text,
		# :instruction, :comment, :doctype, :attlistdecl, :entitydecl,
		# :notationdecl, :entity, :cdata, :xmldecl, or :error.
		attr_reader :type
		def initialize(*args)
			concat args[1..-1]
			@type = args[0]
		end
		# Content: [ String tag_name, Hash attributes ]
		def start_tag?
			@type == :tag_start
		end
		# Content: [ String tag_name ]
		def end_tag?
			@type == :end_tag
		end
		# Content: [ String text ]
		def text?
			@type == :text
		end
		# Content: [ String text ]
		def instruction?
			@type == :instruction
		end
		# Content: [ String text ]
		def comment?
			@type == :comment
		end
		# Content: [ String name, String pub_sys, String long_name, String uri ]
		def doctype?
			@type == :doctype
		end
		# Content: [ String text ]
		def attlistdecl?
			@type == :attlistdecl
		end
		# Content: [ String text ]
		def elementdecl?
			@type == :elementdecl
		end
		# Content: [ String text ]
		def entitydecl?
			@type == :entitydecl
		end
		# Content: [ String text ]
		def notationdecl?
			@type == :notationdecl
		end
		# Content: [ String text ]
		def entity?
			@type == :entity
		end
		# Content: [ String text ]
		def cdata?
			@type == :cdata
		end
		# Content: [ String version, String encoding, String standalone ]
		def xmldecl?
			@type == :xmldecl
		end
		def error?
			@type == :error
		end
	end

	private
	class PullListener
		def initialize main
			@main = main
		end
		def tag_start name, attributes
			attrs = {}
			attributes.each{|v| attrs[v[0]]=v[1]}
			@main.push PullEvent.new( :tag_start, name, attrs )
		end
		def tag_end name
			@main.push PullEvent.new( :tag_end, name )
		end
		def text text
			@main.push PullEvent.new( :text, text )
		end
		def instruction name, instruction
			@main.push PullEvent.new( :instruction, name, instruction )
		end
		def comment comment
			@main.push PullEvent.new( :comment, comment )
		end
		def doctype name, pub_sys, long_name, uri
			@main.push PullEvent.new( :doctype, name, pub_sys, long_name, uri )
		end
		def attlistdecl content
			@main.push PullEvent.new( :attlistdecl, content )
		end
		def elementdecl content
			@main.push PullEvent.new( :elementdecl, content )
		end
		def entitydecl content
			@main.push PullEvent.new( :entitydecl, content )
		end
		def notationdecl content
			@main.push PullEvent.new( :notationdecl, content )
		end
		def entity content
			@main.push PullEvent.new( :entity, content )
		end
		def cdata content
			@main.push PullEvent.new( :cdata, content )
		end
		def xmldecl version, encoding, standalone
			@main.push PullEvent.new( :xmldecl, version, encoding, standalone )
		end
	end
end
