# -*- 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

from urllib.parse import urlsplit


from rdflib import URIRef
from rdflib import BNode
from rdflib import Namespace

from .utils import quote_URI
from .host import predefined_1_0_rel, warn_xmlns_usage
from . import IncorrectPrefixDefinition, RDFA_VOCAB, UnresolvableReference, PrefixRedefinitionWarning

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(r"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 i in context_ids[state.options.host_language]:
            # This gives the id of a initial context, valid for this media type:
            data = context_data[i]
            
            # 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
        ns_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
                        ns_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()
                                ns_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(ns_dict) == 0:
            self.ns = inherited_prefixes
        else:
            self.ns = {}
            for key in inherited_prefixes : self.ns[key] = inherited_prefixes[key]
            for key in ns_dict : 
                if (key in inherited_prefixes and ns_dict[key] != inherited_prefixes[key]) or (key in self.default_prefixes and ns_dict[key] != self.default_prefixes[key][0]):
                    state.options.add_warning(err_prefix_redefinition % key, PrefixRedefinitionWarning, node=state.node.nodeName)
                self.ns[key] = ns_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
