# -*- coding: utf-8 -*-
#
# this module provides django database related methods
import json

from django.db import models
from django.db.models import Case, FilteredRelation, IntegerField, Q, Sum, When

import hutils


class DynamicField:
    """ 动态域，伪 JSONField。a replacement for JSONField.

    Examples::

        class User(models.Model):
            json_data = models.CharField(max_length=1023, default='{}')
            data = DynamicField.make_field('json_data')
            is_developer = DynamicField.make_property('data', 'is_developer', bool, False)
            editor = DynamicField.make_property('data', 'editor', str, 'vim')
    """

    def __init__(self, model: models.Model, field: str):
        """
        :param model: the django model
        :param field: the database field name to store json data
        """
        self._memory_data = {}  # in-memory json data
        self._model = model
        self._field = field
        self._field_value = getattr(self._model, self._field, None)
        if not self._field_value:
            self._set_model_field()
        self._memory_data = json.loads(self._field_value)

    def _set_model_field(self):
        """ set model's json field value """
        self._field_value = hutils.format_json(self._memory_data)
        setattr(self._model, self._field, self._field_value)

    def __setattr__(self, key, value):
        if key.startswith("_"):
            return super(DynamicField, self).__setattr__(key, value)
        if value is None:  # None is default value, don't save
            self._memory_data.pop(key, None)
        else:
            self._memory_data[key] = value
        self._set_model_field()

    def __setitem__(self, key, value):
        self.__setattr__(key, value)

    def __getattr__(self, attr):
        if attr.startswith("_"):
            return super(DynamicField, self).__getattribute__(attr)
        return self._memory_data.get(attr, None)

    def __getitem__(self, item):
        return self.__getattr__(item)

    @classmethod
    def make_field(cls, field_name):
        def _wrap(obj) -> DynamicField:
            private_field_name = "_{}".format("field_name")
            if not hasattr(obj, private_field_name):
                setattr(obj, private_field_name, cls(obj, field_name))
            return getattr(obj, private_field_name)

        return property(_wrap)

    @classmethod
    def make_property(cls, field_name, property_name, type_wrapper=None, default=None):
        """
        :type field_name: str
        :type property_name: str
        :type type_wrapper: (...) -> T
        :type default: T
        :rtype: T
        """

        def _wrap(value):
            if type_wrapper is not None:
                if value is not None:
                    value = type_wrapper(value)
                else:
                    value = type_wrapper()
            return value

        return property(
            lambda obj: _wrap(getattr(getattr(obj, field_name), property_name, default)),
            lambda obj, value: setattr(getattr(obj, field_name), property_name, _wrap(value)),
        )


class ModelMixin:
    """ 集合了一些 Model 的方法。collects some model helper methods.

    Examples::

        class User(models.Model, ModelMixin):
            name = models.CharField()
            age = models.IntegerField()
        User.increase(age=1)
        User.modify(name='kevin')
    """

    def modify(self, extra_updates=(), refresh=False, **fields):
        """ 只修改指定域。specify fields to update.

        Examples::

            user.modify(age=18, extra_updates=('first_name', 'last_name'))
        """
        for field, value in fields.items():
            setattr(self, field, value)
        update_fields = list(extra_updates) + list(fields.keys())
        self.save(update_fields=update_fields)
        if refresh:
            self.refresh_from_db(fields=update_fields)

    def increase(self, extra_updates=(), **fields):
        """ 利用 F() 来修改指定域。increase fields value using F().

        Examples::

            user.increase(points=10)
        """
        fields = {field: models.F(field) + amount for field, amount in fields.items()}
        self.modify(extra_updates=extra_updates, refresh=True, **fields)

    def __str__(self):
        """ 返回一个 ID，方便查看 """
        return str(self.pk)


class QuerySetMixin:
    """ 减少代码重复，一个地方写，两个地方用 :) """

    def filter_related(self, name: str, *, include_all=False, **conditions) -> "HQuerySet":
        """ 利用 FilteredRelation 优化 Query 的方法
        官方文档参见: https://docs.djangoproject.com/en/2.1/ref/models/querysets/#filteredrelation-objects

        还有一种写法是 Manager.from_queryset, 不过那样就没有 Pycharm Django 的补全和提示了，很不好
        https://docs.djangoproject.com/en/2.1/topics/db/managers/#calling-custom-queryset-methods-from-the-manager

        Examples::

            queryset = account.followers.filter(tags__name='rap', tags__deactivated_at__isnull=True)

        Equals to::

            queryset = account.followers.filter_related('tags', name='rap')

        :param name: Django related name
        :param include_all: True to include deactivated instances
        :param conditions: real filters
        """
        filtered_name = "filtered_{}".format(name)
        key, value = conditions.popitem()
        condition = {"{}__{}".format(filtered_name, key): value}
        if not include_all:
            conditions.setdefault("deactivated_at__isnull", True)
        conditions = {"{}__{}".format(name, k): v for k, v in conditions.items()}
        return self._queryset.annotate(**{filtered_name: FilteredRelation(name, condition=Q(**conditions))}).filter(
            **condition
        )

    def filter_if_in(self, data: dict, **fields: str) -> "HQuerySet":
        """ 根据传不传值决定要不要筛选的方法

        Examples::

            queryset = queryset.filter_if_in(validated_data, name__contains='name')

        Equals to::

            if 'name' in validated_data:
                queryset = queryset.filter(name__contains=validated_data['name'])

        - 经常前端会传一些 name(required=False) 的数据
        - 以前的话，要 if 'name' in validated_data: queryset = queryset.filter(name=validated_data['name'])
        - 代码里可能会充斥大量这样的冗余判断
        - 所以我们加个小语法糖
        """
        queryset = self._queryset
        for condition, key in fields.items():
            if key in data:
                queryset = queryset.filter(**{condition: data[key]})
        return queryset

    def exclude_if_in(self, data: dict, **fields: str) -> "HQuerySet":
        """ `filter_if_in` 的反操作 """
        queryset = self._queryset
        for condition, key in fields.items():
            if key in data:
                queryset = queryset.exclude(**{condition: data[key]})
        return queryset

    def annotate_sum(self, key: str, **queries):
        """ 实现 Sum/Case/When 的一套快捷方式

        Examples::

            queryset.annotate_sum('is_followed', followers__followee=account)

        Equals to::

            queryset.annotate(is_followed=Sum(Case(When(followers__followee=account, then=1), default=0)))
        """
        queryset = self._queryset
        queries.setdefault("then", 1)
        return queryset.annotate(**{key: Sum(Case(When(**queries), default=0, output_field=IntegerField()))})

    @property
    def _queryset(self) -> "HQuerySet":
        if isinstance(self, models.Manager):
            return self.get_queryset()
        # noinspection PyTypeChecker
        return self


class HQuerySet(models.QuerySet, QuerySetMixin):
    """ HUtils Query Set """


class HManager(models.Manager, QuerySetMixin):
    """ HUtils Manager """

    def get_queryset(self):
        return HQuerySet(self.model, using=self._db, hints=self._hints)


class ExtendModelMixin(ModelMixin):
    """ 拓展表，加上增改删的三个时间 """

    class DeactivatedManager(HManager):
        def get_queryset(self):
            return super(ExtendModelMixin.DeactivatedManager, self).get_queryset().filter(deactivated_at__isnull=True)

    class Meta:
        abstract = True

    created_at = models.DateTimeField(auto_now_add=True, help_text="创建时间")
    updated_at = models.DateTimeField(auto_now=True, help_text="更新时间")
    deactivated_at = models.DateTimeField(default=None, null=True, help_text="失效时间")
    objects = DeactivatedManager()
    all_objects = HManager()

    @property
    def is_deactivated(self) -> bool:
        """ 标记软删的快捷方式 """
        return self.deactivated_at is not None

    def modify(self, *, extra_updates=(), refresh=False, **fields):
        """ 每次都更新一下 updated_at """
        if "updated_at" not in extra_updates:
            extra_updates = ("updated_at", *extra_updates)
        return super(ExtendModelMixin, self).modify(extra_updates=extra_updates, refresh=refresh, **fields)
