#

import json
import math
import re
from abc import ABC, abstractmethod

import colour
import django_tables2 as tables
# from colour import Color, web2hex
from crispy_forms.utils import flatatt
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.db.models import QuerySet
from django.forms import Media
from django.http import JsonResponse
from django.template.loader import render_to_string
from django.templatetags.l10n import localize, unlocalize
from django.utils.encoding import force_str
from django.utils.safestring import mark_safe
from django.views.generic.base import ContextMixin, TemplateResponseMixin
from django_filters import FilterSet
from django_filters.views import FilterMixin
from django_tables2 import SingleTableMixin, LazyPaginator

from customrolepermissions.mixins import RequiredPermissionMixin
from customrolepermissions.permissions import has_permission_exactly


class DashboardLayout(list):
    def __init__(self, *args):
        super().__init__(args)

    def construct(self, request, **kwargs):
        rows = []
        media = Media()

        for dashboard_row in self:  # type: DashboardRow
            row = dashboard_row.construct(request, **kwargs)
            rows.append(row)
            media = media + row['media']

        return {
            'media': media,
            'rows': rows,
        }


class DashboardRow(object):
    def __init__(self, *card_classes, css_class='', **attrs):
        self.css_class = 'dashboard-row ' + css_class
        self.card_classes = card_classes
        self.attrs = attrs
        super().__init__()

    def construct(self, request, **kwargs):
        cards = []
        cards_html = []
        media = Media(js=[
            'futplus/js/dashboard-cards.js',
        ])
        render_html = not (request.GET.get('format') == 'json')
        # ?only=card-1&only=card-2&...
        only = [o for o in map(str.lower, request.GET.getlist('only', [])) if o]

        for card_class in self.card_classes:
            card = card_class(request=request, **kwargs)
            # if card.is_permitted():
            #     if settings.DEBUG:
            #         card_class_selector = card.get_card_class_selector()
            #         if only and card_class_selector.lower() not in only:
            #             continue  # ignore
            if render_html:
                card_html = card.as_html()
                cards_html.append(card_html)
            cards.append(card)
            media = media + card.media

        return {
            'cards': cards,
            'cards_html': cards_html,
            'media': media,
            'css_class': self.css_class,
            'flat_attrs': flatatt(self.attrs),
        }


class DashboardCard(TemplateResponseMixin, ContextMixin):
    # required_permission__all = 'dashboard_view'
    model = None
    title = None
    title_mdi = None
    title_link = None
    jsonable = True

    def __init__(self, request, *args, **kwargs):
        self.request = request
        self.args = args
        self.kwargs = kwargs
        super().__init__()

    def get_request_user(self):
        return self.request.user

    @property
    def media(self):
        media = Media()
        return media

    def render_card(self, **kwargs):
        context = self.get_context_data(**kwargs)
        response = self.render_to_response(context)
        response.render()
        return response.content

    def as_html(self):
        res = self.render_card()
        # if self.is_permitted():
        #     res = self.render_card()
        # else:
        #     res = '<!-- rendering of `{}` is not permitted -->'.format(self.__class__.__name__)

        return mark_safe(force_str(res))  # nosec

    def as_json(self):
        result = self.__json__()
        # if self.is_permitted():
        #     result = self.__json__()
        # else:
        #     result = None
        return result

    def get_card_class_selector(self):
        return camecase_to_dashedcase(self.__class__.__name__)

    def __json__(self):
        result = {
            # '_name': self.get_card_class_selector(),
        }
        return result

    def get_context_data(self, **kwargs):
        if self.title and 'title' not in kwargs:
            kwargs['title'] = self.title
        if self.title_mdi and 'title_mdi' not in kwargs:
            kwargs['title_mdi'] = self.title_mdi
        if self.title_link and 'title_link' not in kwargs:
            kwargs['title_link'] = self.title_link
        if 'card_class_selector' not in kwargs:
            kwargs['card_class_selector'] = self.get_card_class_selector()
        return super().get_context_data(**kwargs)

    def is_grantall(self):
        """ check if current queryset or object is granted to request_user or not """
        return has_permission_exactly(self.get_request_user(), self.required_permission__all)

    def is_rel(self):
        """ check if current queryset or object is related to request_user or not """
        return has_permission_exactly(self.get_request_user(), self.required_permission__rel)

    def is_own(self):
        """ check if current queryset or object is owned by request_user or not """
        return has_permission_exactly(self.get_request_user(), self.required_permission__own)
    def get_queryset(self):
        if self.model:
            return self.model._meta.default_manager.all()
        return QuerySet()

class BlankDashboardCard(DashboardCard):
    required_permission__all = 'dashboard_view'
    template_name = 'generic_views/dashboard-cards/blank.html'


class CountDashboardCard(DashboardCard):
    template_name = 'generic_views/dashboard-cards/generic-counter.html'
    request_cache_variable = None  # to cache the value to self.request

    def get_context_data(self, **kwargs):
        if 'count' not in kwargs:
            kwargs['count'] = self.get_count_as_text()
        return super().get_context_data(**kwargs)

    def _get_count(self):
        return self.get_queryset().count()

    def get_count(self):
        if self.request_cache_variable:
            try:
                return getattr(self.request, self.request_cache_variable)

            except AttributeError:
                count = self._get_count()
                setattr(self.request, self.request_cache_variable, count)
                return count

        else:
            count = self._get_count()
            return count

    def get_count_as_text(self):
        return localize(self.get_count())

    def __json__(self):
        result = super().__json__()
        if 'count' not in result:
            result['count'] = self.get_count_as_text()
        return result


class RatioDashboardCard(DashboardCard):
    template_name = 'generic_views/dashboard-cards/generic-ratio.html'

    def get_context_data(self, **kwargs):
        if 'ratio' not in kwargs:
            ratio = self.get_ratio()
            kwargs['ratio'] = ratio
        return super().get_context_data(**kwargs)

    def get_ratio(self):
        count = self.get_count()
        partial_count = self.get_partial_count()
        if count == 0:
            ratio = float('nan')
        else:
            ratio = round(partial_count / count, 4)
        return ratio

    def get_queryset_partial(self):
        return self.get_queryset()

    def get_partial_count(self):
        return self.get_queryset_partial().count()

    def get_count(self):
        return self.get_queryset().count()

    def __json__(self):
        result = super().__json__()
        ratio = self.get_ratio()
        if math.isnan(ratio):
            ratio = 'nan'
        result['ratio'] = ratio
        return result


class TableDashboardCard(SingleTableMixin, DashboardCard):
    template_name = 'generic_views/dashboard-cards/generic-table.html'
    paginate_by = 10
    # paginator_class = LazyPaginator
    table_prefix = ''
    table_page_field = 'page'
    table_wrapper_css = ''
    show_result_count = False

    def get_context_data(self, **kwargs):
        kwargs = super().get_context_data(**kwargs)
        if 'table_wrapper_css' not in kwargs:
            kwargs['table_wrapper_css'] = self.table_wrapper_css
        paginator = kwargs.get('paginator')
        if self.show_result_count and paginator and not getattr(paginator, 'is_lazy', False):
            kwargs['results_count'] = paginator.count  # may evaluate query
        return kwargs

    def get_table(self, **kwargs):
        kwargs['prefix'] = self.table_prefix
        kwargs['page_field'] = self.table_page_field
        table = super().get_table(**kwargs)  # type: tables.TableBase
        return table

    remove_whitespace_re = re.compile(r'\s\s+')

    def __json__(self):
        result = super().__json__()

        table = self.get_table()
        table_data = []
        for row in table.paginated_rows:
            row_data = []
            table_data.append(row_data)
            for column, cell in row.items():
                if column.localize is None:
                    value = force_str(cell)
                elif column.localize:
                    value = localize(cell)
                else:
                    value = unlocalize(cell)
                row_data.append(value)

        # re create table because its `row_counter` are increased by above table_data
        table = self.get_table(**self.get_table_kwargs())
        table_html = render_to_string('generic_views/dashboard-cards/_table.partial.html', request=self.request,
                                      context={'table': table, })
        table_html = self.remove_whitespace_re.sub(' ', str(table_html).strip())

        result['table'] = {
            'data': table_data,
            'html': table_html,
        }
        return result


class ChartDashboardCard(DashboardCard):
    template_name = 'generic_views/dashboard-cards/generic-chart.html'
    chart_width = None
    chart_height = None
    chart_wrapper_css = ''

    def get_context_data(self, **kwargs):
        if 'chart_config' not in kwargs:
            kwargs['chart_config'] = self.get_chart_config()
        if 'chart_width' not in kwargs:
            kwargs['chart_width'] = self.chart_width
        if 'chart_height' not in kwargs:
            kwargs['chart_height'] = self.chart_height
        if 'chart_wrapper_css' not in kwargs:
            kwargs['chart_wrapper_css'] = self.chart_wrapper_css

        return super().get_context_data(**kwargs)

    def get_chart_config(self):
        return {}

    @property
    def media(self):
        return Media(
            # js=[
            #     settings.CHARTJS_JS,
            #     settings.CHARTJS_PLUGIN_EMPTY_OVERLAY_JS,
            #     settings.CHARTJS_PLUGIN_DATALABELS_JS,
            #     settings.CHARTJS_INIT_JS,
            # ],
            js=[
                "vendor/chartjs_3.5/chart.min.js",
                "vendor/chartjs_3.5/plugins/datalabels/chartjs-plugin-datalabels.js",
                "vendor/chartjs_3.5/Chart.init.js",
            ],
        )

    def get_chart_dataset_background_color(self, n, delta=0):
        return generate_colors(n + delta)[delta:]

    def get_chart_dataset_border_color(self, n, delta=0):
        return generate_colors(n + delta)[delta:]

    def __json__(self):
        result = super().__json__()
        result['chart'] = self.get_chart_config()
        return result


class CalendarDashboardCard(DashboardCard, ABC):
    template_name = 'generic_views/dashboard-cards/generic-calendar.html'
    title_mdi = 'mdi-calendar'

    @property
    def media(self):
        media = super().media + Media(
            css={
                'screen': [
                    'vendor/fullcalendar/fullcalendar.css',
                ],
            },
            js=[
                'vendor/fullcalendar/lib/moment.min.js',
                'vendor/fullcalendar/fullcalendar.min.js',
                'js/calendar-view.js',
            ]
        )
        return media

    @abstractmethod
    def __json__(self):
        if 'type' not in self.request.GET:
            return {'calendar': 'refetchEvents'}
        return []

    def get_context_data(self, **kwargs):
        if 'calendar_options_json' not in kwargs:
            kwargs['calendar_options_json'] = json.dumps(self.get_calendar_options())
        return super().get_context_data(**kwargs)

    def get_calendar_options(self):
        options = {
            'eventSources': [{
                'url': '',
                'startParam': 'start_date',
                'endParam': 'end_date',
                'data': {
                    'flat': 't',
                    'type': 'default',
                    'format': 'json',
                    'only': self.get_card_class_selector(),
                },
            }],
        }
        return options


def generate_colors(n):
    base_colors = [
        colour.Color('#ff6384'),
        colour.Color('#36a2eb'),
        colour.Color('#ffce56'),
        colour.Color('#4bc0c0'),
        colour.Color('#9966ff'),
        colour.Color('#ff9f40'),
    ]
    colors = []
    for i in range(math.ceil(n / len(base_colors))):
        ratio = 1 - i * 10 / 100  # 100%, 90%, 80%, ...
        for c in base_colors:
            c2 = colour.Color(c)
            c2.set_saturation(c.get_saturation() * ratio)
            colors.append(c2)

    return list(map(lambda c3: colour.web2hex(c3.web, force_long=True), colors))[:n]  # force 6-digits


class FilterDashboardCard(FilterMixin, DashboardCard):
    filterset_class = None
    _filterset = None

    @property
    def filterset(self) -> FilterSet:
        if self._filterset is None:
            self._filterset = self.get_filterset(self.get_filterset_class())
        self._filterset.is_bound = True  # to force create the form
        return self._filterset

    def get_filterset_kwargs(self, filterset_class):
        """ Returns the keyword arguments for instanciating the filterset. """
        kwargs = {
            'data': self.request.GET or {},
            'request': self.request,
            'queryset': None,  # wT
        }
        return kwargs

    def filter_queryset(self, queryset, *, filterset=None):
        # self._filterset = None  # there is a unknown bug that querysets are affected from pre filterset
        self.cleanup_filterset_form(filterset=filterset)
        filterset = filterset or self.filterset
        if filterset.is_valid() or not self.get_strict():
            queryset = filterset.filter_queryset(queryset)
        else:
            queryset = queryset.none()
        return queryset

    def cleanup_filterset_form(self, *, filterset=None):
        """
        In some filtersets we filter date range only once. to do this we set __filter_by_{date-field} to True
        and this was the side effect that lead to affect querysets from pre used filterset
        """
        filterset = filterset or self.filterset
        form = filterset.form
        for attr in dir(form):
            if attr.startswith('__filter_by_'):
                value = getattr(form, attr)
                if value is True:
                    delattr(form, attr)

    def get_context_data(self, **kwargs):
        if 'filter' not in kwargs:
            kwargs['filter'] = self.filterset
        return super().get_context_data(**kwargs)


class DashboardViewMixin:
    template_name = 'generic/list-view.html'
    dashboard_layout: DashboardLayout = None
    request = None
    kwargs = None

    def get_context_data(self, **kwargs):
        if 'dashboard_layout' not in kwargs:
            if self.dashboard_layout is None:
                raise ImproperlyConfigured('please set dashboard_layout on {}'.format(self.__class__.__name__))

            kwargs['dashboard_layout'] = self.dashboard_layout.construct(self.request, **self.kwargs)

        kwargs['global_row_expanded'] = 'expanded'
        # noinspection PyUnresolvedReferences
        return super().get_context_data(**kwargs)

    def render_to_response(self, context, **kwargs):
        dashboard_layout = context['dashboard_layout']

        # check for ExportMixin on DashboardCards
        for dashboard_row in dashboard_layout['rows']:
            for card in dashboard_row['cards']:
                if hasattr(card, 'export_trigger_param'):
                    export_format = self.request.GET.get(card.export_trigger_param, None)
                    if export_format and card.export_class.is_valid_format(export_format):
                        return card.create_export(export_format)

        # noinspection PyUnresolvedReferences
        return super().render_to_response(context, **kwargs)


class DashboardJsonViewMixin(DashboardViewMixin):
    request = None
    dashboard_layout: DashboardLayout = None

    def get_context_data(self, **kwargs):
        kwargs = super().get_context_data(**kwargs)
        kwargs['dashboard_layout']['updatable'] = True
        return kwargs

    def render_to_response(self, context, **kwargs):
        if self.request.GET.get('format') == 'json':
            # remove ?format=, ?_= from request.GET so they don't repeated via {% querystring %} templatetag
            self.request.GET._mutable = True
            self.request.GET.pop('format', None)
            self.request.GET.pop('_', None)  # jquery cache variable
            # ?only=card-1&only=card-2&...
            only = set(o for o in map(str.lower, self.request.GET.pop('only', [])) if o)  # pop always return list
            flat = self.request.GET.pop('flat', False)
            self.request.GET._mutable = False

            result = {}
            dashboard_layout = context['dashboard_layout']
            for dashboard_row in dashboard_layout['rows']:
                for card in dashboard_row['cards']:
                    if card.jsonable:
                        card_class_selector = card.get_card_class_selector()
                        if only and card_class_selector.lower() not in only:
                            continue  # ignore
                        card_as_json = card.as_json()
                        result[card_class_selector] = card_as_json

            safe = True
            if only and flat and len(only) == 1 and result:
                result = list(result.values())[0]
                safe = False  # result may become a list

            return JsonResponse(result, safe=safe, status=200)

        else:
            return super().render_to_response(context)


_camecase_to_snakecase_re1 = re.compile(r'(.)([A-Z][a-z]+)')
_camecase_to_snakecase_re2 = re.compile('([a-z0-9])([A-Z])')


def camecase_to_snakecase(s):
    subbed = _camecase_to_snakecase_re1.sub(r'\1_\2', s)
    return _camecase_to_snakecase_re2.sub(r'\1_\2', subbed).lower()


def camecase_to_dashedcase(s):
    subbed = _camecase_to_snakecase_re1.sub(r'\1-\2', s)
    return _camecase_to_snakecase_re2.sub(r'\1-\2', subbed).lower()


def height_16_9(width):
    return int(width / 16 * 9)


def height_16_8(width):
    return int(width / 16 * 8)


def height_panorama(width):
    return int(width / 256 * 81)


def height_4_3(width):
    return int(width / 4 * 3)
