# -*- coding: utf-8 -*-

# --------------------------------------------------------------------
# The MIT License (MIT)
#
# Copyright (c) 2016 Jonathan Labéjof <jonathan.labejof@gmail.com>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# --------------------------------------------------------------------

"""Schema utilities package."""

from six import iteritems, add_metaclass

from inspect import getmembers

from .registry import getbydatatype, register
from .lang.factory import build, getschemacls
from .base import Schema, DynamicValue

__all__ = [
    'DynamicValue', 'data2schema', 'MetaRegisteredSchema', 'ThisSchema',
    'updatecontent', 'validate', 'dump', 'RegisteredSchema',
    'datatype2schemacls', 'RefSchema', 'data2schemacls', 'AnySchema'
]


class AnySchema(Schema):
    """Schema for any data."""

    def _validate(*args, **kwargs):
        pass


def datatype2schemacls(
        _datatype, _registry=None, _factory=None, _force=True,
        _besteffort=True, **kwargs
):
    """Get a schema class which has been associated to input data type by the
    registry or the factory in this order.

    :param type datatype: data type from where get associated schema.
    :param SchemaRegisgry _registry: registry from where call the getbydatatype
        . Default is the global registry.
    :param SchemaFactory _factory: factory from where call the getschemacls if
        getbydatatype returns None. Default is the global factory.
    :param bool _force: if true (default), force the building of schema class
        if no schema is associated to input data type.
    :param bool _besteffort: if True (default), try to resolve schema by
        inheritance.
    :param dict kwargs: factory builder kwargs.
    :rtype: type
    :return: Schema associated to input registry or factory. None if no
        association found.
    """
    result = None

    gdbt = getbydatatype if _registry is None else _registry.getbydatatype

    result = gdbt(_datatype, besteffort=_besteffort)

    if result is None:
        gscls = getschemacls if _factory is None else _factory.getschemacls
        result = gscls(_datatype, besteffort=_besteffort)

    if result is None and _force:
        _build = build if _factory is None else _factory.build

        result = _build(_resource=_datatype, **kwargs)

    return result


def data2schema(
        _data=None, _force=False, _besteffort=True, _registry=None,
        _factory=None, _buildkwargs=None, **kwargs
):
    """Get the schema able to instanciate input data.

    The default value of schema will be data.

    Can be used such as a decorator:

    ..code-block:: python

        @data2schema
        def example(): pass  # return a function schema

        @data2schema(_registry=myregistry)
        def example(): pass  # return a function schema with specific registry

    ..warning::

        return this function id _data is None.

    :param _data: data possibly generated by a schema. Required but in case of
        decorator.
    :param bool _force: if True (False by default), create the data schema
        on the fly if it does not exist.
    :param bool _besteffort: if True (default), find a schema class able to
        validate data class by inheritance.
    :param SchemaRegistry _registry: default registry to use. Global by
        default.
    :param SchemaFactory factory: default factory to use. Global by default.
    :param dict _buildkwargs: factory builder kwargs.
    :param kwargs: schema class kwargs.
    :return: Schema.
    :rtype: Schema.
    """
    if _data is None:
        return lambda _data: data2schema(
            _data, _force=False, _besteffort=True, _registry=None,
            _factory=None, _buildkwargs=None, **kwargs
        )

    result = None

    fdata = _data() if isinstance(_data, DynamicValue) else _data

    datatype = type(fdata)

    content = getattr(fdata, '__dict__', {})
    if _buildkwargs:
        content.udpate(_buildkwargs)

    schemacls = datatype2schemacls(
        _datatype=datatype, _registry=_registry, _factory=_factory,
        _force=_force, _besteffort=_besteffort, **content
    )

    if schemacls is not None:
        result = schemacls(default=_data, **kwargs)

        for attrname in dir(_data):

            if not hasattr(schemacls, attrname):
                attr = getattr(_data, attrname)
                if attr is not None:
                    setattr(result, attrname, attr)

    if result is None and _data is None:
        result = AnySchema()

    return result


def data2schemacls(_data, **kwargs):
    """Convert a data to a schema cls.

    :param data: object or dictionary from where get a schema cls.
    :return: schema class.
    :rtype: type
    """
    content = {}

    for key in list(kwargs):  # fill kwargs
        kwargs[key] = data2schema(kwargs[key])

    if isinstance(_data, dict):
        datacontent = iteritems(_data)

    else:
        datacontent = getmembers(_data)

    for name, value in datacontent:

        if name[0] == '_':
            continue

        if isinstance(value, dict):
            schema = data2schemacls(value)()

        else:
            schema = data2schema(value)

        content[name] = schema

    content.update(kwargs)  # update content

    result = type('GeneratedSchema', (Schema,), content)

    return result


class ThisSchema(object):
    """Tool Used to set inner schemas with the same type with specific arguments
    .

    ThisSchema one might be use at the condition instanciation methods must not
    reference the class.

    ..example::

        class Test(Schema):
            # contain an inner schema nullable 'test' of type Test.
            test = ThisSchema(nullable=False)

            def __init__(self, *args, **kwargs):

                # old style call because when the schema will be automatically
                # updated, the class Test does not exist in the scope
                Schema.__init__(self, *args, **kwargs)

    :param args: schema class vargs to use.
    :param kwargs: schema class kwargs to use.

    :return: input args and kwargs.
    :rtype: tuple
    """

    def __init__(self, *args, **kwargs):

        super(ThisSchema, self).__init__()

        self.args = args
        self.kwargs = kwargs


def validate(schema, data, owner=None):
    """Validate input data with input schema.

    :param Schema schema: schema able to validate input data.
    :param data: data to validate.
    :param Schema owner: input schema parent schema.
    :raises: Exception if the data is not validated.
    """
    schema._validate(data=data, owner=owner)


def dump(schema):
    """Get a serialized value of input schema.

    :param Schema schema: schema to serialize.
    :rtype: dict
    """
    result = {}

    for name, _ in iteritems(schema.getschemas()):

        if hasattr(schema, name):
            val = getattr(schema, name)

            if isinstance(val, DynamicValue):
                val = val()

            if isinstance(val, Schema):
                val = dump(val)

            result[name] = val

    return result


class RefSchema(Schema):
    """Schema which references another schema."""

    ref = Schema(name='ref')  #: the reference must be a schema.

    def __init__(self, ref=None, *args, **kwargs):
        """
        :param Schema ref: refereed schema.
        """

        super(RefSchema, self).__init__(*args, **kwargs)

        if ref is not None:

            if self.default is None:
                self.default = ref.default

            self.ref = ref

    def _validate(self, data, owner=None, *args, **kwargs):

        ref = owner if self.ref is None else self.ref

        if ref is not self:
            ref._validate(data=data, owner=owner, *args, **kwargs)

    def _setvalue(self, schema, value, *args, **kwargs):

        super(RefSchema, self)._setvalue(schema, value, *args, **kwargs)

        if schema.name == 'ref' and value is not None:

            if self.default is None and not value.nullable:

                self.default = value.default

            value._validate(self.default)


def updatecontent(schemacls=None, updateparents=True, exclude=None):
    """Transform all schema class attributes to schemas.

    It can be used such as a decorator in order to ensure to update attributes
    with the decorated schema but take care to the limitation to use old style
    method call for overidden methods.

    .. example:
        @updatecontent  # update content at the end of its definition.
        class Test(Schema):
            this = ThisSchema()  # instance of Test.
            def __init__(self, *args, **kwargs):
                Test.__init__(self, *args, **kwargs)  # old style method call.

    :param type schemacls: sub class of Schema.
    :param bool updateparents: if True (default), update parent content.
    :param list exclude: attribute names to exclude from updating.
    :return: schemacls.
    """
    if schemacls is None:
        return lambda schemacls: updatecontent(
            schemacls=schemacls, updateparents=updateparents, exclude=exclude
        )

    if updateparents and hasattr(schemacls, 'mro'):
        schemaclasses = reversed(list(schemacls.mro()))

    else:
        schemaclasses = [schemacls]

    for schemaclass in schemaclasses:
        for name, member in iteritems(getattr(schemaclass, '__dict__', {})):
            # transform only public members
            if name[0] != '_' and (exclude is None or name not in exclude):

                toset = False  # flag for setting schemas

                fmember = member

                if isinstance(fmember, DynamicValue):
                    fmember = fmember()
                    toset = True

                if isinstance(fmember, Schema):
                    schema = fmember

                    if not schema.name:
                        schema.name = name

                else:
                    toset = True

                    data = member

                    if name == 'default':

                        if isinstance(fmember, ThisSchema):
                            data = schemaclass(*fmember.args, **fmember.kwargs)

                        schema = RefSchema(default=data, name=name)

                    elif isinstance(fmember, ThisSchema):

                        schema = schemaclass(
                            name=name, *fmember.args, **fmember.kwargs
                        )

                    elif member is None:
                        schema = AnySchema(name=name)

                    else:
                        schema = data2schema(_data=data, name=name)

                if isinstance(schema, Schema) and toset:

                    try:
                        setattr(schemaclass, name, schema)

                    except (AttributeError, TypeError):
                        break

    return schemacls

updatecontent(RefSchema)


class MetaRegisteredSchema(type):
    """Automatically register schemas."""

    def __new__(mcs, *args, **kwargs):

        result = super(MetaRegisteredSchema, mcs).__new__(mcs, *args, **kwargs)

        # update all sub schemas related to values
        if result.__update_content__:
            updatecontent(schemacls=result)

        return result

    def __call__(cls, *args, **kwargs):

        result = super(MetaRegisteredSchema, cls).__call__(*args, **kwargs)

        if result.__register__:  # register all new schema
            register(schema=result)

        return result


# use metaRegisteredschema such as the schema metaclass
@add_metaclass(MetaRegisteredSchema)
class RegisteredSchema(Schema):
    """Ease auto-registering of schemas and auto-updating content."""

    #: Register instances in the registry if True (False by default).
    __register__ = False

    """update automatically the content if True (default).

    If True, take care to not having called the class in overidden methods.
    In such case, take a look to the using of the class ThisSchema which
    recommands to use old style method call for overriden methods.

    ..example:
        class Test(Schema):
            __udpate_content__ = True  # set update content to True
            test = ThisSchema()
            def __init__(self, *args, **kwargs):
                Schema.__init__(self, *args, **kwargs)  # old style call.
        """
    __update_content__ = True
