#
# Copyright (c), 2016-2020, SISSA (International School for Advanced Studies).
# All rights reserved.
# This file is distributed under the terms of the MIT License.
# See the file 'LICENSE' in the root directory of the present
# distribution, or http://opensource.org/licenses/MIT.
#
# @author Davide Brunato <brunato@sissa.it>
#
"""
This module contains classes for other XML Schema identity constraints.
"""
import re
import math
from collections import Counter
from elementpath import XPath2Parser, ElementPathError, XPathContext, translate_pattern

from ..exceptions import XMLSchemaTypeError, XMLSchemaValueError
from ..qnames import XSD_ANNOTATION, XSD_QNAME, XSD_UNIQUE, XSD_KEY, XSD_KEYREF, \
    XSD_SELECTOR, XSD_FIELD, get_qname, get_extended_qname, is_not_xsd_annotation
from ..xpath import iter_schema_nodes
from .exceptions import XMLSchemaValidationError
from .xsdbase import XsdComponent
from .attributes import XsdAttribute

QNAME_PATTERN = re.compile(
    r'(?:(?P<prefix>[^\d\W][\w\-.\u00B7\u0300-\u036F\u0387\u06DD\u06DE\u203F\u2040]*):)?'
    r'(?P<local>[^\d\W][\w\-.\u00B7\u0300-\u036F\u0387\u06DD\u06DE\u203F\u2040]*)',
)

XSD_IDENTITY_XPATH_SYMBOLS = {
    'processing-instruction', 'following-sibling', 'preceding-sibling',
    'ancestor-or-self', 'attribute', 'following', 'namespace', 'preceding',
    'ancestor', 'position', 'comment', 'parent', 'child', 'false', 'text', 'node',
    'true', 'last', 'not', 'and', 'mod', 'div', 'or', '..', '//', '!=', '<=', '>=', '(', ')',
    '[', ']', '.', '@', ',', '/', '|', '*', '-', '=', '+', '<', '>', ':', '(end)', '(name)',
    '(string)', '(float)', '(decimal)', '(integer)', '::', '{', '}',
}


# XSD identities use a restricted parser and a context for iterate element
# references. The XMLSchemaProxy is not used for the specific selection of
# fields and elements and the XSD fields are got at first validation run.
class IdentityXPathContext(XPathContext):
    _iter_nodes = staticmethod(iter_schema_nodes)


class IdentityXPathParser(XPath2Parser):
    symbol_table = {
        k: v for k, v in XPath2Parser.symbol_table.items() if k in XSD_IDENTITY_XPATH_SYMBOLS
    }
    SYMBOLS = XSD_IDENTITY_XPATH_SYMBOLS


IdentityXPathParser.build_tokenizer()


class XsdSelector(XsdComponent):
    """Class for defining an XPath selector for an XSD identity constraint."""
    _ADMITTED_TAGS = {XSD_SELECTOR}
    xpath_default_namespace = ''
    pattern = re.compile(translate_pattern(
        r"(\.//)?(((child::)?((\i\c*:)?(\i\c*|\*)))|\.)(/(((child::)?"
        r"((\i\c*:)?(\i\c*|\*)))|\.))*(\|(\.//)?(((child::)?((\i\c*:)?"
        r"(\i\c*|\*)))|\.)(/(((child::)?((\i\c*:)?(\i\c*|\*)))|\.))*)*",
        back_references=False,
        lazy_quantifiers=False,
        anchors=False
    ))
    token = None
    parser = None

    def __init__(self, elem, schema, parent):
        super(XsdSelector, self).__init__(elem, schema, parent)

    def _parse(self):
        super(XsdSelector, self)._parse()
        try:
            self.path = self.elem.attrib['xpath']
        except KeyError:
            self.parse_error("'xpath' attribute required:", self.elem)
            self.path = '*'
        else:
            if not self.pattern.match(self.path.replace(' ', '')):
                self.parse_error("Wrong XPath expression for an xs:selector")

        # XSD 1.1 xpathDefaultNamespace attribute
        if self.schema.XSD_VERSION > '1.0':
            if 'xpathDefaultNamespace' in self.elem.attrib:
                self.xpath_default_namespace = self._parse_xpath_default_namespace(self.elem)
            else:
                self.xpath_default_namespace = self.schema.xpath_default_namespace

        self.parser = IdentityXPathParser(
            namespaces=self.namespaces,
            strict=False,
            compatibility_mode=True,
            default_namespace=self.xpath_default_namespace,
        )

        try:
            self.token = self.parser.parse(self.path)
        except ElementPathError as err:
            self.parse_error(err)
            self.token = self.parser.parse('*')

    def __repr__(self):
        return '%s(path=%r)' % (self.__class__.__name__, self.path)

    @property
    def built(self):
        return self.token is not None

    @property
    def target_namespace(self):
        if ':' in self.path:
            match = QNAME_PATTERN.findall(self.path)
            if match is not None:
                prefix = match[0][0]
                if prefix:
                    return self.namespaces[prefix]
                elif self.xpath_default_namespace:
                    return self.xpath_default_namespace

        return self.schema.target_namespace


class XsdFieldSelector(XsdSelector):
    """Class for defining an XPath field selector for an XSD identity constraint."""
    _ADMITTED_TAGS = {XSD_FIELD}
    pattern = re.compile(translate_pattern(
        r"(\.//)?((((child::)?((\i\c*:)?(\i\c*|\*)))|\.)/)*((((child::)?"
        r"((\i\c*:)?(\i\c*|\*)))|\.)|((attribute::|@)((\i\c*:)?(\i\c*|\*))))"
        r"(\|(\.//)?((((child::)?((\i\c*:)?(\i\c*|\*)))|\.)/)*"
        r"((((child::)?((\i\c*:)?(\i\c*|\*)))|\.)|"
        r"((attribute::|@)((\i\c*:)?(\i\c*|\*)))))*",
        back_references=False,
        lazy_quantifiers=False,
        anchors=False
    ))


class XsdIdentity(XsdComponent):
    """
    Common class for XSD identity constraints.

    :ivar selector: the XPath selector of the identity constraint.
    :ivar fields: a list containing the XPath field selectors of the identity constraint.
    """
    selector = None
    elements = None  # XSD elements bound by selector (for speed-up and lazy mode)
    fields = ()

    def __init__(self, elem, schema, parent):
        super(XsdIdentity, self).__init__(elem, schema, parent)

    def _parse(self):
        super(XsdIdentity, self)._parse()
        elem = self.elem
        try:
            self.name = get_qname(self.target_namespace, elem.attrib['name'])
        except KeyError:
            self.parse_error("missing required attribute 'name'", elem)
            self.name = None

        for index, child in enumerate(elem):
            if child.tag == XSD_SELECTOR:
                self.selector = XsdSelector(child, self.schema, self)
                break
            elif child.tag != XSD_ANNOTATION:
                self.parse_error("'selector' declaration expected.", elem)
                break
        else:
            self.parse_error("missing 'selector' declaration.", elem)
            index = -1

        self.fields = []
        for child in filter(is_not_xsd_annotation, elem[index + 1:]):
            if child.tag == XSD_FIELD:
                self.fields.append(XsdFieldSelector(child, self.schema, self))
            else:
                self.parse_error("%r is not allowed here" % child, elem)

    def _parse_identity_reference(self):
        super(XsdIdentity, self)._parse()
        self.name = get_qname(self.target_namespace, self.elem.attrib['ref'])
        if 'name' in self.elem.attrib:
            self.parse_error("attributes 'name' and 'ref' are mutually exclusive")
        elif self._parse_child_component(self.elem) is not None:
            self.parse_error("a reference cannot has child definitions")

    def build(self):
        if self.ref is True:
            try:
                ref = self.maps.identities[self.name]
            except KeyError:
                self.parse_error("Unknown identity constraint {!r}".format(self.name))
                return
            else:
                if not isinstance(ref, self.__class__):
                    self.parse_error("attribute 'ref' points to a different kind constraint")
                self.selector = ref.selector
                self.fields = ref.fields
                self.ref = ref

        context = IdentityXPathContext(self.schema, item=self.parent)

        self.elements = {
            e: None for e in self.selector.token.select_results(context) if e.name
        }

        if any(isinstance(e, XsdAttribute) for e in self.elements):
            self.parse_error("selector xpath cannot select attributes")
        elif not self.elements:
            # Try to detect target XSD elements extracting QNames
            # of the leaf elements from the XPath expression and
            # use them to match global elements.

            for qname in self.selector.token.iter_leaf_elements():
                xsd_element = self.maps.elements.get(get_extended_qname(qname, self.namespaces))
                if xsd_element is not None and xsd_element not in self.elements:
                    self.elements[xsd_element] = None

    @property
    def built(self):
        return self.elements is not None

    def get_fields(self, elem, namespaces=None, decoders=None):
        """
        Get fields for a schema or instance context element.

        :param elem: an Element or an XsdElement
        :param namespaces: is an optional mapping from namespace prefix to URI.
        :param decoders: context schema fields decoders.
        :return: a tuple with field values. An empty field is replaced by `None`.
        """
        fields = []
        if isinstance(elem, XsdComponent):
            context_class = IdentityXPathContext
        else:
            context_class = XPathContext

        for k, field in enumerate(self.fields):
            result = field.token.get_results(context_class(elem))
            if not result:
                if decoders is not None and decoders[k] is not None:
                    value = decoders[k].value_constraint
                    if value is not None:
                        if decoders[k].type.root_type.name == XSD_QNAME:
                            value = get_extended_qname(value, namespaces)

                        if isinstance(value, list):
                            fields.append(tuple(value))
                        elif isinstance(value, bool):
                            fields.append((value, bool))
                        elif not isinstance(value, float):
                            fields.append(value)
                        elif math.isnan(value):
                            fields.append(('nan', float))
                        else:
                            fields.append((value, float))

                        continue

                if not isinstance(self, XsdKey) or 'ref' in elem.attrib and \
                        self.schema.meta_schema is None and self.schema.XSD_VERSION != '1.0':
                    fields.append(None)
                elif field.target_namespace not in self.maps.namespaces:
                    fields.append(None)
                else:
                    raise XMLSchemaValueError("%r key field must have a value!" % field)

            elif len(result) == 1:
                if decoders is None or decoders[k] is None:
                    fields.append(result[0])
                else:
                    if decoders[k].type.content_type_label not in ('simple', 'mixed'):
                        raise XMLSchemaTypeError("%r field doesn't have a simple type!" % field)

                    value = decoders[k].data_value(result[0])
                    if decoders[k].type.root_type.name == XSD_QNAME:
                        value = get_extended_qname(value, namespaces)

                    if isinstance(value, list):
                        fields.append(tuple(value))
                    elif isinstance(value, bool):
                        fields.append((value, bool))
                    elif not isinstance(value, float):
                        fields.append(value)
                    elif math.isnan(value):
                        fields.append(('nan', float))
                    else:
                        fields.append((value, float))
            else:
                raise XMLSchemaValueError("%r field selects multiple values!" % field)

        return tuple(fields)

    def get_counter(self, enabled=True):
        return IdentityCounter(self, enabled)


class XsdUnique(XsdIdentity):
    _ADMITTED_TAGS = {XSD_UNIQUE}


class XsdKey(XsdIdentity):
    _ADMITTED_TAGS = {XSD_KEY}


class XsdKeyref(XsdIdentity):
    """
    Implementation of xs:keyref.

    :ivar refer: reference to a *xs:key* declaration that must be in the same element \
    or in a descendant element.
    """
    _ADMITTED_TAGS = {XSD_KEYREF}
    refer = None
    refer_path = '.'

    def _parse(self):
        super(XsdKeyref, self)._parse()
        try:
            self.refer = self.schema.resolve_qname(self.elem.attrib['refer'])
        except (KeyError, ValueError, RuntimeError) as err:
            if 'refer' not in self.elem.attrib:
                self.parse_error("missing required attribute 'refer'")
            else:
                self.parse_error(err)

    def build(self):
        super(XsdKeyref, self).build()

        if isinstance(self.refer, (XsdKey, XsdUnique)):
            return  # referenced key/unique identity constraint already set
        elif isinstance(self.ref, XsdKeyref):
            self.refer = self.ref.refer

        if self.refer is None:
            return  # attribute or key/unique identity constraint missing
        elif isinstance(self.refer, str):
            refer = self.parent.identities.get(self.refer)
            if refer is not None and refer.ref is None:
                self.refer = refer
            else:
                try:
                    self.refer = self.maps.identities[self.refer]
                except KeyError:
                    self.parse_error("key/unique identity constraint %r is missing" % self.refer)
                    return

        if not isinstance(self.refer, (XsdKey, XsdUnique)):
            self.parse_error("reference to a non key/unique identity constraint %r" % self.refer)
        elif len(self.refer.fields) != len(self.fields):
            self.parse_error("field cardinality mismatch between %r and %r" % (self, self.refer))
        elif self.parent is not self.refer.parent:
            refer_path = self.refer.parent.get_path(ancestor=self.parent)
            if refer_path is None:
                # From a note in par. 3.11.5 Part 1 of XSD 1.0 spec: "keyref
                # identity-constraints may be defined on domains distinct from
                # the embedded domain of the identity-constraint they reference,
                # or the domains may be the same but self-embedding at some depth.
                # In either case the node table for the referenced identity-constraint
                # needs to propagate upwards, with conflict resolution."
                refer_path = self.parent.get_path(ancestor=self.refer.parent, reverse=True)
                if refer_path is None:
                    refer_path = self.parent.get_path(reverse=True) + '/' + \
                        self.refer.parent.get_path()

            self.refer_path = refer_path

    @property
    def built(self):
        return self.elements is not None and isinstance(self.refer, XsdIdentity)

    def __call__(self, identities, elem, namespaces=None):
        if self.refer is None:
            return

        values = identities[self].counter
        refer_values = identities[self.refer].counter

        for v in values:
            if v not in refer_values:
                reason = "Value {!r} not found for key {} ({} times)" \
                    .format(v, self.refer.prefixed_name, values[v])
                yield XMLSchemaValidationError(validator=self, obj=elem, reason=reason)

    def get_counter(self, enabled=True):
        return KeyrefCounter(self, enabled)


class Xsd11Unique(XsdUnique):

    def _parse(self):
        if self._parse_reference():
            super(XsdIdentity, self)._parse()
            self.ref = True
        else:
            super(Xsd11Unique, self)._parse()


class Xsd11Key(XsdKey):

    def _parse(self):
        if self._parse_reference():
            super(XsdIdentity, self)._parse()
            self.ref = True
        else:
            super(Xsd11Key, self)._parse()


class Xsd11Keyref(XsdKeyref):

    def _parse(self):
        if self._parse_reference():
            super(XsdIdentity, self)._parse()
            self.ref = True
        else:
            super(Xsd11Keyref, self)._parse()


class IdentityCounter(object):

    def __init__(self, identity, enabled=True):
        self.counter = Counter()
        self.identity = identity
        self.enabled = enabled

    def __repr__(self):
        return "%s(counter=%r)" % (self.__class__.__name__, self.counter)

    def clear(self):
        self.counter.clear()
        self.enabled = True

    def increase(self, fields):
        self.counter[fields] += 1
        if self.counter[fields] == 2:
            msg = "duplicated value {!r} for {!r}"
            raise XMLSchemaValueError(msg.format(fields, self.identity))


class KeyrefCounter(IdentityCounter):

    def increase(self, fields):
        self.counter[fields] += 1

    def iter_errors(self, identities):
        try:
            refer_values = identities[self.identity.refer].counter
        except KeyError:
            if self.identity.refer is not None:
                raise
        else:
            for v in filter(lambda x: x not in refer_values, self.counter):
                if len(v) == 1 and v[0] in refer_values:
                    continue
                elif self.counter[v] > 1:
                    msg = "Value {} not found for {!r} ({} times)"
                    yield XMLSchemaValueError(msg.format(v, self.identity.refer, self.counter[v]))
                else:
                    msg = "Value {} not found for {!r}"
                    yield XMLSchemaValueError(msg.format(v, self.identity.refer))
