# Copyright (c) 2006,2007,2008 Mitch Garnaat http://garnaat.org/
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish, dis-
# tribute, sublicense, and/or sell copies of the Software, and to permit
# persons to whom the Software is furnished to do so, subject to the fol-
# lowing conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL-
# ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
# SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.

import datetime
from key import Key
from boto.utils import Password
from boto.sdb.db.query import Query
import re
import boto
import boto.s3.key
from boto.sdb.db.blob import Blob


class Property(object):

    data_type = str
    type_name = ''
    name = ''
    verbose_name = ''

    def __init__(self, verbose_name=None, name=None, default=None,
                 required=False, validator=None, choices=None, unique=False):
        self.verbose_name = verbose_name
        self.name = name
        self.default = default
        self.required = required
        self.validator = validator
        self.choices = choices
        if self.name:
            self.slot_name = '_' + self.name
        else:
            self.slot_name = '_'
        self.unique = unique

    def __get__(self, obj, objtype):
        if obj:
            obj.load()
            return getattr(obj, self.slot_name)
        else:
            return None

    def __set__(self, obj, value):
        self.validate(value)

        # Fire off any on_set functions
        try:
            if obj._loaded and hasattr(obj, "on_set_%s" % self.name):
                fnc = getattr(obj, "on_set_%s" % self.name)
                value = fnc(value)
        except Exception:
            boto.log.exception("Exception running on_set_%s" % self.name)

        setattr(obj, self.slot_name, value)

    def __property_config__(self, model_class, property_name):
        self.model_class = model_class
        self.name = property_name
        self.slot_name = '_' + self.name

    def default_validator(self, value):
        if isinstance(value, basestring) or value == self.default_value():
            return
        if not isinstance(value, self.data_type):
            raise TypeError('Validation Error, %s.%s expecting %s, got %s' % (self.model_class.__name__, self.name, self.data_type, type(value)))

    def default_value(self):
        return self.default

    def validate(self, value):
        if self.required and value == None:
            raise ValueError('%s is a required property' % self.name)
        if self.choices and value and not value in self.choices:
            raise ValueError('%s not a valid choice for %s.%s' % (value, self.model_class.__name__, self.name))
        if self.validator:
            self.validator(value)
        else:
            self.default_validator(value)
        return value

    def empty(self, value):
        return not value

    def get_value_for_datastore(self, model_instance):
        return getattr(model_instance, self.name)

    def make_value_from_datastore(self, value):
        return value

    def get_choices(self):
        if callable(self.choices):
            return self.choices()
        return self.choices


def validate_string(value):
    if value == None:
        return
    elif isinstance(value, str) or isinstance(value, unicode):
        if len(value) > 1024:
            raise ValueError('Length of value greater than maxlength')
    else:
        raise TypeError('Expecting String, got %s' % type(value))


class StringProperty(Property):

    type_name = 'String'

    def __init__(self, verbose_name=None, name=None, default='',
                 required=False, validator=validate_string,
                 choices=None, unique=False):
        Property.__init__(self, verbose_name, name, default, required,
                          validator, choices, unique)


class TextProperty(Property):

    type_name = 'Text'

    def __init__(self, verbose_name=None, name=None, default='',
                 required=False, validator=None, choices=None,
                 unique=False, max_length=None):
        Property.__init__(self, verbose_name, name, default, required,
                          validator, choices, unique)
        self.max_length = max_length

    def validate(self, value):
        value = super(TextProperty, self).validate(value)
        if not isinstance(value, str) and not isinstance(value, unicode):
            raise TypeError('Expecting Text, got %s' % type(value))
        if self.max_length and len(value) > self.max_length:
            raise ValueError('Length of value greater than maxlength %s' % self.max_length)


class PasswordProperty(StringProperty):
    """

    Hashed property whose original value can not be
    retrieved, but still can be compared.

    Works by storing a hash of the original value instead
    of the original value.  Once that's done all that
    can be retrieved is the hash.

    The comparison

       obj.password == 'foo'

    generates a hash of 'foo' and compares it to the
    stored hash.

    Underlying data type for hashing, storing, and comparing
    is boto.utils.Password.  The default hash function is
    defined there ( currently sha512 in most cases, md5
    where sha512 is not available )

    It's unlikely you'll ever need to use a different hash
    function, but if you do, you can control the behavior
    in one of two ways:

      1) Specifying hashfunc in PasswordProperty constructor

         import hashlib

         class MyModel(model):
             password = PasswordProperty(hashfunc=hashlib.sha224)

      2) Subclassing Password and PasswordProperty

         class SHA224Password(Password):
             hashfunc=hashlib.sha224

         class SHA224PasswordProperty(PasswordProperty):
             data_type=MyPassword
             type_name="MyPassword"

         class MyModel(Model):
             password = SHA224PasswordProperty()

    """
    data_type = Password
    type_name = 'Password'

    def __init__(self, verbose_name=None, name=None, default='', required=False,
                 validator=None, choices=None, unique=False, hashfunc=None):

        """
           The hashfunc parameter overrides the default hashfunc in boto.utils.Password.

           The remaining parameters are passed through to StringProperty.__init__"""

        StringProperty.__init__(self, verbose_name, name, default, required,
                                validator, choices, unique)
        self.hashfunc = hashfunc

    def make_value_from_datastore(self, value):
        p = self.data_type(value, hashfunc=self.hashfunc)
        return p

    def get_value_for_datastore(self, model_instance):
        value = StringProperty.get_value_for_datastore(self, model_instance)
        if value and len(value):
            return str(value)
        else:
            return None

    def __set__(self, obj, value):
        if not isinstance(value, self.data_type):
            p = self.data_type(hashfunc=self.hashfunc)
            p.set(value)
            value = p
        Property.__set__(self, obj, value)

    def __get__(self, obj, objtype):
        return self.data_type(StringProperty.__get__(self, obj, objtype), hashfunc=self.hashfunc)

    def validate(self, value):
        value = Property.validate(self, value)
        if isinstance(value, self.data_type):
            if len(value) > 1024:
                raise ValueError('Length of value greater than maxlength')
        else:
            raise TypeError('Expecting %s, got %s' % (type(self.data_type), type(value)))


class BlobProperty(Property):
    data_type = Blob
    type_name = "blob"

    def __set__(self, obj, value):
        if value != self.default_value():
            if not isinstance(value, Blob):
                oldb = self.__get__(obj, type(obj))
                id = None
                if oldb:
                    id = oldb.id
                b = Blob(value=value, id=id)
                value = b
        Property.__set__(self, obj, value)


class S3KeyProperty(Property):

    data_type = boto.s3.key.Key
    type_name = 'S3Key'
    validate_regex = "^s3:\/\/([^\/]*)\/(.*)$"

    def __init__(self, verbose_name=None, name=None, default=None,
                 required=False, validator=None, choices=None, unique=False):
        Property.__init__(self, verbose_name, name, default, required,
                          validator, choices, unique)

    def validate(self, value):
        value = super(S3KeyProperty, self).validate(value)
        if value == self.default_value() or value == str(self.default_value()):
            return self.default_value()
        if isinstance(value, self.data_type):
            return
        match = re.match(self.validate_regex, value)
        if match:
            return
        raise TypeError('Validation Error, expecting %s, got %s' % (self.data_type, type(value)))

    def __get__(self, obj, objtype):
        value = Property.__get__(self, obj, objtype)
        if value:
            if isinstance(value, self.data_type):
                return value
            match = re.match(self.validate_regex, value)
            if match:
                s3 = obj._manager.get_s3_connection()
                bucket = s3.get_bucket(match.group(1), validate=False)
                k = bucket.get_key(match.group(2))
                if not k:
                    k = bucket.new_key(match.group(2))
                    k.set_contents_from_string("")
                return k
        else:
            return value

    def get_value_for_datastore(self, model_instance):
        value = Property.get_value_for_datastore(self, model_instance)
        if value:
            return "s3://%s/%s" % (value.bucket.name, value.name)
        else:
            return None


class IntegerProperty(Property):

    data_type = int
    type_name = 'Integer'

    def __init__(self, verbose_name=None, name=None, default=0, required=False,
                 validator=None, choices=None, unique=False, max=2147483647, min=-2147483648):
        Property.__init__(self, verbose_name, name, default, required, validator, choices, unique)
        self.max = max
        self.min = min

    def validate(self, value):
        value = int(value)
        value = Property.validate(self, value)
        if value > self.max:
            raise ValueError('Maximum value is %d' % self.max)
        if value < self.min:
            raise ValueError('Minimum value is %d' % self.min)
        return value

    def empty(self, value):
        return value is None

    def __set__(self, obj, value):
        if value == "" or value == None:
            value = 0
        return Property.__set__(self, obj, value)


class LongProperty(Property):

    data_type = long
    type_name = 'Long'

    def __init__(self, verbose_name=None, name=None, default=0, required=False,
                 validator=None, choices=None, unique=False):
        Property.__init__(self, verbose_name, name, default, required, validator, choices, unique)

    def validate(self, value):
        value = long(value)
        value = Property.validate(self, value)
        min = -9223372036854775808
        max = 9223372036854775807
        if value > max:
            raise ValueError('Maximum value is %d' % max)
        if value < min:
            raise ValueError('Minimum value is %d' % min)
        return value

    def empty(self, value):
        return value is None


class BooleanProperty(Property):

    data_type = bool
    type_name = 'Boolean'

    def __init__(self, verbose_name=None, name=None, default=False, required=False,
                 validator=None, choices=None, unique=False):
        Property.__init__(self, verbose_name, name, default, required, validator, choices, unique)

    def empty(self, value):
        return value is None


class FloatProperty(Property):

    data_type = float
    type_name = 'Float'

    def __init__(self, verbose_name=None, name=None, default=0.0, required=False,
                 validator=None, choices=None, unique=False):
        Property.__init__(self, verbose_name, name, default, required, validator, choices, unique)

    def validate(self, value):
        value = float(value)
        value = Property.validate(self, value)
        return value

    def empty(self, value):
        return value is None


class DateTimeProperty(Property):
    """This class handles both the datetime.datetime object
    And the datetime.date objects. It can return either one,
    depending on the value stored in the database"""

    data_type = datetime.datetime
    type_name = 'DateTime'

    def __init__(self, verbose_name=None, auto_now=False, auto_now_add=False, name=None,
                 default=None, required=False, validator=None, choices=None, unique=False):
        Property.__init__(self, verbose_name, name, default, required, validator, choices, unique)
        self.auto_now = auto_now
        self.auto_now_add = auto_now_add

    def default_value(self):
        if self.auto_now or self.auto_now_add:
            return self.now()
        return Property.default_value(self)

    def validate(self, value):
        if value == None:
            return
        if isinstance(value, datetime.date):
            return value
        return super(DateTimeProperty, self).validate(value)

    def get_value_for_datastore(self, model_instance):
        if self.auto_now:
            setattr(model_instance, self.name, self.now())
        return Property.get_value_for_datastore(self, model_instance)

    def now(self):
        return datetime.datetime.utcnow()


class DateProperty(Property):

    data_type = datetime.date
    type_name = 'Date'

    def __init__(self, verbose_name=None, auto_now=False, auto_now_add=False, name=None,
                 default=None, required=False, validator=None, choices=None, unique=False):
        Property.__init__(self, verbose_name, name, default, required, validator, choices, unique)
        self.auto_now = auto_now
        self.auto_now_add = auto_now_add

    def default_value(self):
        if self.auto_now or self.auto_now_add:
            return self.now()
        return Property.default_value(self)

    def validate(self, value):
        value = super(DateProperty, self).validate(value)
        if value == None:
            return
        if not isinstance(value, self.data_type):
            raise TypeError('Validation Error, expecting %s, got %s' % (self.data_type, type(value)))

    def get_value_for_datastore(self, model_instance):
        if self.auto_now:
            setattr(model_instance, self.name, self.now())
        val = Property.get_value_for_datastore(self, model_instance)
        if isinstance(val, datetime.datetime):
            val = val.date()
        return val

    def now(self):
        return datetime.date.today()


class TimeProperty(Property):
    data_type = datetime.time
    type_name = 'Time'

    def __init__(self, verbose_name=None, name=None,
                 default=None, required=False, validator=None, choices=None, unique=False):
        Property.__init__(self, verbose_name, name, default, required, validator, choices, unique)

    def validate(self, value):
        value = super(TimeProperty, self).validate(value)
        if value is None:
            return
        if not isinstance(value, self.data_type):
            raise TypeError('Validation Error, expecting %s, got %s' % (self.data_type, type(value)))


class ReferenceProperty(Property):

    data_type = Key
    type_name = 'Reference'

    def __init__(self, reference_class=None, collection_name=None,
                 verbose_name=None, name=None, default=None, required=False, validator=None, choices=None, unique=False):
        Property.__init__(self, verbose_name, name, default, required, validator, choices, unique)
        self.reference_class = reference_class
        self.collection_name = collection_name

    def __get__(self, obj, objtype):
        if obj:
            value = getattr(obj, self.slot_name)
            if value == self.default_value():
                return value
            # If the value is still the UUID for the referenced object, we need to create
            # the object now that is the attribute has actually been accessed.  This lazy
            # instantiation saves unnecessary roundtrips to SimpleDB
            if isinstance(value, str) or isinstance(value, unicode):
                value = self.reference_class(value)
                setattr(obj, self.name, value)
            return value

    def __set__(self, obj, value):
        """Don't allow this object to be associated to itself
        This causes bad things to happen"""
        if value != None and (obj.id == value or (hasattr(value, "id") and obj.id == value.id)):
            raise ValueError("Can not associate an object with itself!")
        return super(ReferenceProperty, self).__set__(obj, value)

    def __property_config__(self, model_class, property_name):
        Property.__property_config__(self, model_class, property_name)
        if self.collection_name is None:
            self.collection_name = '%s_%s_set' % (model_class.__name__.lower(), self.name)
        if hasattr(self.reference_class, self.collection_name):
            raise ValueError('duplicate property: %s' % self.collection_name)
        setattr(self.reference_class, self.collection_name,
                _ReverseReferenceProperty(model_class, property_name, self.collection_name))

    def check_uuid(self, value):
        # This does a bit of hand waving to "type check" the string
        t = value.split('-')
        if len(t) != 5:
            raise ValueError

    def check_instance(self, value):
        try:
            obj_lineage = value.get_lineage()
            cls_lineage = self.reference_class.get_lineage()
            if obj_lineage.startswith(cls_lineage):
                return
            raise TypeError('%s not instance of %s' % (obj_lineage, cls_lineage))
        except:
            raise ValueError('%s is not a Model' % value)

    def validate(self, value):
        if self.validator:
            self.validator(value)
        if self.required and value == None:
            raise ValueError('%s is a required property' % self.name)
        if value == self.default_value():
            return
        if not isinstance(value, str) and not isinstance(value, unicode):
            self.check_instance(value)


class _ReverseReferenceProperty(Property):
    data_type = Query
    type_name = 'query'

    def __init__(self, model, prop, name):
        self.__model = model
        self.__property = prop
        self.collection_name = prop
        self.name = name
        self.item_type = model

    def __get__(self, model_instance, model_class):
        """Fetches collection of model instances of this collection property."""
        if model_instance is not None:
            query = Query(self.__model)
            if isinstance(self.__property, list):
                props = []
                for prop in self.__property:
                    props.append("%s =" % prop)
                return query.filter(props, model_instance)
            else:
                return query.filter(self.__property + ' =', model_instance)
        else:
            return self

    def __set__(self, model_instance, value):
        """Not possible to set a new collection."""
        raise ValueError('Virtual property is read-only')


class CalculatedProperty(Property):

    def __init__(self, verbose_name=None, name=None, default=None,
                 required=False, validator=None, choices=None,
                 calculated_type=int, unique=False, use_method=False):
        Property.__init__(self, verbose_name, name, default, required,
                          validator, choices, unique)
        self.calculated_type = calculated_type
        self.use_method = use_method

    def __get__(self, obj, objtype):
        value = self.default_value()
        if obj:
            try:
                value = getattr(obj, self.slot_name)
                if self.use_method:
                    value = value()
            except AttributeError:
                pass
        return value

    def __set__(self, obj, value):
        """Not possible to set a new AutoID."""
        pass

    def _set_direct(self, obj, value):
        if not self.use_method:
            setattr(obj, self.slot_name, value)

    def get_value_for_datastore(self, model_instance):
        if self.calculated_type in [str, int, bool]:
            value = self.__get__(model_instance, model_instance.__class__)
            return value
        else:
            return None


class ListProperty(Property):

    data_type = list
    type_name = 'List'

    def __init__(self, item_type, verbose_name=None, name=None, default=None, **kwds):
        if default is None:
            default = []
        self.item_type = item_type
        Property.__init__(self, verbose_name, name, default=default, required=True, **kwds)

    def validate(self, value):
        if self.validator:
            self.validator(value)
        if value is not None:
            if not isinstance(value, list):
                value = [value]

        if self.item_type in (int, long):
            item_type = (int, long)
        elif self.item_type in (str, unicode):
            item_type = (str, unicode)
        else:
            item_type = self.item_type

        for item in value:
            if not isinstance(item, item_type):
                if item_type == (int, long):
                    raise ValueError('Items in the %s list must all be integers.' % self.name)
                else:
                    raise ValueError('Items in the %s list must all be %s instances' %
                                     (self.name, self.item_type.__name__))
        return value

    def empty(self, value):
        return value is None

    def default_value(self):
        return list(super(ListProperty, self).default_value())

    def __set__(self, obj, value):
        """Override the set method to allow them to set the property to an instance of the item_type instead of requiring a list to be passed in"""
        if self.item_type in (int, long):
            item_type = (int, long)
        elif self.item_type in (str, unicode):
            item_type = (str, unicode)
        else:
            item_type = self.item_type
        if isinstance(value, item_type):
            value = [value]
        elif value == None:  # Override to allow them to set this to "None" to remove everything
            value = []
        return super(ListProperty, self).__set__(obj, value)


class MapProperty(Property):

    data_type = dict
    type_name = 'Map'

    def __init__(self, item_type=str, verbose_name=None, name=None, default=None, **kwds):
        if default is None:
            default = {}
        self.item_type = item_type
        Property.__init__(self, verbose_name, name, default=default, required=True, **kwds)

    def validate(self, value):
        value = super(MapProperty, self).validate(value)
        if value is not None:
            if not isinstance(value, dict):
                raise ValueError('Value must of type dict')

        if self.item_type in (int, long):
            item_type = (int, long)
        elif self.item_type in (str, unicode):
            item_type = (str, unicode)
        else:
            item_type = self.item_type

        for key in value:
            if not isinstance(value[key], item_type):
                if item_type == (int, long):
                    raise ValueError('Values in the %s Map must all be integers.' % self.name)
                else:
                    raise ValueError('Values in the %s Map must all be %s instances' %
                                     (self.name, self.item_type.__name__))
        return value

    def empty(self, value):
        return value is None

    def default_value(self):
        return {}
