from ray.model import Model
from google.appengine.ext.ndb import Model as AppEngineModel
from google.appengine.ext import ndb
from datetime import datetime


class GAEModel(AppEngineModel, Model):

    @classmethod
    def columns(cls):
        return sorted(cls._properties.keys())

    @classmethod
    def to_instance(cls, json_attributes):
        keys_and_properties = cls._get_keys_and_properties()

        ancestor_kind, ancestor_key = cls.ancestor()
        if not ancestor_kind:
            instance = cls()
        else:
            if ancestor_key not in json_attributes:
                raise Exception('the ancestor key was not found')

            instance = cls(parent=ndb.Key(ancestor_kind, json_attributes[ancestor_key]))
            del json_attributes[ancestor_key]

        for field_name in json_attributes.keys():
            value = json_attributes[field_name]
            if field_name in keys_and_properties:
                json_value = json_attributes[field_name]
                kind, repeated = keys_and_properties[field_name]
                if repeated:
                    value = [ndb.Key(kind, val) for val in json_value]
                else:
                    value = ndb.Key(kind, json_value)

            setattr(instance, field_name, value)
        return instance

    def put(self):
        can_save = Model.put(self)
        if can_save:
            AppEngineModel.put(self)
            return self

    def delete(self, id=None):
        can_delete = Model.delete(self)
        if not id:
            my_key = self
        else:
            my_key = self.__class__.get_by_id(int(id))

        if can_delete and my_key:
            my_key.key.delete()
            return self

    def to_json(self):
        return self.__model_to_json()

    @classmethod
    def ancestor(cls):
        """
            map the usage of the ancestor field in GAEModel

            override this method returning a tuple containing
            (ancestor_kind, json_id)
        """
        return (None, None)

    @classmethod
    def find(cls, *args, **kwargs):
        if not kwargs:
            return cls.query().fetch()

        ancestor_kind, ancestor_key = cls.ancestor()
        if not ancestor_kind and ancestor_key not in kwargs:
            query = cls.query()
        else:
            query = cls.query(ancestor=ndb.Key(ancestor_kind, kwargs[ancestor_key]))
            del kwargs[ancestor_key]

        keys_and_properties = cls._get_keys_and_properties()
        fields_to_filter = set(kwargs.keys())
        keys = set(keys_and_properties.keys())
        fields_arent_keys = fields_to_filter - keys

        for field in fields_arent_keys:
            prop = getattr(cls, field)
            value = kwargs[field]
            if type(value) is list and prop._repeated:
                query = query.filter(prop.IN(value))
            else:
                query = query.filter(prop == value)

        if bool(fields_to_filter & keys):  # check if there are keys in the fields to filter
            for key, prop in keys_and_properties.items():
                key_id = kwargs[key]
                kind, repeated = prop
                if type(key_id) is list and repeated:
                    query = query.filter(getattr(cls, key).IN(key_id))
                else:
                    query = query.filter(getattr(cls, key) == ndb.Key(kind, key_id))

        return query.fetch()

    @classmethod
    def get(cls, id=None):
        return cls.get_by_id(int(id))

    @classmethod
    def update(cls, fields_to_update):
        if 'id' not in fields_to_update:
            raise Exception('You should provide an id')

        entity = ndb.Key(cls.__name__, fields_to_update['id']).get()

        for key, value in fields_to_update.items():
            if key in entity._properties:
                value = cls.from_raw_to_type(cls._properties[key], value)
                setattr(entity, key, value)

        entity.put()
        return entity

    @classmethod
    def _get_keys_and_properties(cls):
        keys = {}
        for name, property_type in cls._properties.items():
            if isinstance(property_type, ndb.KeyProperty):
                keys[name] = property_type._kind, property_type._repeated

        return keys

    def __model_to_json(self):
        model_json = {}
        for prop in self._properties:
            value = getattr(self, prop)
            model_json[prop] = self.__class__.__from_type_to_raw_value(self._properties[prop], value)

        if 'id' in dir(self.key):
            model_json['id'] = self.key.id()

        return model_json

    @classmethod
    def from_raw_to_type(cls, field, value):
        return cls.__from_raw_to_type(field, value)

    @classmethod
    def __from_type_to_raw_value(cls, field, value):
        types = {'StringProperty': cls.__decode_str,
                 'IntegerProperty': cls.__to_int,
                 'FloatProperty': cls.__to_float,
                 'DateTimeProperty': cls._convert_date,
                 'BooleanProperty': bool,
                 'BlobKeyProperty': cls.__blob_to_url,
                 'TextProperty': cls.__decode_str,
                 'KeyProperty': cls.__key_property_to_id}

        field_type = type(field).__name__
        return types[field_type](value) if field_type in types else None

    @classmethod
    def __from_raw_to_type(cls, field, value):
        types = {'StringProperty': cls.__decode_str,
                 'IntegerProperty': cls.__to_int,
                 'FloatProperty': cls.__to_float,
                 'DateTimeProperty': cls._str_to_date,
                 'BooleanProperty': bool,
                 'TextProperty': cls.__decode_str}

        field_type = type(field).__name__
        value = cls.__decode_str(value)
        return types[field_type](value) if field_type in types else None

    @classmethod
    def _convert_date(cls, date):
        if type(date) == datetime:
            return date.strftime("%d/%m/%Y")
        else:
            return cls._str_to_date(date)

    @classmethod
    def __key_property_to_id(cls, key):
        if not key:
            return None

        if type(key) is list:
            return [k.id() for k in key]

        return key.id()

    @classmethod
    def __to_int(cls, value):
        if not value:
            return 0
        elif type(value) is list:
            return value
        return int(value)

    @classmethod
    def __to_float(cls, value):
        if not value:
            return 0.0
        elif type(value) is list:
            return value
        return float(value)

    @classmethod
    def __blob_to_url(self, blob):
        return str(blob)

    @classmethod
    def _str_to_date(cls, data):
        if not data:
            return None
        date_parts = data.split('/')
        date = (datetime(int(date_parts[2]), int(date_parts[1]), int(date_parts[0])))
        return date

    @classmethod
    def __decode_str(cls, value):
        if isinstance(value, str):
            return value
        if isinstance(value, unicode):
            return value.encode('utf-8')
        return value