import logging
from decimal import Decimal

from django.core import exceptions
from django.db import models
from django.utils.translation import ugettext_lazy
from idmap.models import IdMapModelBase

from otree.common import expand_choice_tuples, get_app_label_from_import_path
from otree.constants import field_required_msg
from otree.currency import Currency, RealWorldCurrency
from .idmap import IdMapModel
from django.forms import widgets as dj_widgets
from .serializedfields import _PickleField  # noqa

logger = logging.getLogger(__name__)


class OTreeModelBase(IdMapModelBase):
    def __new__(mcs, name, bases, attrs):
        meta = attrs.get("Meta")
        module = attrs.get("__module__")
        is_concrete = not getattr(meta, "abstract", False)
        app_label = getattr(meta, "app_label", "")

        if is_concrete and module and not app_label:
            if meta is None:
                meta = type("Meta", (), {})
            app_label = get_app_label_from_import_path(module)
            meta.app_label = app_label
            meta.db_table = "{}_{}".format(app_label, name.lower())
            # i think needs to be here even though it's set on base model,
            # because meta is not inherited (but not tested this)
            meta.use_strong_refs = True
            attrs["Meta"] = meta

        new_class = super().__new__(mcs, name, bases, attrs)
        if not hasattr(new_class._meta, 'use_strong_refs'):
            new_class._meta.use_strong_refs = False

        for f in new_class._meta.fields:
            if hasattr(new_class, f.name + '_choices'):
                attr_name = 'get_%s_display' % f.name
                setattr(new_class, attr_name, make_get_display(f))

        new_class._setattr_fields = frozenset(f.name for f in new_class._meta.fields)
        new_class._setattr_attributes = frozenset(dir(new_class))

        return new_class


def make_get_display(field):
    def get_FIELD_display(self):
        choices = getattr(self, field.name + '_choices')()
        value = getattr(self, field.attname)
        return dict(expand_choice_tuples(choices))[value]

    return get_FIELD_display


class OTreeModel(IdMapModel, metaclass=OTreeModelBase):
    class Meta:
        abstract = True

    def __repr__(self):
        return '<{} pk={}>'.format(self.__class__.__name__, self.pk)

    _is_frozen = False
    NoneType = type(None)

    _setattr_datatypes = {
        # first value should be the "recommmended" datatype,
        # because that's what we recommend in the error message.
        # it seems the habit of setting boolean values to 0 or 1
        # is very common. that's even what oTree shows in the "live update"
        # view, and the data export.
        'BooleanField': (bool, int, NoneType),
        # forms seem to save Decimal to CurrencyField
        'CurrencyField': (Currency, NoneType, int, float, Decimal),
        'FloatField': (float, NoneType, int),
        'IntegerField': (int, NoneType),
        'StringField': (str, NoneType),
        'LongStringField': (str, NoneType),
    }
    _setattr_whitelist = {
        '_initial_prep_values',
        # used by Prefetch.
        '_ordered_players',
        '_is_frozen',
        # extras on 2018-11-24
        'id',
        '_changed_fields',
        'pk',
    }

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # cache it for performance
        self._super_setattr = super().__setattr__
        # originally I had:
        # self._dir_attributes = set(dir(self))
        # but this tripled memory usage when creating a session
        self._is_frozen = True

    def __setattr__(self, field_name: str, value):
        if self._is_frozen:

            if field_name in self._setattr_fields:
                # hasattr() cannot be used inside
                # a Django model's setattr, as I discovered.
                field = self._meta.get_field(field_name)

                field_type_name = type(field).__name__
                if field_type_name in self._setattr_datatypes:
                    allowed_types = self._setattr_datatypes[field_type_name]
                    if (
                        isinstance(value, allowed_types)
                        # numpy uses its own datatypes, e.g. numpy._bool,
                        # which doesn't inherit from python bool.
                        or 'numpy' in str(type(value))
                        # 2018-07-18:
                        # have an exception for the bug in the 'quiz' sample game
                        # after a while, we can remove this
                        or field_name == 'question_id'
                    ):
                        pass
                    else:
                        msg = ('{} should be set to {}, not {}.').format(
                            field_type_name,
                            allowed_types[0].__name__,
                            type(value).__name__,
                        )
                        raise TypeError(msg)
            elif (
                field_name in self._setattr_attributes
                or field_name in self._setattr_whitelist
                or
                # idmap uses _group_cache, _subsession_cache,
                # _prefetched_objects_cache, etc
                field_name.endswith('_cache')
            ):
                # django sometimes reassigns to non-field attributes that
                # were set before the class was frozen, such as
                # .pk and ._changed_fields (from SaveTheChange)
                # or assigning to a property like Player.payoff
                pass
            else:
                msg = ('{} has no field "{}".').format(
                    self.__class__.__name__, field_name
                )
                raise AttributeError(msg)

            self._super_setattr(field_name, value)
        else:
            # super() is a bit slower but only gets run during __init__
            super().__setattr__(field_name, value)

    def save(self, *args, **kwargs):
        # Use with FieldTracker
        if self.pk and hasattr(self, '_ft') and 'update_fields' not in kwargs:
            kwargs['update_fields'] = [k for k in self._ft.changed()]
        super().save(*args, **kwargs)


def fix_choices_arg(kwargs):
    '''allows the programmer to define choices as a list of values rather
    than (value, display_value)

    '''
    choices = kwargs.get('choices')
    if not choices:
        return
    choices = expand_choice_tuples(choices)
    kwargs['choices'] = choices


class _OtreeModelFieldMixin:
    def __init__(
        self,
        *,
        initial=None,
        label=None,
        min=None,
        max=None,
        doc='',
        widget=None,
        **kwargs,
    ):

        self.widget = widget
        self.doc = doc
        self.min = min
        self.max = max

        fix_choices_arg(kwargs)

        kwargs.setdefault('help_text', '')
        kwargs.setdefault('null', True)

        # to be more consistent with {% formfield %}
        # this is more intuitive for newbies
        kwargs.setdefault('verbose_name', label)

        # "initial" is an alias for default. in the context of oTree, 'initial'
        # is a more intuitive name. (since the user never instantiates objects
        # themselves. also, "default" could be misleading -- people could think
        # it's the default choice in the form
        kwargs.setdefault('default', initial)

        # if default=None, Django will omit the blank choice from form widget
        # https://code.djangoproject.com/ticket/10792
        # that is contrary to the way oTree views blank/None values, so to
        # correct for this, we get rid of default=None args.
        # setting null=True already should make the field null
        if 'default' in kwargs and kwargs['default'] is None:
            kwargs.pop('default')

        super().__init__(**kwargs)

    def formfield(self, **kwargs):
        if self.widget:
            kwargs['widget'] = self.widget
        return super().formfield(**kwargs)


class _OtreeNumericFieldMixin(_OtreeModelFieldMixin):
    auto_submit_default = 0


class BaseCurrencyField(_OtreeNumericFieldMixin, models.DecimalField):

    MONEY_CLASS = None  # need to set in subclasses

    def __init__(self, **kwargs):
        # i think it's sufficient just to store a high number;
        # this needs to be higher than decimal_places
        decimal_places = self.MONEY_CLASS.get_num_decimal_places()
        # where does this come from?
        max_digits = 12
        super().__init__(max_digits=max_digits, decimal_places=decimal_places, **kwargs)

    def deconstruct(self):
        name, path, args, kwargs = super().deconstruct()
        # 2017-09-24: why do we need to do this?
        del kwargs["decimal_places"]
        del kwargs["max_digits"]
        return name, path, args, kwargs

    def to_python(self, value):
        value = models.DecimalField.to_python(self, value)
        if value is None:
            return value

        return self.MONEY_CLASS(value)

    def get_prep_value(self, value):
        if value is None:
            return None
        return Decimal(self.to_python(value))

    def from_db_value(self, value, expression, connection):
        return self.to_python(value)


class CurrencyField(BaseCurrencyField):
    MONEY_CLASS = Currency
    auto_submit_default = Currency(0)

    def formfield(self, **kwargs):
        import otree.forms

        defaults = {
            'form_class': otree.forms.CurrencyField,
            'choices_form_class': otree.forms.CurrencyChoiceField,
        }
        defaults.update(kwargs)
        return super().formfield(**defaults)


class RealWorldCurrencyField(BaseCurrencyField):
    MONEY_CLASS = RealWorldCurrency
    auto_submit_default = RealWorldCurrency(0)

    def formfield(self, **kwargs):
        import otree.forms

        defaults = {
            'form_class': otree.forms.RealWorldCurrencyField,
            'choices_form_class': otree.forms.CurrencyChoiceField,
        }
        defaults.update(kwargs)
        return super().formfield(**defaults)


class BooleanField(_OtreeModelFieldMixin, models.BooleanField):
    def __init__(self, **kwargs):
        # usually checkbox is not required, except for consent forms.
        widget = kwargs.get('widget')
        if isinstance(widget, dj_widgets.CheckboxInput):
            kwargs.setdefault('blank', True)

        # we need to set explicitly because otherwise the empty choice will show up as
        # "Unknown" in a select widget. This makes it set to '-----------'.
        kwargs.setdefault(
            'choices', [(True, ugettext_lazy('Yes')), (False, ugettext_lazy('No'))]
        )
        super().__init__(**kwargs)

    auto_submit_default = False


class StringField(_OtreeModelFieldMixin, models.CharField):
    '''
    Many people are already using initial=None, and i don't think it's
    causing any problems, even though Django recommends against that, but
    that's for forms on pages that get viewed multiple times
    '''

    def __init__(
        self,
        *,
        # varchar max length doesn't affect performance or even storage
        # size; it's just for validation. so, to be easy to use,
        # there is no reason for oTree to set a short default length
        # for CharFields. The main consideration is that MySQL cannot index
        # varchar longer than 255 chars, but that is not relevant here
        # because oTree only uses indexes for fields defined in otree-core,
        # which have explicit max_lengths anyway.
        max_length=10000,
        **kwargs,
    ):

        super().__init__(max_length=max_length, **kwargs)

    auto_submit_default = ''


# currently it's just a regular model, but we might customize it in the future
# (e.g. custom model manager)
ExtraModel = models.Model


class Link(models.ForeignKey):
    def __init__(self, to):
        # should we allow it to be null? That could be useful for network games.
        # or for games where we create resources that are initially unclaimed by users.
        # it's necessary for devserver auto migrations. if you add or rename a Link field,
        # the existing records must have null.
        # the biggest concern is that
        # people will forget to pass player or group.
        # but i guess i have no choice. anyway this is more advanced functionality.
        kwargs = dict(to=to, on_delete=models.CASCADE, null=True)
        # don't make reverse relation, then we don't have to worry about conflicting
        # related_name error.
        super().__init__(related_name="+", **kwargs)

    def deconstruct(self):
        name, path, args, kwargs = super().deconstruct()
        new_kwargs = dict(to=kwargs['to'])
        return name, path, (), new_kwargs

    def formfield(self, *args, **kwargs):
        raise Exception('oTree Link cannot be used as formfield')


class DecimalField(_OtreeNumericFieldMixin, models.DecimalField):
    pass


Model = models.Model
ForeignKey = models.ForeignKey


class FloatField(_OtreeNumericFieldMixin, models.FloatField):
    pass


class IntegerField(_OtreeNumericFieldMixin, models.IntegerField):
    pass


class PositiveIntegerField(_OtreeNumericFieldMixin, models.PositiveIntegerField):
    pass


class LongStringField(_OtreeModelFieldMixin, models.TextField):
    auto_submit_default = ''


MSG_DEPRECATED_FIELD = """
{} does not exist in oTree. 
You should either replace it with one of oTree's field types, or import it from Django directly.
""".replace(
    '\n', ' '
)


def make_deprecated_field(FieldName):
    def DeprecatedField(*args, **kwargs):
        # putting the msg on a separate line gives better tracebacks
        raise Exception(MSG_DEPRECATED_FIELD.format(FieldName))

    return DeprecatedField


ManyToOneRel = make_deprecated_field("ManyToOneRel")
ManyToManyField = make_deprecated_field("ManyToManyField")
OneToOneField = make_deprecated_field("OneToOneField")
AutoField = make_deprecated_field("AutoField")
BigIntegerField = make_deprecated_field("BigIntegerField")
BinaryField = make_deprecated_field("BinaryField")
EmailField = make_deprecated_field("EmailField")
FileField = make_deprecated_field("FileField")
GenericIPAddressField = make_deprecated_field("GenericIPAddressField")
PositiveSmallIntegerField = make_deprecated_field("PositiveSmallIntegerField")
SlugField = make_deprecated_field("SlugField")
SmallIntegerField = make_deprecated_field("SmallIntegerField")
TimeField = make_deprecated_field("TimeField")
URLField = make_deprecated_field("URLField")
DateField = make_deprecated_field("DateField")
DateTimeField = make_deprecated_field("DateTimeField")


CharField = StringField
TextField = LongStringField
# keep ForeignKey around


CASCADE = models.CASCADE
