# -*- coding: utf-8 -*-
#
# Copyright (c), 2016-2019, 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 exception and warning classes for the 'xmlschema.validators' subpackage.
"""
from __future__ import unicode_literals

from ..compat import PY3, string_base_type
from ..exceptions import XMLSchemaException, XMLSchemaWarning, XMLSchemaValueError
from ..qnames import qname_to_prefixed
from ..etree import etree_tostring, etree_getpath
from ..helpers import is_etree_element
from ..resources import XMLResource


class XMLSchemaValidatorError(XMLSchemaException):
    """
    Base class for XSD validator errors.

    :param validator: the XSD validator.
    :type validator: XsdValidator or function
    :param message: the error message.
    :type message: str or unicode
    :param elem: the element that contains the error.
    :type elem: Element
    :param source: the XML resource that contains the error.
    :type source: XMLResource
    :param namespaces: is an optional mapping from namespace prefix to URI.
    :type namespaces: dict
    :ivar path: the XPath of the element, calculated when the element is set or the XML resource is set.
    """
    def __init__(self, validator, message, elem=None, source=None, namespaces=None):
        self.path = None
        self.validator = validator
        self.message = message[:-1] if message[-1] in ('.', ':') else message
        self.namespaces = namespaces
        self.source = source
        self.elem = elem

    def __str__(self):
        return unicode(self).encode("utf-8")

    def __unicode__(self):
        if self.elem is None:
            return '%s.' % self.message
        else:
            msg = ['%s:\n' % self.message]
            if self.elem is not None:
                elem_as_string = etree_tostring(self.elem, self.namespaces, '  ', 20)
                if hasattr(self.elem, 'sourceline'):
                    msg.append("Schema (line %r):\n\n%s\n" % (self.elem.sourceline, elem_as_string))
                else:
                    msg.append("Schema:\n\n%s\n" % elem_as_string)
            if self.path is not None:
                msg.append("Path: %s\n" % self.path)
            if self.schema_url is not None:
                msg.append("Schema URL: %s\n" % self.schema_url)
                if self.origin_url not in (None, self.schema_url):
                    msg.append("Origin URL: %s\n" % self.origin_url)
            return '\n'.join(msg)

    if PY3:
        __str__ = __unicode__

    @property
    def msg(self):
        return self.__unicode__()

    def __setattr__(self, name, value):
        if name == 'elem' and value is not None:
            if not is_etree_element(value):
                raise XMLSchemaValueError("'elem' attribute requires an Element, not %r." % type(value))
            if self.source is not None:
                self.path = etree_getpath(value, self.root, self.namespaces, relative=False, add_position=True)
                if self.source.is_lazy():
                    value = None  # Don't save the element of a lazy resource
        if name == 'source' and value is not None and getattr(self, 'elem', None) is not None:
            self.path = etree_getpath(self.elem, value.root, self.namespaces, relative=False, add_position=True)
            if value.is_lazy():
                self.elem = None
        super(XMLSchemaValidatorError, self).__setattr__(name, value)

    @property
    def sourceline(self):
        return getattr(self.elem, 'sourceline', None)

    @property
    def root(self):
        try:
            return self.source.root
        except AttributeError:
            return

    @property
    def schema_url(self):
        try:
            return self.validator.schema.source.url
        except AttributeError:
            return

    @property
    def origin_url(self):
        try:
            return self.validator.maps.validator.source.url
        except AttributeError:
            return


class XMLSchemaNotBuiltError(XMLSchemaValidatorError, RuntimeError):
    """
    Raised when there is an improper usage attempt of a not built XSD validator.

    :param validator: the XSD validator.
    :type validator: XsdValidator
    :param message: the error message.
    :type message: str or unicode
    """
    def __init__(self, validator, message):
        super(XMLSchemaNotBuiltError, self).__init__(
            validator=validator,
            message=message,
            elem=getattr(validator, 'elem', None),
            source=getattr(validator, 'source', None),
            namespaces=getattr(validator, 'namespaces', None)
        )


class XMLSchemaParseError(XMLSchemaValidatorError, SyntaxError):
    """
    Raised when an error is found during the building of an XSD validator.

    :param validator: the XSD validator.
    :type validator: XsdValidator or function
    :param message: the error message.
    :type message: str or unicode
    :param elem: the element that contains the error.
    :type elem: Element
    """
    def __init__(self, validator, message, elem=None):
        super(XMLSchemaParseError, self).__init__(
            validator=validator,
            message=message,
            elem=elem if elem is not None else getattr(validator, 'elem', None),
            source=getattr(validator, 'source', None),
            namespaces=getattr(validator, 'namespaces', None),
        )


class XMLSchemaModelError(XMLSchemaValidatorError, ValueError):
    """
    Raised when a model error is found during the checking of a model group.

    :param group: the XSD model group.
    :type group: XsdGroup
    :param message: the error message.
    :type message: str or unicode
    """
    def __init__(self, group, message):
        super(XMLSchemaModelError, self).__init__(
            validator=group,
            message=message,
            elem=group.elem,
            source=getattr(group, 'source', None),
            namespaces=group.namespaces
        )


class XMLSchemaModelDepthError(XMLSchemaModelError):
    """Raised when recursion depth is exceeded while iterating a model group."""
    def __init__(self, group):
        msg = "maximum model recursion depth exceeded while iterating group %r" % group
        super(XMLSchemaModelDepthError, self).__init__(group, message=msg)


class XMLSchemaValidationError(XMLSchemaValidatorError, ValueError):
    """
    Raised when the XML data is not validated with the XSD component or schema.
    It's used by decoding and encoding methods. Encoding validation errors do
    not include XML data element and source, so the error is limited to a message
    containing object representation and a reason.

    :param validator: the XSD validator.
    :type validator: XsdValidator or function
    :param obj: the not validated XML data.
    :type obj: Element or tuple or str or list or int or float or bool
    :param reason: the detailed reason of failed validation.
    :type reason: str or unicode
    :param source: the XML resource that contains the error.
    :type source: XMLResource
    :param namespaces: is an optional mapping from namespace prefix to URI.
    :type namespaces: dict
    """
    def __init__(self, validator, obj, reason=None, source=None, namespaces=None):
        if not isinstance(obj, string_base_type):
            _obj = obj
        else:
            _obj = obj.encode('ascii', 'xmlcharrefreplace').decode('utf-8')

        super(XMLSchemaValidationError, self).__init__(
            validator=validator,
            message="failed validating {!r} with {!r}".format(_obj, validator),
            elem=obj if is_etree_element(obj) else None,
            source=source,
            namespaces=namespaces,
        )
        self.obj = obj
        self.reason = reason

    def __str__(self):
        # noinspection PyCompatibility,PyUnresolvedReferences
        return unicode(self).encode("utf-8")

    def __unicode__(self):
        msg = ['%s:\n' % self.message]
        if self.reason is not None:
            msg.append('Reason: %s\n' % self.reason)
        if hasattr(self.validator, 'tostring'):
            msg.append("Schema:\n\n%s\n" % self.validator.tostring('  ', 20))
        if is_etree_element(self.elem):
            try:
                elem_as_string = etree_tostring(self.elem, self.namespaces, '  ', 20)
            except (ValueError, TypeError):
                elem_as_string = repr(self.elem)

            if hasattr(self.elem, 'sourceline'):
                msg.append("Instance (line %r):\n\n%s\n" % (self.elem.sourceline, elem_as_string))
            else:
                msg.append("Instance:\n\n%s\n" % elem_as_string)
        if self.path is not None:
            msg.append("Path: %s\n" % self.path)
        return '\n'.join(msg)

    if PY3:
        __str__ = __unicode__


class XMLSchemaDecodeError(XMLSchemaValidationError):
    """
    Raised when an XML data string is not decodable to a Python object.

    :param validator: the XSD validator.
    :type validator: XsdValidator or function
    :param obj: the not validated XML data.
    :type obj: Element or tuple or str or list or int or float or bool
    :param decoder: the XML data decoder.
    :type decoder: type or function
    :param reason: the detailed reason of failed validation.
    :type reason: str or unicode
    :param source: the XML resource that contains the error.
    :type source: XMLResource
    :param namespaces: is an optional mapping from namespace prefix to URI.
    :type namespaces: dict
    """
    message = "failed decoding {!r} with {!r}.\n"

    def __init__(self, validator, obj, decoder, reason=None, source=None, namespaces=None):
        super(XMLSchemaDecodeError, self).__init__(validator, obj, reason, source, namespaces)
        self.decoder = decoder


class XMLSchemaEncodeError(XMLSchemaValidationError):
    """
    Raised when an object is not encodable to an XML data string.

    :param validator: the XSD validator.
    :type validator: XsdValidator or function
    :param obj: the not validated XML data.
    :type obj: Element or tuple or str or list or int or float or bool
    :param encoder: the XML encoder.
    :type encoder: type or function
    :param reason: the detailed reason of failed validation.
    :type reason: str or unicode
    :param source: the XML resource that contains the error.
    :type source: XMLResource
    :param namespaces: is an optional mapping from namespace prefix to URI.
    :type namespaces: dict
    """
    message = "failed encoding {!r} with {!r}.\n"

    def __init__(self, validator, obj, encoder, reason=None, source=None, namespaces=None):
        super(XMLSchemaEncodeError, self).__init__(validator, obj, reason, source, namespaces)
        self.encoder = encoder


class XMLSchemaChildrenValidationError(XMLSchemaValidationError):
    """
    Raised when a child element is not validated.

    :param validator: the XSD validator.
    :type validator: XsdValidator or function
    :param elem: the not validated XML element.
    :type elem: Element or ElementData
    :param index: the child index.
    :type index: int
    :param particle: the validator particle that generated the error. Maybe the validator itself.
    :type particle: ParticleMixin
    :param occurs: the particle occurrences.
    :type occurs: int
    :param expected: the expected element tags/object names.
    :type expected: str or list or tuple
    :param source: the XML resource that contains the error.
    :type source: XMLResource
    :param namespaces: is an optional mapping from namespace prefix to URI.
    :type namespaces: dict
    """
    def __init__(self, validator, elem, index, particle, occurs=0, expected=None, source=None, namespaces=None):
        self.index = index
        self.particle = particle
        self.occurs = occurs
        self.expected = expected

        tag = qname_to_prefixed(elem.tag, validator.namespaces, use_empty=False)
        if index >= len(elem):
            reason = "The content of element %r is not complete." % tag
        else:
            child_tag = qname_to_prefixed(elem[index].tag, validator.namespaces, use_empty=False)
            reason = "Unexpected child with tag %r at position %d." % (child_tag, index + 1)

        if occurs and particle.is_missing(occurs):
            reason += " The particle %r occurs %d times but the minimum is %d." % (
                particle, occurs, particle.min_occurs
            )
        elif particle.is_over(occurs):
            reason += " The particle %r occurs %d times but the maximum is %d." % (
                particle, occurs, particle.max_occurs
            )

        if expected is None:
            pass
        else:
            expected_tags = []
            for xsd_element in expected:
                if xsd_element.name is not None:
                    expected_tags.append(xsd_element.prefixed_name)
                elif xsd_element.process_contents == 'strict':
                    expected_tags.append('from %r namespace/s' % xsd_element.namespace)

            if not expected_tags:
                pass  # reason += " No child element is expected at this point." <-- this can be misleading
            elif len(expected_tags) == 1:
                reason += " Tag %r expected." % expected_tags[0]
            else:
                reason += " Tag (%s) expected." % ' | '.join(expected_tags)

        super(XMLSchemaChildrenValidationError, self).__init__(validator, elem, reason, source, namespaces)


class XMLSchemaIncludeWarning(XMLSchemaWarning):
    """A schema include fails."""


class XMLSchemaImportWarning(XMLSchemaWarning):
    """A schema namespace import fails."""


class XMLSchemaTypeTableWarning(XMLSchemaWarning):
    """Not equivalent type table found in model."""
