# -*- coding: utf-8 -*-

# external imports
import typing
import json
import bson
import peewee

from peewee import AutoField, BigAutoField, Check

# parent imports
from poobrains import app, g
import poobrains.helpers
import poobrains.form


# hack; makes sure *AutoField is ignored in AutoForms
AutoField.form_widget = None
BigAutoField.form_widget = None


class BytesParamType(poobrains.form.types.ParamType):

    def convert(self, value, param, ctx):

        if isinstance(value, typing.ByteString):
            return value

        try:
            if isinstance(value, typing.AnyStr): 
                return bytes(value, 'utf-8')

            return bytes(value)

        except Exception as e:
            if type(e) is ValueError:
                raise


BYTES = BytesParamType() 


class StorableInstanceParamType(poobrains.form.types.ParamType):

    storable = None
    choices = None

    def __init__(self, storable, choices=None):

        super(StorableInstanceParamType, self).__init__()

        self.storable = storable
        self.choices = choices


    def convert(self, value, param, ctx):

        if value == '':
            return None

        if isinstance(value, self.storable):
            return value # idempotency/gigo/ducks


        try:
            instance = self.storable.load(value)

        except self.storable.DoesNotExist:

            self.fail("No such %s: %s." % (self.storable.__name__, value))

        if self.choices is not None and instance not in self.choices:
            self.fail("'%s' is not an approved choice." % value)

        return instance

poobrains.form.types.StorableInstanceParamType = StorableInstanceParamType


class ForeignKeyChoice(poobrains.form.fields.Text, metaclass=poobrains.form.fields.BoundFieldMeta):

    """
    Note: This field expects to be bound to a ForeignKeyField.
    Note: This is a FORM field, not a storage field.
    """

    #storable = None

    def __init__(self, fkfield, choices=None, **kwargs):

        related_model = fkfield.rel_model
                
        if not choices:

            choices = []

            if hasattr(related_model, 'list') and callable(related_model.list): # means we have a Storable, where not all rows are supposed to be exposed to everyone
                for choice in related_model.list('read', g.user):
                    choices.append((choice, choice.title))
            else: # means we have a non-Storable Model
                for choice in related_model.select():
                    choices.append((choice, str(choice)))

        kwargs['choices'] = choices

        kwargs['type'] = StorableInstanceParamType(related_model, choices=[choice for choice, _ in choices])

        super(ForeignKeyChoice, self).__init__(**kwargs)

poobrains.form.fields.ForeignKeyChoice = ForeignKeyChoice


class Field(peewee.Field, poobrains.helpers.ChildAware):

    form_widget = poobrains.form.fields.Text
    type = poobrains.form.types.STRING

    def __init__(self, *args, **kwargs):

        if 'form_widget' in kwargs:
            self.form_widget = kwargs.pop('form_widget')

        super(Field, self).__init__(*args, **kwargs)


    def form(self):

        if self.form_widget is None: # allows fields to be left out of forms
            return None

        kw = {}
        kw['name'] = self.name
        kw['type'] = self.type
        kw['default'] = self.default
        kw['help_text'] = self.help_text

        if self.verbose_name:
            kw['label'] = self.verbose_name
            kw['placeholder'] = self.verbose_name
        else:
            kw['placeholder'] = self.name
        
        if self.null == False and self.default is None:
            kw['required'] = True
        else:
            kw['required'] = False

        if self.choices:
            kw['choices'] = self.choices

        return self.form_widget(**kw)


class IntegerField(Field, peewee.IntegerField):
    type = poobrains.form.types.INT


class BigIntegerField(Field, peewee.BigIntegerField):
    type = poobrains.form.types.INT


class SmallIntegerField(Field, peewee.SmallIntegerField):
    type = poobrains.form.types.INT


class FloatField(Field, peewee.FloatField):
    type = poobrains.form.types.FLOAT


class DoubleField(Field, peewee.DoubleField):
    type = poobrains.form.types.FLOAT


class DecimalField(Field, peewee.DecimalField):
    type = poobrains.form.types.FLOAT


class CharField(Field, peewee.CharField):
    type = poobrains.form.types.STRING


class FixedCharField(Field, peewee.FixedCharField):
    type = poobrains.form.types.STRING


class ColorField(Field, peewee.CharField):

    form_widget = poobrains.form.fields.Color
    type = poobrains.form.types.STRING

    def __init__(self, *args, **kwargs):
        kwargs['max_length'] = 6 # Force length of color column, since it's static.
        super()(*args, **kwargs)


class TextField(Field, peewee.TextField):
    form_widget = poobrains.form.fields.TextArea
    type = poobrains.form.types.STRING


class BlobField(Field, peewee.BlobField):
    form_widget = poobrains.form.fields.File
    type = BYTES


class BitField(Field, peewee.BitField):
    type = poobrains.form.types.INT


class BigBitField(Field, peewee.BigBitField):
    type = BYTES


class UUIDField(Field, peewee.UUIDField):
    type = poobrains.form.types.UUID


class BinaryUUIDField(Field, peewee.BinaryUUIDField):
    type = BYTES


class DateField(Field, peewee.DateField):

    type = poobrains.form.types.DATE
    form_widget = poobrains.form.fields.Date


class DateTimeField(Field, peewee.DateTimeField):

    type = poobrains.form.types.DATETIME
    form_widget = poobrains.form.fields.DateTime


class ForeignKeyField(Field, peewee.ForeignKeyField):

    form_widget = ForeignKeyChoice

    def bind(self, model, name, set_attribute=True):
    
        # NOTE: type set in here because it needs a Storable class passed in 

        super(ForeignKeyField, self).bind(model, name, set_attribute=set_attribute)
        self.type = StorableInstanceParamType(self.rel_model) # basically just needed for the CLI, which checks field.type


class BooleanField(Field, peewee.BooleanField):

    form_widget = poobrains.form.fields.Checkbox
    type = poobrains.form.types.BOOL


class SerializedFieldAccessor(peewee.FieldAccessor):

    """
        Field Accessor for fields with automatically serialized values.
        Also implements caching for the deserialized values.
    """


    __cache__ = {}
    __casts__ = {
        'deserializer': lambda x: x,
        'serializer': lambda x: x
    }

    def __get__(self, instance, instance_type=None):

        if instance is None:
            return super().__get__(instance, instance_type=instance_type)

        key = id(instance)

        if not key in self.__cache__:
            self.__cache__[key] = {}

        if not self.name in self.__cache__[key]:
            self.__cache__[key][self.name] = super().__get__(instance, instance_type=instance_type)

        return self.__cache__[key][self.name]

    def __set__(self, instance, value):

        key = id(instance)

        if not key in self.__cache__:
            self.__cache__[key] = {}

        if isinstance(value, dict):
            self.__cache__[key][self.name] = value
            value = self.serialize(value)
        elif value is None:
            pass
        else:
            self.__cache__[key][self.name] = self.deserialize(value)
        super().__set__(instance, value)

    @classmethod
    def invalidate_cache(cls, instance):
        key = id(instance)
        if key in cls.__cache__:
            del(cls.__cache__[key])

    def deserialize(self, value):
        deserialized = self.__casts__['deserializer'](value)
        return self.deserialize_postprocess(deserialized)

    def deserialize_postprocess(self, value):
        return value

    def serialize(self, value):
        processed = self.serialize_preprocess(value)
        return self.__casts__['serializer'](processed)

    def serialize_preprocess(self, value):
        return value


class SerializedField(Field):

    __casts__ = {
        'serializer': lambda x: x,
        'deserializer': lambda x: x
    }

    form_widget = None

    def python_value(self, value):
        if not value is None:
            return self.python_postprocess(self.__casts__['deserializer'](value))
        return value

    def python_postprocess(self, value):
        return value

    def db_value(self, value):
        if not value is None:
            return self.__casts__['serializer'](self.db_preprocess(value))

    def db_preprocess(self, value):
        return value

class JSONField(SerializedField):

    __casts__ = {
        'serializer': json.dumps,
        'deserializer': json.loads,
    }

    type = TextField.type
    field_type = TextField.field_type

class BSONField(SerializedField):

    __casts__ = {
        'serializer': bson.dumps,
        'deserializer': bson.loads,
    }

    type = BlobField.type
    field_type = BlobField.field_type

    # optional overload of python_value
    # because psycopg2 (upstream of peewee using postgres) returns memoryview
    # which the bson implementation we use fails to handle
    if isinstance(app.db, peewee.PostgresqlDatabase):
        def python_value(self, value):
            if isinstance(value, memoryview):
                return super().python_value(value.tobytes()) # do the same as in SerializedField, but cast it to bytes first.
            return super().python_value(value)
