from django.http import HttpResponse, HttpResponseNotAllowed
from django.shortcuts import render as _render

__author__ = 'Michael'


class RequiredParameterNotFound(Exception):
    def __init__(self, name):
        self.name = name

    def __str__(self):
        return "Required parameter '{}' not found".format(self.name)


def __get_params_def(func, types, start=0):
    argcount = func.__code__.co_argcount
    func_args = func.__code__.co_varnames[:argcount]
    defaults = len(func.__defaults__) if func.__defaults__ is not None else 0

    def __yield():
        for i in range(start, argcount):
            name = func_args[i]
            if name in types:
                type = types[name]
            else:
                type = str
            required = i + defaults < argcount
            yield name, type, required

    return list(__yield())


def __get_parameters(params_def, provider):
    parameters = {}

    for name, type, required in params_def:
        if name not in provider:
            if required:
                raise RequiredParameterNotFound(name)
            else:
                continue
        val = type(provider[name])
        parameters[name] = val

    return parameters


def view(pass_request=False, **kwargs):
    """
    Decorate a function with named parameters, rewriting it as a view.

    :param pass_request: Whether `request` should be passed as the first argument.
    :param kwargs:
        Specify types for parameters.
        Unspecified parameters will have the default type `str`.
    :return: The view.

    e.g.

    @view(x=int)
    def f(name, x):
        name is the value of request param `name`
        x is the int value of request param `x`
    """

    def __decor(func):
        params = __get_params_def(func, kwargs, 1 if pass_request else 0)

        def __view(request):
            d = getattr(request, request.method)
            parameters = __get_parameters(params, d)

            if pass_request:
                result = func(request, **parameters)
            else:
                result = func(**parameters)

            return result

        return __view

    return __decor


def render():
    """
    Decorates a view-like function, passing its return values to `django.shortcuts.render`.

    - If the return value is a str, call render(request, result).
    - If the return value is a tuple, call render(request, *result).
    - If the return value is a dict, call render(request, **result).

    e.g.

    @render()
    @view()
    def index():
        return 'index.html'
    """

    def __decor(view_func):

        def __view(request):
            result = view_func(request)

            if isinstance(result, HttpResponse):
                return result

            if isinstance(result, str):
                return _render(request, result)
            elif isinstance(result, tuple):
                return _render(request, *result)
            elif isinstance(result, dict):
                return _render(request, **result)
            else:
                raise TypeError("return type with @render must be str, tuple or dict")

        return __view

    return __decor


def combine(**kwargs):
    """
    Decorates a view-like function and creates a new view that,
    based on the request method (GET/POST),
    process the request with another view.

    If the view body is not empty, it will be called first.
    If the function returns a value that is not None,
    the value is immediately returned and no method-based view will be called.

    e.g.

    @render()
    def __get_form():
        return 'form.html'

    @view()
    def __post_form():
        do stuff here

    @combine(GET=__get_form, POST=__post_form)
    @view(id=int)
    def form(id):
        if id not in xxx:
            return HttpResponseBadRequest()
    """

    def __decor(func):
        def __view(request):
            init = func(request)
            if init is not None:
                return init

            if request.method not in kwargs:
                return HttpResponseNotAllowed(kwargs.keys())

            return kwargs[request.method](request)

        return __view

    return __decor


def method(*args):
    """
    Decorates a view, limit the request method.

    e.g.

    @method('GET')
    @render()
    @view()
    def index():
        return 'index.html'
    """

    def __decor(view_func):
        def __view(request):
            if request.method not in args:
                return HttpResponseNotAllowed(args)
            return view_func(request)

        return __view

    return __decor
