require "rexml/parent"

module REXML
	##
	# Represents an XML DOCTYPE declaration; that is, the contents of <!DOCTYPE
	# ... >.  DOCTYPES can be used to declare the DTD of a document, as well as
	# being used to declare entities used in the document.
	class DocType < Parent
		START = "<!DOCTYPE"
		START_RE = /\A\s*#{START}\s/m
		STOP = ">"
		STOP_RE = />/
		SYSTEM = "SYSTEM"
		PUBLIC = "PUBLIC"
		OPEN_RE = /\A\s*\[/
		PATTERN_RE = /\s*#{START}\s+(.*?)(\[|>)/m

		## name is the name of the doctype
		# external_id is the referenced DTD, if given
		attr_reader :name, :external_id

		##
		# Constructor
		# @param parent if given, is set as the parent of this object
		# @param first can be multiple types.  If String, name is set to this
		# and external_id is set to nil.  If DocType, the object is cloned.  If
		# Source, the source is scanned for the DOCTYPE declaration.
		def initialize( first, parent=nil )
			super(parent)
			if first.kind_of? String
				@name = first
				@external_id = nil
			elsif first.kind_of? DocType
				@name = first.name
				@external_id = first.external_id
			elsif first.kind_of? Source
				md = first.match( PATTERN_RE, true )
				identity = md[1]
				close = md[2]

				identity =~ /^(\w+)(\s+\w+)?(\s+["'].*?['"])?(\s+['"].*?["'])?/
				@name = $1

				raise "DOCTYPE is missing a name" if @name.nil?

				@pub_sys = $2.nil? ? nil : $2.strip
				@long_name = $3.nil? ? nil : $3.strip
				@uri = $4.nil? ? nil : $4.strip
				@external_id = nil

				case @pub_sys
				when "SYSTEM"
					@external_id = "SYSTEM"
				when "PUBLIC"
					@external_id = "PUBLIC"
				else
					# Done, or junk
				end
				# If these raise nil exceptions, then the doctype was malformed
				begin
					@external_id << " #@long_name" if @long_name
					@external_id << " #@uri" if @uri
				rescue
					raise "malformed DOCTYPE declaration #$&"
				end

				return if close == ">"
				parse_entities first
			end
		end

		def clone
			DocType.new self
		end

		def write( output, indent=0 )
			indent( output, indent )
			output << START
			output << ' '
			output << @name
			output << " #@external_id" unless @external_id.nil?
			unless @children.empty?
				#output << "\n"
				next_indent = indent + 2
				#output << '   '*next_indent
				output << ' ['
				child = nil		# speed
				@children.each { |child|
					output << "\n"
					child.write( output, next_indent )
				}
				output << "\n"
				#output << '   '*next_indent
				output << "]"
			end
			output << STOP
		end

		def DocType.parse_stream source, listener
			md = source.match( PATTERN_RE, true )
			identity = md[1]
			close = md[2]

			identity =~ /^(\w+)(\s+\w+)?(\s+["'].*?['"])?(\s+['"].*?["'])?/
			name = $1

			raise "DOCTYPE is missing a name" if name.nil?

			pub_sys = $2.nil? ? nil : $2.strip
			long_name = $3.nil? ? nil : $3.strip
			uri = $4.nil? ? nil : $4.strip

			listener.doctype name, pub_sys, long_name, uri
			return if close == ">"
			parse_entities_source source, listener
		end

		private
		def DocType.parser source
			md = source.match(/\s*(.*?)>/m)
			nxt = md[1]
			until md[1] == "]" 
				case md[1]
				when /^%/ #/
					md = source.match(/^\s*%(.*?);/m, true)
					yield md[1]
				when AttlistDecl::START_RE
					yield AttlistDecl
				when ElementDecl::START_RE
					yield ElementDecl
				when EntityDecl::START_RE
					yield EntityDecl
				when NotationDecl::START_RE
					yield NotationDecl
				when Comment::START_RE
					yield Comment
				when Instruction::START_RE
					yield Instruction
				else
					raise "illegal entry #{md[1]} in DOCTYPE"
				end
				md = source.match(/\s*(.*?)>/m)
			end
		end

		def DocType.parse_entities_source source, listener
			DocType.parser source do |arg|
				if arg.kind_of? String
					listener.entity arg
				else
					arg.parse_source source, listener
				end
			end
		end

		def parse_entities src
			DocType.parser src do |arg|
				if arg.kind_of? String
					add_entity_sub arg
				else
					self.add( arg.new(src) )
				end
			end
		end

		def add_entity_sub ent
		end
	end

	# We don't really handle any of these since we're not a validating
	# parser, so we can be pretty dumb about them.  All we need to be able
	# to do is spew them back out on a write()

	class Declaration < Child
		def initialize src
			super()
			md = src.match( pattern, true )
			@string = md[1]
		end

		def to_s
			@string
		end

		def write( output, indent )
			output << ('   '*indent) if indent > 0
			output << @string
		end

		def Declaration.parse_source source, listener
			md = src.match( pattern, true )
			listener.send inspect.downcase, md[1]
		end
	end
	
	class AttlistDecl < Declaration
		START = "<!ATTLIST"
		START_RE = /\s*#{START}/m
		PATTERN_RE = /\s*(#{START}.*?>)/m
		def pattern
			PATTERN_RE
		end
	end

	class ElementDecl < Declaration
		START = "<!ELEMENT"
		START_RE = /\s*#{START}/m
		PATTERN_RE = /^s*(#{START}.*?>)/m
		def pattern
			PATTERN_RE
		end
	end

	class EntityDecl < Declaration
		START = "<!ENTITY"
		START_RE = /^\s*#{START}/m
		PATTERN_RE = /^\s*(#{START}.*?>)/m
		def pattern
			PATTERN_RE
		end
	end

	class NotationDecl < Declaration
		START = "<!NOTATION"
		START_RE = /^\s*#{START}/m
		PATTERN_RE = /^\s*(#{START}.*?>)/m
		def pattern
			PATTERN_RE
		end
	end
end
