import json
from collections import namedtuple

from django.views.generic import View
from django.http import Http404, HttpResponse
from django.core.paginator import Paginator, EmptyPage
from django.utils.cache import patch_cache_control, patch_vary_headers

from mimeparse import best_match, MimeTypeParseException


Attribute = namedtuple('Attribute', ('name', 'category'))
Action = namedtuple('Action', ('method', 'attributes'))


class SingleObjectMixin(object):
    model = None

    def get_object(self):
        if not getattr(self, 'obj', None):
            self.obj = self.model.objects.get(pk=self.kwargs['pk'])

        return self.obj


class Resource(View):
    uri = None
    cache_max_age = None

    def get_attributes(self):
        return {}

    def get_relations(self):
        """
        Returns a dictionary of relations, key must be a string and the value
        should be another resource or a list of resources.
        """
        return {}

    def can_embed(self, relation):
        """
        When this method returns `True` for a given relation, we will embed it in
        the response when applicable.
        """
        return True

    def get_actions(self):
        """
        Returns a dictionary of actions
        """
        return {}

    def get_uri(self):
        return self.uri

    def content_handlers(self):
        return {
            'application/json': to_json,
            'application/hal+json': to_hal,
            'application/vnd.hal+json': to_hal,
            'application/vnd.siren+json': to_siren,
        }

    def get(self, request, *args, **kwargs):
        content_type = self.determine_content_type(request)
        handlers = self.content_handlers()
        handler = handlers[str(content_type)]
        response = HttpResponse(json.dumps(handler(self)), content_type)
        patch_vary_headers(response, ['Accept'])
        if self.cache_max_age is not None:
            patch_cache_control(response, max_age=self.cache_max_age)

        if str(content_type) == 'application/json':
            # Add a Link header
            can_embed_relation = lambda relation: not self.can_embed(relation[0])
            relations = filter(can_embed_relation, self.get_relations().items())
            relation_to_link = lambda relation: '<{}>; rel="{}"'.format(relation[1].get_uri(), relation[0])
            links = list(map(relation_to_link, relations))
            if len(links) > 0:
                response['Link'] = ', '.join(links)

        if str(content_type) != 'application/vnd.siren+json':
            # Add an Allow header
            methods = ['HEAD', 'GET'] + list(map(lambda a: a.method, self.get_actions().values()))
            response['allow'] = ', '.join(methods)

        return response

    def determine_content_type(self, request):
        content_types = [
            'application/vnd.siren+json',
            'application/vnd.hal+json',
            'application/hal+json',
            'application/json',
        ]

        accept = request.META.get('HTTP_ACCEPT', '*/*')

        try:
            content_type = best_match(content_types, accept)
        except MimeTypeParseException:
            content_type = None

        if not content_type:
            return content_types[-1]

        return content_type

class CollectionResource(Resource):
    model = None
    resource = None  # A resource class that inherits from SingleObjectMixin
    relation = 'objects'
    paginate_by = 20

    def __init__(self, page=None):
        self.page = page
        super(CollectionResource, self).__init__()

    def get_uri(self):
        if self.page is not None:
            return '{}?page={}'.format(self.uri, self.page)
        return self.uri

    def get_objects(self):
        return self.model.objects.all()

    def get_paginator(self):
        return Paginator(self.get_objects(), self.paginate_by)

    def get_resources(self, objects):
        def to_resource(obj):
            resource = self.resource()
            resource.obj = obj
            resource.request = self.request
            return resource

        return list(map(to_resource, objects))

    def get_relations(self):
        paginator = self.get_paginator()

        try:
            page = paginator.page(int(self.request.GET.get('page', 1)))
        except EmptyPage:
            raise Http404()

        objects = page.object_list
        relations = {
            self.relation: self.get_resources(page)
        }

        relations['first'] = self.__class__()

        if page.has_next():
            relations['next'] = self.__class__(page.next_page_number())

        if page.has_previous():
            relations['prev'] = self.__class__(page.previous_page_number())

        if page.has_other_pages():
            relations['last'] = self.__class__(paginator.num_pages)

        return relations

    def content_handlers(self):
        """
        Override `content_handlers` to change JSON handler to return arrays
        """

        def json_handler(resource):
            relations = self.get_relations()
        handlers = super(CollectionResource, self).content_handlers()
        handlers['application/json'] = lambda resource: list(map(to_json, self.get_relations()[self.relation]))
        return handlers

    def can_embed(self, relation):
        return relation not in ('next', 'prev', 'first', 'last')


def to_json(resource):
    document = resource.get_attributes()
    document['url'] = resource.get_uri()

    for relation, related_resource in resource.get_relations().items():
        if isinstance(related_resource, list):
            if resource.can_embed(relation):
                document[relation] = list(map(to_json, related_resource))
            else:
                document[relation] = list(map(lambda r: {'url': r.get_uri()},
                                         related_resource))
        else:
            if resource.can_embed(relation):
                document[relation] = to_json(related_resource)
            else:
                document['{}_url'.format(relation)] = related_resource.get_uri()

    return document


def to_hal(resource):
    document = resource.get_attributes()
    relations = resource.get_relations()

    embed = {}
    links = {}

    for relation in relations:
        related_resource = relations[relation]

        if resource.can_embed(relation) or isinstance(related_resource, list):
            if isinstance(related_resource, list):
                embed[relation] = list(map(to_hal, related_resource))
            else:
                embed[relation] = to_hal(related_resource)
        else:
            href = related_resource.get_uri()
            links[relation] = {'href': href}

    links['self'] = {'href': resource.get_uri()}

    document['_links'] = links
    if len(embed):
        document['_embed'] = embed

    return document


def to_siren_relation(relation):
    def inner(resource):
        document = to_siren(resource)
        document['rel'] = [relation]
        return document
    return inner


def to_siren(resource):
    def to_siren_link(relation):
        def inner(resource):
            return {'rel': [relation], 'href': resource.get_uri()}
        return inner

    document = {}

    attributes = resource.get_attributes()
    if len(attributes):
        document['properties'] = attributes

    links = []
    entities = []
    for relation, related_resource in resource.get_relations().items():
        if resource.can_embed(relation):
            if isinstance(related_resource, list):
                entities += list(map(to_siren_relation(relation), related_resource))
            else:
                entity = to_siren_relation(relation)(related_resource)
                entities.append(entity)
        else:
            if isinstance(related_resource, list):
                items = list(map(to_siren_link(relation), related_resource))
                links += items
            else:
                links.append(to_siren_link(relation)(related_resource))

    links.append(to_siren_link('self')(resource))
    document['links'] = links

    if len(entities):
        document['entities'] = entities

    actions = []

    for name, action in resource.get_actions().items():
        action_dict = {
            'name': name,
            'method': action.method,
            'href': resource.get_uri(),
            'type': 'application/json',
        }

        if action.attributes:
            def to_field(attribute):
                return {
                    'name': attribute.name,
                    'type': attribute.category,
                    'title': attribute.name.capitalize(),
                }
            action_dict['fields'] = list(map(to_field, action.attributes))

        actions.append(action_dict)

    if len(actions):
        document['actions'] = actions

    return document