#!/usr/bin/env python
# -*- coding:utf-8 -*-
"""Model and Argument."""
from __future__ import print_function, division, unicode_literals

import abc
import inspect

import six

from .types import Nested
from .exception import ConvertError, ArgumentMissError, ArgumentInvalidError


@six.add_metaclass(abc.ABCMeta)
class BaseAdapter(object):
    """To implement your own adapter, you need inherit from
    :class:`~parameter.model.BaseAdapter`.

    There two methods must be overwirtten:

    - ``get_argument``: Returns a single value
    - ``get_arguments``: Returns a sequence of values.

    Example::

        from parameter.model import BaseAdapter


        class DemoAdapter(BaseAdapter):
            "demo adapter"
            def __init__(self, arguments):
                self.arguments = arguments

            def get_argument(self, name, default):
                return self.arguments.get(name, default)

            def get_arguments(self, name):
                return self.arguments.getlist(name)

    If you want your adapter to support nested, you need to override
    the ``spawn`` method, this method use the given value to return an new
    instance of the current adapter.

    ::

        from parameter.model import BaseAdapter


        class DemoAdapter(BaseAdapter):
            # see above
            def spawn(self, arguments):
                return DemoAdapter(arguments)
    """
    @abc.abstractmethod
    def get_argument(self, name, default, *args, **kwargs):
        """Returns the argument's value via ``name``.

        :param name: The name of the argument.
        :param default: The default value.
        :raises: :class:`~parameter.exception.ArgumentMissError`
        :raises: :class:`~parameter.exception.ArgumentInvalidError`
        """

    @abc.abstractmethod
    def get_arguments(self, name, *args, **kwargs):
        """Returns the argument's values via ``name``.

        :param name: The name of the argument.
        :raises: :class:`~parameter.exception.ArgumentMissError`
        :raises: :class:`~parameter.exception.ArgumentInvalidError`
        """

    @staticmethod
    def spawn(val):
        """Use the new value to spawn an new adapter of this adapter.
        """
        raise NotImplementedError()


class Argument(object):
    """Represents a parameter in HTTP request."""

    _DEFAULT = []       # type: list

    def __init__(self, type_, default=_DEFAULT, alias=None, multiple=False,
                 miss_message=None, invalid_message=None):
        """Initialize

        :param type_:
            The parameter's type, indicated using an instance which subclasses
            :class:`parameter.types.BaseType`.
        :type type_: :class:`parameter.types.BaseType`.
        :param default: The default value.
        :param alias:
            The alias name of this argument as represented in the HTTP request.
        :param multiple: This argument have multiple values.
        :param miss_message:
            The message of
            :class:`~parameter.exception.ArgumentMissError`
        :param invalid_message:
            The message of
            :class:`~parameter.exception.ArgumentInvalidError`
        """
        self.name = None
        self.alias = alias
        self.type_ = type_() if inspect.isclass(type_) else type_
        self.default = default
        self.multiple = multiple
        self.miss_message = miss_message
        self.invalid_message = invalid_message

    @classmethod
    def is_init_default(cls, value):
        """Returns ``True`` if the ``value`` is the initial default."""
        return value is cls._DEFAULT

    def convert(self, value):
        """Check and convert the value to the specified type.

        :raises: :class:`~parameter.exception.ArgumentMissError`
        :raises: :class:`~parameter.exception.ArgumentInvalidError`
        """
        if self.is_init_default(value):
            raise ArgumentMissError(self.miss_message, self.name)

        try:
            return self.type_.convert(value)
        except ConvertError as e:
            raise ArgumentInvalidError(self.invalid_message, self.name, e)


class ModelMeta(type):
    def __new__(cls, name, base, __dict__):
        arguments = []

        for attr, val in __dict__.items():
            if not isinstance(val, Argument):
                continue

            # If not specify alias for this argument, set argument's name
            # to current attribute name.
            if val.alias is None:
                val.name = attr
            else:
                # Otherwise set to the alias that user specified
                val.name = val.alias

            arguments.append((attr, val))
        __dict__["_meta_arguments"] = arguments

        for key, _ in arguments:
            __dict__.pop(key)

        return type.__new__(cls, name, base, __dict__)


@six.add_metaclass(ModelMeta)
class Model(object):
    _meta_arguments = None       # type: list

    def __init__(self, adapter):
        """Initialize

        :param adapter:
            The adapter to get argument,
            indicate using an instance which subclasses
            :class:`BaseAdapter`
        :type adapter: :class:`BaseAdapter`
        """
        self.adapter = adapter
        self._arguments = {}
        self._init()

    def _attempt_construct_adapter(self, arg, val):
        """Attempt construct an instance which subclasses :class:`~BaseAdapter`
        if the type of arg's is :class:`~parameter.types.Nested`.
        """
        if isinstance(arg.type_, Nested):
            return self.adapter.spawn(val)
        return val

    def _init(self):
        """Initialize arguments"""
        for attr, arg in self._meta_arguments:
            if arg.multiple:
                val = self.adapter.get_arguments(arg.name)
                val = [arg.convert(self._attempt_construct_adapter(arg, v))
                       for v in val]
            else:
                val = self.adapter.get_argument(arg.name, arg.default)
                val = arg.convert(self._attempt_construct_adapter(arg, val))

            self._arguments[attr] = val

    def __getattr__(self, key):
        return self._arguments[key]

    def __repr__(self):
        args = " ".join("<%s: %s>" % (attr, arg.type_.__class__.__name__)
                        for attr, arg in self._meta_arguments)
        return "<%s [%s]>" % (self.__class__.__name__, args)
