from __future__ import unicode_literals

import hashlib

from django.apps import apps
from django.conf import settings
from django.db import models
from django.template import loader
from django.template.response import TemplateResponse
from django.urls import reverse
from django.utils.encoding import force_text, python_2_unicode_compatible
from django.utils.translation import ugettext


@python_2_unicode_compatible
class Collection(object):
    _registry = []

    @classmethod
    def get_all(cls):
        return sorted(cls._registry, key=lambda entry: entry._order)

    def __init__(self, label, icon=None, icon_class=None, link=None, queryset=None, model=None, order=None):
        self._label = label
        self._icon = icon
        self._icon_class = icon_class
        self._link = link
        self._queryset = queryset
        self._model = model
        self._order = order or 99
        self.__class__._registry.append(self)

    def __str__(self):
        return force_text(self.label)

    def resolve(self):
        self.children = self._get_children()
        self.icon = self._icon
        self.label = self._label
        self.url = None
        if self._link:
            self.icon = getattr(self._link, 'icon', self._icon)
            self.icon_class = getattr(self._link, 'icon_class', self._icon_class)
            self.url = reverse(viewname=self._link.view, args=self._link.args)
        return ''

    def _get_children(self):
        if self._queryset:
            return self._queryset
        else:
            if self._model:
                return self._model.objects.all()


class Dashboard(object):
    _registry = {}

    @classmethod
    def get(cls, name):
        return cls._registry[name]

    def __init__(self, name, label):
        self.name = name
        self.label = label
        self.widgets = {}
        self.removed_widgets = []
        self.__class__._registry[name] = self

    def add_widget(self, widget, order=0):
        self.widgets[widget] = {'widget': widget, 'order': order}

    def get_widgets(self):
        """
        Returns a list of widgets sorted by their 'order'.
        If two or more widgets have the same 'order', sort by label.
        """
        return map(
            lambda x: x['widget'],
            filter(
                lambda x: x['widget'] not in self.removed_widgets,
                sorted(
                    self.widgets.values(),
                    key=lambda x: (x['order'], x['widget'].label)
                )
            )
        )

    def remove_widget(self, widget):
        self.removed_widgets.append(widget)

    def render(self, request):
        rendered_widgets = [widget().render(request=request) for widget in self.get_widgets()]

        return loader.render_to_string(
            template_name='dashboard/dashboard.html', context={
                'widgets': rendered_widgets
            }
        )


class BaseDashboardWidget(object):
    _registry = {}
    context = {}
    template_name = None

    @classmethod
    def get(cls, name):
        return cls._registry[name]

    @classmethod
    def get_all(cls):
        return cls._registry.items()

    @classmethod
    def register(cls, klass):
        cls._registry[klass.name] = klass

    def get_context(self):
        return self.context

    def render(self, request):
        if self.template_name:
            return loader.render_to_string(
                template_name=self.template_name, context=self.get_context(),
            )


class DashboardWidgetNumeric(BaseDashboardWidget):
    count = 0
    icon_class = None
    label = None
    link = None
    template_name = 'dashboard/numeric_widget.html'

    def get_context(self):
        return {
            'count': self.count,
            'icon_class': self.icon_class,
            'label': self.label,
            'link': self.link,
        }


@python_2_unicode_compatible
class ErrorLogNamespace(object):
    def __init__(self, name, label=None):
        self.name = name
        self.label = label or name

    def __str__(self):
        return force_text(self.label)

    def create(self, obj, result):
        obj.error_logs.create(namespace=self.name, result=result)

    def all(self):
        ErrorLogEntry = apps.get_model(
            app_label='common', model_name='ErrorLogEntry'
        )

        return ErrorLogEntry.objects.filter(namespace=self.name)


class MissingItem(object):
    _registry = []

    @classmethod
    def get_all(cls):
        return cls._registry

    def __init__(self, label, condition, description, view):
        self.label = label
        self.condition = condition
        self.description = description
        self.view = view
        self.__class__._registry.append(self)


@python_2_unicode_compatible
class ModelAttribute(object):
    _registry = {}

    @classmethod
    def get_for(cls, model):
        try:
            return cls._registry[model]
        except KeyError:
            # We were passed a model instance, try again using the model of
            # the instance

            # If we are already in the model class, exit with an error
            if model.__class__ == models.base.ModelBase:
                raise

            return cls.get_for(model=type(model))

    @classmethod
    def get_choices_for(cls, model):
        return [
            (attribute.name, attribute) for attribute in cls.get_for(model)
        ]

    @classmethod
    def get_help_text_for(cls, model, show_name=False):
        result = []
        for count, attribute in enumerate(cls.get_for(model=model), 1):
            result.append(
                '{}) {}'.format(
                    count, force_text(attribute.get_display(show_name=show_name))
                )
            )

        return ' '.join(
            [ugettext('Available attributes: \n'), '\n'.join(result)]
        )

    def __init__(self, model, name, label=None, description=None):
        self.model = model
        self.label = label
        self.name = name
        self.description = description
        self._registry.setdefault(model, [])
        self._registry[model].append(self)

    def __str__(self):
        return self.get_display()

    def get_display(self, show_name=False):
        if self.description:
            return '{} - {}'.format(
                self.name if show_name else self.label, self.description
            )
        else:
            return force_text(self.name if show_name else self.label)


class ModelField(ModelAttribute):
    """Subclass to handle model database fields"""
    _registry = {}

    @classmethod
    def get_help_text_for(cls, model, show_name=False):
        result = []
        for count, model_field in enumerate(cls.get_for(model=model), 1):
            result.append(
                '{}) {} - {}'.format(
                    count,
                    model_field.name if show_name else model_field.label,
                    model_field.description
                )
            )

        return ' '.join(
            [ugettext('Available fields: \n'), '\n'.join(result)]
        )

    def __init__(self, *args, **kwargs):
        super(ModelField, self).__init__(*args, **kwargs)
        self._final_model_verbose_name = None

        if not self.label:
            self.label = self.get_field_attribute(
                attribute='verbose_name'
            )
            if self.label != self._final_model_verbose_name:
                self.label = '{} {}'.format(
                    self._final_model_verbose_name, self.label
                )

        if not self.description:
            self.description = self.get_field_attribute(
                attribute='help_text'
            )

    def get_field_attribute(self, attribute, model=None, field_name=None):
        if not model:
            model = self.model

        if not field_name:
            field_name = self.name

        parts = field_name.split('__')
        if len(parts) > 1:
            return self.get_field_attribute(
                model=model._meta.get_field(parts[0]).related_model,
                field_name='__'.join(parts[1:]), attribute=attribute
            )
        else:
            self._final_model_verbose_name = model._meta.verbose_name
            return getattr(
                model._meta.get_field(field_name=field_name),
                attribute
            )


class ModelProperty(object):
    _registry = []

    @classmethod
    def get_for(cls, model):
        result = []

        for klass in cls._registry:
            result.extend(klass.get_for(model=model))

        return result

    @classmethod
    def get_choices_for(cls, model):
        result = []

        for klass in cls._registry:
            result.extend(klass.get_choices_for(model=model))

        return result

    @classmethod
    def get_help_text_for(cls, model, show_name=False):
        result = []

        for klass in cls._registry:
            result.append(
                klass.get_help_text_for(model=model, show_name=show_name)
            )

        return '\n'.join(result)

    @classmethod
    def register(cls, klass):
        cls._registry.append(klass)


class Package(object):
    _registry = []

    @classmethod
    def get_all(cls):
        return cls._registry

    def __init__(self, label, license_text):
        self.label = label
        self.license_text = license_text
        self.__class__._registry.append(self)


class PropertyHelper(object):
    """
    Makes adding fields using __class__.add_to_class easier.
    Each subclass must implement the `constructor` and the `get_result`
    method.
    """
    @staticmethod
    @property
    def constructor(source_object):
        return PropertyHelper(source_object)

    def __init__(self, instance):
        self.instance = instance

    def __getattr__(self, name):
        return self.get_result(name=name)

    def get_result(self, name):
        """
        The method that produces the actual result. Must be implemented
        by each subclass.
        """
        raise NotImplementedError


class Template(object):
    _registry = {}

    @classmethod
    def get(cls, name):
        return cls._registry[name]

    def __init__(self, name, template_name):
        self.name = name
        self.template_name = template_name
        self.__class__._registry[name] = self

    def get_absolute_url(self):
        return reverse('rest_api:template-detail', args=(self.name,))

    def render(self, request):
        context = {
            'home_view': settings.HOME_VIEW,
        }
        result = TemplateResponse(
            request=request,
            template=self.template_name,
            context=context,
        ).render()

        self.html = result.content
        self.hex_hash = hashlib.sha256(result.content).hexdigest()
        return self


ModelProperty.register(ModelAttribute)
ModelProperty.register(ModelField)
