# -*- coding: utf-8 -*-
'''
    flask.ext.pbj
    ---------------

    This module provides support for protobuf and json formatted request and
    response data.

    :copyright: (c) 2014 by Keen Browne.
    :license: MIT/X11, see LICENSE for more details.
'''

__version_info__ = ('0', '1', '0')
__version__ = ".".join(__version_info__)
__author__ = "Keen Browne"
__license__ = "MIT/X11"
__copyright__ = "(c) 2014 by Keen Browne"
__all__ = ['api', 'json', 'protobuf']

from functools import wraps

from flask import abort, jsonify, request, Flask

from google.protobuf.internal.containers import BaseContainer
from google.protobuf.reflection import GeneratedProtocolMessageType
from google.protobuf.message import Message as ProtocolMessage, DecodeError

from werkzeug.wrappers import Response


class EncodeError(Exception):
    pass


class PbjRequest(Flask.request_class):
    def __init__(self, *args, **kwargs):
        super(PbjRequest, self).__init__(*args, **kwargs)
        self.data_dict = None

Flask.request_class = PbjRequest


# TODO: consider using the word 'decode' and 'encode' instead of copy
def copy_dict_to_pb(instance, dictionary):
    """
        Copy the key, value pairs in a dictionary to the fields of an instance
        of a protobuf message. This method assumes that key values in the
        dictionary correspond to field names in the message. Enums are not well
        supported.
    """
    assert(isinstance(dictionary, dict))
    for key, value in dictionary.iteritems():
        if value is None:
            continue
        # If the value is another dictionary set the field to the values
        # in the nested dictionary
        if isinstance(value, dict):
            attribute = getattr(instance, key)
            copy_dict_to_pb(attribute, value)
        # If the value is iterable, copy the list into the repeated field
        elif hasattr(value, "__iter__"):
            attribute = getattr(instance, key)
            if len(value) == 0 or not isinstance(value[0], dict):
                attribute.extend(value)
            else:
                for item in value:
                    copy_dict_to_pb(attribute.add(), item)
        # Otherwise the value is a basic type, so set the field directly
        else:
            setattr(instance, key, value)


def copy_pb_to_dict(dictionary, instance):

    for descriptor, value in instance.ListFields():
        # If the field is another Protobuf Message, make a new dictionary
        # and copy the messages fields
        if isinstance(value, ProtocolMessage):
            dictionary[descriptor.name] = {}
            copy_pb_to_dict(dictionary[descriptor.name], value)
        # If the field is repeated, create a list and copy the repeated field
        # values into the dictionary
        elif isinstance(value, BaseContainer):
            dictionary[descriptor.name] = []
            for item in value:
                if isinstance(item, ProtocolMessage):
                    dict_item = {}
                    copy_pb_to_dict(dict_item, item)
                    dictionary[descriptor.name].append(dict_item)
                else:
                    dictionary[descriptor.name].append(item)
        # Otherwise the field value is just a basic type and should be set
        # on the dict.
        else:
            dictionary[descriptor.name] = value


def _result_to_response_tuple(result):
    # Returned tuples are also evaluated
    if isinstance(result, tuple):
        assert(len(result) > 0 and len(result) <= 3)
        if (len(result) == 1):
            return result[0], 200, {}
        if (len(result) == 2):
            return result[0], result[1], {}
        elif (len(result) == 3):
            return result

    return result, 200, {}


class JsonDictKeyError(KeyError):
    pass


class JsonResponseDict(dict):
    def __getitem__(self, key):
        try:
            return super(JsonResponseDict, self).__getitem__(key)
        except KeyError:
            raise JsonDictKeyError(key)


class JsonCodec(object):
    mimetype = "application/json"

    def parse_request_data(self, _request):
        return JsonResponseDict(_request.get_json())

    def make_response(self, data, status_code, headers):
        response = jsonify(**data)
        return response, status_code, headers


class ProtobufCodec(object):
    mimetype = "application/x-protobuf"

    def __init__(self, sends=None, receives=None, errors=None):
        assert(sends or receives)
        if sends:
            assert(isinstance(sends, GeneratedProtocolMessageType))
        if receives:
            assert(isinstance(receives, GeneratedProtocolMessageType))
        if errors:
            assert(isinstance(errors, GeneratedProtocolMessageType))

        self.send_type = sends
        self.receive_type = receives
        self.error_type = errors

    def parse_request_data(self, _request):
        if not self.receive_type:
            abort(400)  # Bad Request
        data_dict = {}
        message = self.receive_type()
        try:
            message.ParseFromString(_request.data)
        except DecodeError:
            abort(400)
        copy_pb_to_dict(data_dict, message)

        return data_dict

    def make_response(self, data, status_code, headers):