# RubyRDF / RDF Parser. Based on code contributed from
# RDF4R RDF Parser, Copyright © 2002 Brandt Kurowski (brandt@kurowski.net)
# packaged as part of RubyRDF, see http://www.w3.org/2001/12/rubyrdf/intro.html
# All Rights Reserved. This work is distributed under the W3C® Software 
# License [1] in the hope that it will be useful, but WITHOUT ANY 
# WARRANTY; without even the implied warranty of MERCHANTABILITY or 
# FITNESS FOR A PARTICULAR PURPOSE. 
# [1] http://www.w3.org/Consortium/Legal/2002/copyright-software-20021231




# this needs a new home
def xml_escape(str)
	return nil if str.nil?
	s = str.kind_of?(String) ? str.clone : str.to_s.clone
	s.gsub!('&', '&amp;')
	s.gsub!('<', '&lt;')
	s.gsub!('>', '&gt;')
	s.gsub!('"', '&quot;')
	return s
end

# this too
def compute_namespace(uri)
	if uri.relative?
		raise "what the fuck am i supposed to do i do with a relative uri?"
	end
	if uri.fragment
		localname = uri.fragment
		namespace = uri + '#'
	elsif uri.path
		namespace = uri + '.'
		localname = uri - namespace
	else
		raise "can't compute namespace for #{uri}"					
	end
	return namespace, localname
end

module RDF4R

	module Model

		class Literal

			@@id = 0

			attr_reader :model, :id
			attr_reader :text, :lang
			attr_reader :statements_for_which_i_am_object

			def initialize(model, text, lang = nil)
				@model = model
				@text = text
				@lang = lang
				@statements_for_which_i_am_object = []
				@id = @@id
				@@id += 1
			end

			def to_ntriple
        # this doesn't handle unicode properly, but then neither does ruby yet
				return @lang ? %Q{#{@text.inspect}-#{@lang}} : @text.inspect
			end

			def to_s
				@text
			end

# FIXME do we need this if we just include drb/eq?
			def ==(other)
				if other.kind_of?(Literal)
					return @id == other.id
				end
				return false
			end

			def ===(other)
				if other.kind_of?(Literal)
					return
					  @text == other.text and
						@lang == other.lang
				elsif other.kind_of?(Array)
					return 
					  @text == other[0] and
						@lang == other[1]
				else
					return false
				end
			end

		end
		
		class Resource

			@@id = 0

			attr_reader :model, :id
			attr_reader :uri
			attr_reader :statements_for_which_i_am_subject
			attr_reader :statements_for_which_i_am_predicate
			attr_reader :statements_for_which_i_am_object

			def initialize(model, uri = nil)
				@model = model
				@uri = uri
				@statements_for_which_i_am_subject = []
				@statements_for_which_i_am_predicate = []
				@statements_for_which_i_am_object = []
				@id = @@id
				@@id += 1
			end

			def to_ntriple
				return uri.nil? ? %Q{_:#{@id}} : %Q{<#{@uri}>}
			end

			def to_s
				return uri.nil? ? %Q{_:#{@id}} : %Q{#{@uri}}
			end

			def properties
				@model.statements.values.select {|s| s.subject == self}.map {|s| [s.predicate, s.object]}
			end

			def rdf_type
				@model.statements.values.select {|s| s.subject == self and s.predicate.uri == 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type'}.map {|s| s.object}
			end

			def ==(other)
				if other.kind_of?(Resource)
					return @id == other.id
				end
				return false
			end

			def ===(other)
				if other.kind_of?(Resource)
					return @id == other.id if @uri.nil? and other.uri.nil?
					return @uri == other.uri 
				elsif other.kind_of?(String)
					return @uri == other
				else
					return false
				end
			end

			def to_rdf_xml(out)
				ns = compute_namespaces
				pfx_by_ns = {}
				out << "<rdf:RDF"
				ns.each_with_index do |uri, n|
					pfx = (uri.to_s == RDF_NS) ? 'rdf' : %Q{ns#{n}}
					pfx_by_ns[uri.to_s] = pfx
					out << %Q{ xmlns:#{pfx}="#{xml_escape uri}"}
				end
				out.print ">\n"
				# FIXME verify rdf/xml-ability, as in Model#to_rdf_xml
				if uri.nil?
					out << %Q{  <rdf:Description>\n}
				else
					out << %Q{  <rdf:Description rdf:about="#{xml_escape uri}">\n}
				end
				inner_rdf_xml(out, pfx_by_ns, 2)
				out << %Q{  </rdf:Description>\n}
				out << "</rdf:RDF>\n"
				out.flush
			end

			def inner_rdf_xml(out, pfx_map, depth)
				@statements_for_which_i_am_subject.each do |s|
					pred_ns, pred_ln = compute_namespace(s.predicate.uri)
					pred_pfx = pfx_map[pred_ns.to_s]
					if s.object.kind_of? Resource
						if s.object.statements_for_which_i_am_subject.empty? or (s.object.statements_for_which_i_am_object.size > 1)
							if s.object.uri
								out << %Q{#{'  '*depth}<#{pred_pfx}:#{pred_ln} rdf:resource="#{xml_escape s.object.uri}" />\n}
							else
								raise "cannot serialize as rdf/xml" if s.object.uri.nil?
							end
						else
							if s.object.uri
								out << %Q{#{'  '*depth}<#{pred_pfx}:#{pred_ln} rdf:about="#{xml_escape s.object.uri}">\n}
							else
								out << %Q{#{'  '*depth}<#{pred_pfx}:#{pred_ln} rdf:parseType='Resource'>\n}
							end
							s.object.inner_rdf_xml(out, pfx_map, depth + 1)
							out << %Q{#{'  '*depth}</#{pred_pfx}:#{pred_ln}>\n}
						end
					elsif s.object.kind_of? Literal
						out << %Q{#{'  '*depth}<#{pred_pfx}:#{pred_ln}}
						if s.object.lang
							out << %Q{ xml:lang="#{xml_escape s.object.lang}"}
						end
						out << '>'
						out << xml_escape(s.object.text)
						out << %Q{</#{pred_pfx}:#{pred_ln}>\n}
					else
						raise "bad model"
					end
				end
				out.flush
			end

      # Note that the namespaces found here are simply
      # for the purpose of generating valid rdf/xml- they
      # have nothing to do with the namespaces used in the
      # xml document this model may have come from, nor do they
      # correspond to rdfs locations or any other convention
			def compute_namespaces
				found = {}
				found[RDF_NS] = URI::parse(RDF_NS)
				@statements_for_which_i_am_subject.each do |s|
					uri = s.predicate.uri
					if s.object.kind_of?(Resource) and !s.object.statements_for_which_i_am_subject.empty?# and (s.object.statements_for_which_i_am_object.size == 1)
						s.object.compute_namespaces.each {|ns| found[ns.to_s] = ns}
					end
					ns, ln = compute_namespace(uri)
					found[ns.to_s] = ns
				end
				return found.values
			end

		end

		class Statement

			@@id = 0

			attr_reader :model, :id
			attr_reader :subject, :predicate, :object

			def initialize(model, subject, predicate, object)
				@model = model
				unless subject.kind_of?(Resource)
					raise "subject must be a Resource"
				end
				@subject = subject
				unless predicate.kind_of?(Resource)
					raise "predicate must be a Resource"
				end
				@predicate = predicate
				unless object.kind_of?(Resource) or object.kind_of?(Literal)
					raise "object must be a Resource or a Literal, not a #{object.class.name}=#{object}"
				end
				@object = object
				subject.statements_for_which_i_am_subject << self
				predicate.statements_for_which_i_am_predicate << self
				object.statements_for_which_i_am_object << self
				@id = @@id
				@@id += 1
			end

			def ==(other)
				if other.kind_of?(Statement)
					return @id == other.id
				end
				return false
			end

			def ===(other)
				if other.kind_of?(Statement)
					return
					  @subject == other.subject and
						@predicate == other.predicate and
						@object == other.object
				elsif other.kind_of?(Array)
					return 
					  @subject == other[0] and
						@predicate == other[1] and
						@object == other[2]
				else
					return false
				end
			end

			def to_ntriple
				return %Q{#{@subject.to_ntriple} #{@predicate.to_ntriple} #{@object.to_ntriple} .}
			end

			def to_s
				return %Q{#{@subject} #{@predicate} #{@object}}
			end

			def debug_s
				return %Q{<#{@subject}> <#{@predicate}> <#{@object}>}
			end

		end

		class Model

			@@id = 0

			attr_reader :id
			attr_reader :resources, :literals, :statements
			attr_reader :paranoid
			attr_reader :resources_by_uri
			attr_accessor :xml_base_uri
			attr_reader :xml_ids

			def initialize(paranoid = false)
				@resources_by_uri = {}
				@literals_by_text = {}
				@resources = {}
				@literals = {}
				@statements = {}
				@paranoid = paranoid
				@id = @@id
				@@id += 1
				@xml_base_uri = URI.parse("urn:rdf4r/#{@id}/") # set a generic base
				@xml_ids = []
			end

			def resource(uri = nil)
				unless uri.nil?
					uri = URI::parse(uri) unless uri.kind_of? URI::Generic
					return @resources_by_uri[uri.to_s] if @resources_by_uri.has_key?(uri.to_s)
				end
				r = Resource.new(self, uri)
				@resources[r.id] = r
				@resources_by_uri[uri.to_s] = r
				return r
			end

			def literal(text, lang = nil)
				if @literals_by_text.has_key?(text)
					l = @literals_by_text[text]
					return l if l.lang == lang
				end
				l = Literal.new(self, text, lang)
				@literals[l.id] = l
				@literals_by_text[text] = l
				return l
			end

			def statement(subject, predicate, object)
				@statements.each_value {|s| return s if s === [subject, predicate, object]} if @paranoid
				s = Statement.new(self, subject, predicate, object)
				@statements[s.id] = s
				return s
			end

			def ==(other)
				if other.kind_of?(Model)
					return @id == other.id
				end
				return false
			end

			def +(other)
				result = Model.new(self.paranoid || other.paranoid)
				selfresourcemap = {}
				self.resources.each do |id, r| 
					selfresourcemap[id] = result.resource(r.uri)
				end
				otherresourcemap = {}
				other.resources.each do |id, r| 
					otherresourcemap[id] = result.resource(r.uri)
				end
				selfliteralmap = {}
				self.literals.each do |id, l| 
					selfliteralmap[id] = result.literal(l.text, l.lang)
				end
				otherliteralmap = {}
				other.literals.each do |id, l| 
					otherliteralmap[id] = result.literal(l.text, l.lang)
				end
				self.statements.each_value do |s| 
					subject = selfresourcemap[s.subject.id]
					predicate = selfresourcemap[s.predicate.id]
					object = s.object.kind_of?(Resource) ?
						selfresourcemap[s.object.id] : selfliteralmap[s.object.id]
					result.statement(subject, predicate, object)
				end
				otherstatementmap = {}
				other.statements.each_value do |s| 
					subject = otherresourcemap[s.subject.id]
					predicate = otherresourcemap[s.predicate.id]
					object = s.object.kind_of?(Resource) ?
						otherresourcemap[s.object.id] : otherliteralmap[s.object.id]
					result.statement(subject, predicate, object)
				end
# FIXME we should normalize the model here (dedup statements)
# or provide for manual normalization
				return result
			end

			# replace resources in self with resources from other
			def update(other)
				other.resources.each do |new_id, new_r|
					next if new_r.statements_for_which_i_am_subject.empty?
					new_r.uri or raise "can't update with no uri!"
					old_r = @resources_by_uri[new_r.uri.to_s]
					old_r.statements_for_which_i_am_subject.each do |s|
						@statements.delete(s.id)
						s.predicate.statements_for_which_i_am_predicate.delete(s)
						if s.predicate.statements_for_which_i_am_subject.empty? and
							 s.predicate.statements_for_which_i_am_predicate.empty? and
							 s.predicate.statements_for_which_i_am_object.empty?
							@resources.delete(s.predicate.id)
							unless s.predicate.uri.nil?
								@resources_by_uri.delete(s.predicate.uri.to_s)
							end
						end
						s.object.statements_for_which_i_am_object.delete(s)
						case s.object
						when Resource
							if s.object.statements_for_which_i_am_subject.empty? and
								 s.object.statements_for_which_i_am_predicate.empty? and
								 s.object.statements_for_which_i_am_object.empty?
								@resources.delete(s.object.id)
								unless s.object.uri.nil?
									@resources_by_uri.delete(s.object.uri.to_s)
								end
							end
						when Literal
							if s.object.statements_for_which_i_am_object.empty?
 								@literals.delete(s.object.id)
								unless s.object.text.nil?
									@literals_by_text.delete(s.object.text)
								end
							end
						end
					end
					old_r.statements_for_which_i_am_subject.clear
					new_r.statements_for_which_i_am_subject.each do |s|
						statement(
								old_r,
								resource(s.predicate.uri),
								s.object.kind_of?(Resource) ? 
									resource(s.object.uri) :
									literal(s.object.text, s.object.lang)
						)
					end
				end
			end

# XXX
# XXX this is where i stopped. make this work.
# XXX
			# add resources from other to self
			def create(other)
				other.resources.each do |new_id, new_r|
					next if new_r.statements_for_which_i_am_subject.empty?
					if new_r.uri
						u = new_r.uri
					else
						id = 0
						begin
							id += 1
							u = (@xml_base_uri + "##{id}")
						end while ((@xml_ids.include? id.to_s) or
						           (@resources_by_uri.has_key? u.to_s))
					end
					old_r = resource(u)
					new_r.statements_for_which_i_am_subject.each do |s|
						statement(
								old_r,
								resource(s.predicate.uri),
								s.object.kind_of?(Resource) ? 
									resource(s.object.uri) :
									literal(s.object.text, s.object.lang)
						)
					end
				end
			end

			def to_rdf_xml(out)
				ns = compute_namespaces
				pfx_by_ns = {}
				out << "<rdf:RDF"
				ns.each_with_index do |uri, n|
					pfx = (uri.to_s == RDF_NS) ? 'rdf' : %Q{ns#{n}}
					pfx_by_ns[uri.to_s] = pfx
					out << %Q{ xmlns:#{pfx}="#{xml_escape uri}"}
				end
				out.print ">\n"
				@resources.each_value do |r|
					next unless r.uri.nil?
					if r.statements_for_which_i_am_object.size > 1
						raise "this model cannot be serialized as rdf/xml, use ntriples instead"
					end
				end
				@resources.values.sort {|a,b| a.uri.to_s <=> b.uri.to_s}.each do |r|
					next if r.statements_for_which_i_am_subject.empty?
					next if (r.statements_for_which_i_am_object.size == 1)
					if r.uri.nil?
						out << %Q{  <rdf:Description>\n}
					else
						out << %Q{  <rdf:Description rdf:about="#{xml_escape r.uri}">\n}
					end
					r.inner_rdf_xml(out, pfx_by_ns, 2)
					out << %Q{  </rdf:Description>\n}
					out.flush
				end
				out << "</rdf:RDF>\n"
				out.flush
			end

      # Note that the namespaces found here are simply
      # for the purpose of generating valid rdf/xml- they
      # have nothing to do with the namespaces used in the
      # xml document this model may have come from, nor do they
      # correspond to rdfs locations or any other convention
			def compute_namespaces
				found = {}
				found[RDF_NS] = URI::parse(RDF_NS)
				@statements.each_value do |s|
					uri = s.predicate.uri
					ns, ln = compute_namespace(uri)
					found[ns.to_s] = ns
				end
				return found.values
			end

		end

	end

end

