# -*- coding: utf-8 -*-
"""
Management of vocabularies, terms, and their mapping to URI-s. The main class of this module (L{TermOrCurie}) is,
conceptually, part of the overall state of processing at a node (L{state.ExecutionContext}) but putting it into a separate
module makes it easider to maintain.

@summary: Management of vocabularies, terms, and their mapping to URI-s.
@requires: U{RDFLib package<http://rdflib.net>}
@organization: U{World Wide Web Consortium<http://www.w3.org>}
@author: U{Ivan Herman<a href="http://www.w3.org/People/Ivan/">}
@license: This software is available for use under the
U{W3C® SOFTWARE NOTICE AND LICENSE<href="http://www.w3.org/Consortium/Legal/2002/copyright-software-20021231">}

@var XHTML_PREFIX: prefix for the XHTML vocabulary URI (set to 'xhv')
@var XHTML_URI: URI prefix of the XHTML vocabulary
@var ncname: Regular expression object for NCNAME
@var termname: Regular expression object for a term
@var xml_application_media_type: Regular expression object for a general XML application media type
"""

"""
$Id: termorcurie.py,v 1.12 2013-10-16 11:48:54 ivan Exp $
$Date: 2013-10-16 11:48:54 $
"""

import re, sys
import xml.dom.minidom
import random

if sys.version_info[0] >= 3 :
	from urllib.parse import urlsplit
else :
	from urllib.parse import urlsplit


import rdflib
from rdflib	import URIRef
from rdflib	import Literal
from rdflib	import BNode
from rdflib	import Namespace
if rdflib.__version__ >= "3.0.0" :
	from rdflib	import Graph
	from rdflib	import RDF  as ns_rdf
	from rdflib	import RDFS as ns_rdfs
else :
	from rdflib.Graph	import Graph
	from rdflib.RDFS	import RDFSNS as ns_rdfs
	from rdflib.RDF		import RDFNS  as ns_rdf

from .options		import Options
from .utils 		import quote_URI, URIOpener
from .host 			import MediaTypes, HostLanguage, predefined_1_0_rel, warn_xmlns_usage
from .				import IncorrectPrefixDefinition, RDFA_VOCAB, UnresolvableReference, PrefixRedefinitionWarning
from .				import ns_rdfa

from . import err_redefining_URI_as_prefix
from . import err_xmlns_deprecated
from . import err_bnode_local_prefix
from . import err_col_local_prefix
from . import err_missing_URI_prefix
from . import err_invalid_prefix
from . import err_no_default_prefix
from . import err_prefix_and_xmlns
from . import err_non_ncname_prefix
from . import err_absolute_reference
from . import err_query_reference
from . import err_fragment_reference
from . import err_prefix_redefinition


# Regular expression object for NCNAME
ncname   = re.compile("^[A-Za-z][A-Za-z0-9._-]*$")

# Regular expression object for term name
termname = re.compile("^[A-Za-z]([A-Za-z0-9._-]|/)*$")

# Regular expression object for a general XML application media type
xml_application_media_type = re.compile("application/[a-zA-Z0-9]+\+xml")

XHTML_PREFIX = "xhv"
XHTML_URI    = "http://www.w3.org/1999/xhtml/vocab#"

#### Managing blank nodes for CURIE-s: mapping from local names to blank nodes.
_bnodes = {}
_empty_bnode = BNode()

####

class InitialContext :
	"""
	Get the initial context values. In most cases this class has an empty content, except for the
	top level (in case of RDFa 1.1). Each L{TermOrCurie} class has one instance of this class. It provides initial
	mappings for terms, namespace prefixes, etc, that the top level L{TermOrCurie} instance uses for its own initialization.

	@ivar terms: collection of all term mappings
	@type terms: dictionary
	@ivar ns: namespace mapping
	@type ns: dictionary
	@ivar vocabulary: default vocabulary
	@type vocabulary: string
	"""

	def __init__(self, state, top_level) :
		"""
		@param state: the state behind this term mapping
		@type state: L{state.ExecutionContext}
		@param top_level : whether this is the top node of the DOM tree (the only place where initial contexts are handled)
		@type top_level : boolean
		"""
		self.state = state

		# This is to store the local terms
		self.terms  = {}
		# This is to store the local Namespaces (a.k.a. prefixes)
		self.ns     = {}
		# Default vocabulary
		self.vocabulary = None

		if state.rdfa_version < "1.1" or top_level == False :
			return

		from .initialcontext	import initial_context    as context_data
		from .host 				import initial_contexts   as context_ids
		from .host				import default_vocabulary

		for id in context_ids[state.options.host_language] :
			# This gives the id of a initial context, valid for this media type:
			data = context_data[id]

			# Merge the context data with the overall definition
			if state.options.host_language in default_vocabulary :
				self.vocabulary = default_vocabulary[state.options.host_language]
			elif data.vocabulary != "" :
				self.vocabulary = data.vocabulary

			for key in data.terms :
				self.terms[key] = URIRef(data.terms[key])
			for key in data.ns :
				self.ns[key] = (Namespace(data.ns[key]),False)


##################################################################################################################

class TermOrCurie :
	"""
	Wrapper around vocabulary management, ie, mapping a term to a URI, as well as a CURIE to a URI. Each instance of this class belongs to a
	"state", instance of L{state.ExecutionContext}. Context definitions are managed at initialization time.

	(In fact, this class is, conceptually, part of the overall state at a node, and has been separated here for an
	easier maintenance.)

	The class takes care of the stack-like behavior of vocabulary items, ie, inheriting everything that is possible
	from the "parent". At initialization time, this works through the prefix definitions (i.e., C{@prefix} or C{@xmln:} attributes)
	and/or C{@vocab} attributes.

	@ivar state: State to which this instance belongs
	@type state: L{state.ExecutionContext}
	@ivar graph: The RDF Graph under generation
	@type graph: rdflib.Graph
	@ivar terms: mapping from terms to URI-s
	@type terms: dictionary
	@ivar ns: namespace declarations, ie, mapping from prefixes to URIs
	@type ns: dictionary
	@ivar default_curie_uri: URI for a default CURIE
	"""
	def __init__(self, state, graph, inherited_state) :
		"""Initialize the vocab bound to a specific state.
		@param state: the state to which this vocab instance belongs to
		@type state: L{state.ExecutionContext}
		@param graph: the RDF graph being worked on
		@type graph: rdflib.Graph
		@param inherited_state: the state inherited by the current state. 'None' if this is the top level state.
		@type inherited_state: L{state.ExecutionContext}
		"""
		def check_prefix(pr) :
			from . import uri_schemes
			if pr in uri_schemes :
				# The prefix being defined is a registered URI scheme, better avoid it...
				state.options.add_warning(err_redefining_URI_as_prefix % pr, node=state.node.nodeName)

		self.state	= state
		self.graph	= graph

		# --------------------------------------------------------------------------------
		# This is set to non-void only on the top level and in the case of 1.1
		default_vocab = InitialContext(self.state, inherited_state == None)

		# Set the default CURIE URI
		if inherited_state == None :
			# This is the top level...
			self.default_curie_uri = Namespace(XHTML_URI)
			# self.graph.bind(XHTML_PREFIX, self.default_curie_uri)
		else :
			self.default_curie_uri = inherited_state.term_or_curie.default_curie_uri

		# --------------------------------------------------------------------------------
		# Set the default term URI
		# This is a 1.1 feature, ie, should be ignored if the version is < 1.0
		if state.rdfa_version >= "1.1" :
			# that is the absolute default setup...
			if inherited_state == None :
				self.default_term_uri = None
			else :
				self.default_term_uri = inherited_state.term_or_curie.default_term_uri

			# see if the initial context has defined a default vocabulary:
			if default_vocab.vocabulary :
				self.default_term_uri = default_vocab.vocabulary

			# see if there is local vocab that would override previous settings
			# However, care should be taken with the vocab="" value that should not become a URI...
			# Indeed, this value is used to 'vipe out', ie, get back to the default vocabulary...
			if self.state.node.hasAttribute("vocab") and self.state.node.getAttribute("vocab") == "" :
				self.default_term_uri = default_vocab.vocabulary
			else :
				def_term_uri = self.state.getURI("vocab")
				if def_term_uri and def_term_uri != "" :
					self.default_term_uri = def_term_uri
					self.graph.add((URIRef(self.state.base),RDFA_VOCAB,URIRef(def_term_uri)))
		else :
			self.default_term_uri = None

		# --------------------------------------------------------------------------------
		# The simpler case: terms, adding those that have been defined by a possible initial context
		if inherited_state is None :
			# this is the vocabulary belonging to the top level of the tree!
			self.terms = {}
			if state.rdfa_version >= "1.1" :
				# Simply get the terms defined by the default vocabularies. There is no need for merging
				for key in default_vocab.terms :
					self.terms[key] = default_vocab.terms[key]
			else :
				# The terms are hardwired...
				for key in predefined_1_0_rel :
					self.terms[key] = URIRef(XHTML_URI + key)
		else :
			# just refer to the inherited terms
			self.terms = inherited_state.term_or_curie.terms

		#-----------------------------------------------------------------
		# the locally defined namespaces
		dict = {}
		# locally defined xmlns namespaces, necessary for correct XML Literal generation
		xmlns_dict = {}

		# Add the locally defined namespaces using the xmlns: syntax
		for i in range(0, state.node.attributes.length) :
			attr = state.node.attributes.item(i)
			if attr.name.find('xmlns:') == 0 :
				# yep, there is a namespace setting
				prefix = attr.localName
				if prefix != "" : # exclude the top level xmlns setting...
					if state.rdfa_version >= "1.1" and state.options.host_language in warn_xmlns_usage :
						state.options.add_warning(err_xmlns_deprecated % prefix, IncorrectPrefixDefinition, node=state.node.nodeName)
					if prefix == "_" :
						state.options.add_warning(err_bnode_local_prefix, IncorrectPrefixDefinition, node=state.node.nodeName)
					elif prefix.find(':') != -1 :
						state.options.add_warning(err_col_local_prefix % prefix, IncorrectPrefixDefinition, node=state.node.nodeName)
					else :
						# quote the URI, ie, convert special characters into %.. This is
						# true, for example, for spaces
						uri = quote_URI(attr.value, state.options)
						# create a new RDFLib Namespace entry
						ns = Namespace(uri)
						# Add an entry to the dictionary if not already there (priority is left to right!)
						if state.rdfa_version >= "1.1" :
							pr = prefix.lower()
						else :
							pr = prefix
						dict[pr]       = ns
						xmlns_dict[pr] = ns
						self.graph.bind(pr,ns)
						check_prefix(pr)

		# Add the locally defined namespaces using the @prefix syntax
		# this may override the definition @xmlns
		if state.rdfa_version >= "1.1" and state.node.hasAttribute("prefix") :
			pr = state.node.getAttribute("prefix")
			if pr != None :
				# separator character is whitespace
				pr_list = pr.strip().split()
				# range(0, len(pr_list), 2)
				for i in range(len(pr_list) - 2, -1, -2) :
					prefix = pr_list[i]
					# see if there is a URI at all
					if i == len(pr_list) - 1 :
						state.options.add_warning(err_missing_URI_prefix % (prefix,pr), node=state.node.nodeName)
						break
					else :
						value = pr_list[i+1]

					# see if the value of prefix is o.k., ie, there is a ':' at the end
					if prefix[-1] != ':' :
						state.options.add_warning(err_invalid_prefix % (prefix,pr), IncorrectPrefixDefinition, node=state.node.nodeName)
						continue
					elif prefix == ":" :
						state.options.add_warning(err_no_default_prefix % pr, IncorrectPrefixDefinition, node=state.node.nodeName)
						continue
					else :
						prefix = prefix[:-1]
						uri    = Namespace(quote_URI(value, state.options))
						if prefix == "" :
							#something to be done here
							self.default_curie_uri = uri
						elif prefix == "_" :
							state.options.add_warning(err_bnode_local_prefix, IncorrectPrefixDefinition, node=state.node.nodeName)
						else :
							# last check: is the prefix an NCNAME?
							if ncname.match(prefix) :
								real_prefix = prefix.lower()
								dict[real_prefix] = uri
								self.graph.bind(real_prefix,uri)
								# Additional warning: is this prefix overriding an existing xmlns statement with a different URI? if
								# so, that may lead to discrepancies between an RDFa 1.0 and RDFa 1.1 run...
								if (prefix in xmlns_dict and xmlns_dict[prefix] != uri) or (real_prefix in xmlns_dict and xmlns_dict[real_prefix] != uri) :
									state.options.add_warning(err_prefix_and_xmlns % (real_prefix,real_prefix), node=state.node.nodeName)
								check_prefix(real_prefix)

							else :
								state.options.add_warning(err_non_ncname_prefix % (prefix,pr), IncorrectPrefixDefinition, node=state.node.nodeName)

		# See if anything has been collected at all.
		# If not, the namespaces of the incoming state is
		# taken over by reference. Otherwise that is copied to the
		# the local dictionary
		if inherited_state == None :
			self.default_prefixes = default_vocab.ns
			inherited_prefixes    = {}
		else :
			self.default_prefixes = inherited_state.term_or_curie.default_prefixes
			inherited_prefixes    = inherited_state.term_or_curie.ns

		if len(dict) == 0 :
			self.ns = inherited_prefixes
		else :
			self.ns = {}
			for key in inherited_prefixes : self.ns[key] = inherited_prefixes[key]
			for key in dict :
				if (key in inherited_prefixes and dict[key] != inherited_prefixes[key]) or (key in self.default_prefixes and dict[key] != self.default_prefixes[key][0]) :
					state.options.add_warning(err_prefix_redefinition % key, PrefixRedefinitionWarning, node=state.node.nodeName)
				self.ns[key] = dict[key]


		# the xmlns prefixes have to be stored separately, again for XML Literal generation
		self.xmlns = {}
		if len(xmlns_dict) == 0 and inherited_state :
			self.xmlns = inherited_state.term_or_curie.xmlns
		else :
			if inherited_state :
				for key in inherited_state.term_or_curie.xmlns	: self.xmlns[key] = inherited_state.term_or_curie.xmlns[key]
				for key in xmlns_dict							: self.xmlns[key] = xmlns_dict[key]
			else :
				self.xmlns = xmlns_dict
	# end __init__

	def _check_reference(self, val) :
		"""Checking the CURIE reference for correctness. It is probably not 100% foolproof, but may take care
		of some of the possible errors. See the URI RFC for the details.
		"""
		def char_check(s, not_allowed = ['#','[',']']) :
			for c in not_allowed :
				if s.find(c) != -1 : return False
			return True
		# Creating an artificial http URI to fool the urlparse module...
		scheme, netloc, url, query, fragment = urlsplit('http:' + val)
		if netloc != "" and self.state.rdfa_version >= "1.1" :
			self.state.options.add_warning(err_absolute_reference % (netloc, val), UnresolvableReference, node=self.state.node.nodeName)
			return False
		elif not char_check(query) :
			self.state.options.add_warning(err_query_reference % (query, val), UnresolvableReference, node=self.state.node.nodeName)
			return False
		elif not char_check(fragment) :
			self.state.options.add_warning(err_fragment_reference % (fragment, val), UnresolvableReference, node=self.state.node.nodeName)
			return False
		else :
			return True

	def CURIE_to_URI(self, val) :
		"""CURIE to URI mapping.

		This method does I{not} take care of the last step of CURIE processing, ie, the fact that if
		it does not have a CURIE then the value is used a URI. This is done on the caller's side, because this has
		to be combined with base, for example. The method I{does} take care of BNode processing, though, ie,
		CURIE-s of the form "_:XXX".

		@param val: the full CURIE
		@type val: string
		@return: URIRef of a URI or None.
		"""
		# Just to be on the safe side:
		if val == "" :
			return None
		elif val == ":" :
			if self.default_curie_uri :
				return URIRef(self.default_curie_uri)
			else :
				return None

		# See if this is indeed a valid CURIE, ie, it can be split by a colon
		curie_split = val.split(':',1)
		if len(curie_split) == 1 :
			# there is no ':' character in the string, ie, it is not a valid CURIE
			return None
		else :
			if self.state.rdfa_version >= "1.1" :
				prefix	= curie_split[0].lower()
			else :
				prefix	= curie_split[0]
			reference = curie_split[1]

			#if len(reference) > 0 :
			#	if self.state.rdfa_version >= "1.1" and (len(prefix) == 0 or prefix in self.ns) and reference.startswith('//') :
			#		# This has been defined as illegal in RDFa 1.1
			#		self.state.options.add_warning(err_absolute_reference % (reference, val), UnresolvableReference, node=self.state.node.nodeName)
			#		return None
			#	if reference[0] == ":" :
			#		return None

			# first possibility: empty prefix
			if len(prefix) == 0 :
				if self.default_curie_uri and self._check_reference(reference) :
					return self.default_curie_uri[reference]
				else :
					return None
			else :
				# prefix is non-empty; can be a bnode
				if prefix == "_" :
					# yep, BNode processing. There is a difference whether the reference is empty or not...
					if len(reference) == 0 :
						return _empty_bnode
					else :
						# see if this variable has been used before for a BNode
						if reference in _bnodes :
							return _bnodes[reference]
						else :
							# a new bnode...
							retval = BNode()
							_bnodes[reference] = retval
							return retval
				# check if the prefix is a valid NCNAME
				elif ncname.match(prefix) :
					# see if there is a binding for this:
					if prefix in self.ns and self._check_reference(reference) :
						# yep, a binding has been defined!
						if len(reference) == 0 :
							return URIRef(str(self.ns[prefix]))
						else :
							return self.ns[prefix][reference]
					elif prefix in self.default_prefixes and self._check_reference(reference) :
						# this has been defined through the default context
						if len(reference) == 0 :
							return URIRef(str(self.default_prefixes[prefix][0]))
						else :
							(ns,used) = self.default_prefixes[prefix]
							# lazy binding of prefixes (to avoid unnecessary prefix definitions in the serializations at the end...)
							if not used :
								self.graph.bind(prefix,ns)
								self.default_prefixes[prefix] = (ns,True)
							return ns[reference]
					else :
						# no definition for this thing...
						return None
				else :
					return None
	# end CURIE_to_URI

	def term_to_URI(self, term) :
		"""A term to URI mapping, where term is a simple string and the corresponding
		URI is defined via the @vocab (ie, default term uri) mechanism. Returns None if term is not defined
		@param term: string
		@return: an RDFLib URIRef instance (or None)
		"""
		if len(term) == 0 : return None

		if termname.match(term) :
			# It is a valid NCNAME

			# First of all, a @vocab nukes everything. That has to be done first...
			if self.default_term_uri != None :
				return URIRef(self.default_term_uri + term)

			# For default terms, the algorithm is (see 7.4.3 of the document): first make a case sensitive match;
			# if that fails than make a case insensive one
			# 1. simple, case sensitive test:
			if term in self.terms :
				# yep, term is a valid key as is
				# lazy binding of the xhv prefix for terms...
				self.graph.bind(XHTML_PREFIX, XHTML_URI)
				return self.terms[term]

			# 2. case insensitive test
			for defined_term in self.terms :
				if term.lower() == defined_term.lower() :
					# lazy binding of the xhv prefix for terms...
					self.graph.bind(XHTML_PREFIX, XHTML_URI)
					return self.terms[defined_term]

		# If it got here, it is all wrong...
		return None
