from .restrictions import restrict_nothing, RestrictionError
from .processors import property_processor

from ..dynamic.internal_call import is_call_internal
from types import NoneType

__all__ = ["property_descriptor",
           "restricted_property",
           "locked_property",
           "function_property",
           "set_function_property",
           "restricted_set_function_property",
           "class_property",
           "read_only_class_property",
           "indirect_property",
           "read_only_indirect_property"]
          

class base_property_descriptor(object):
    """ base class for property descriptors """
    __allowed_keyword_arguments__ = ["required", "doc"]
    def __init__(self, **kwargs):
        self.required = False
        if kwargs.has_key("doc"):
            self.__doc__ = kwargs["doc"]
        else:
            self.__doc__ = ""
            
        for (k,v) in kwargs.items():
            if k in self.__allowed_keyword_arguments__:
                object.__setattr__(self, k, v)
            else:
                raise AttributeError("Argument %s is not valid for definition of %s" % (k, self))
    
    def bind_to_class(self, container, name):
        pass

    def validate_definition(self, host_cls, name):
        return True
            
class property_descriptor(base_property_descriptor):
    """ property which stores data in a (private) member variable """
    __allowed_keyword_arguments__ = ["required", "default", "locked", "preprocess", "allow_none", "doc"]
    def __init__(self, internal_member_name = None, **kwargs):
        self.__name__ = internal_member_name
        self.name = internal_member_name
        self.locked = False
        self.allow_none = False
        self.preprocess = property_processor()
        base_property_descriptor.__init__(self, **kwargs)

    def __get_default__(self):
        import inspect
        if inspect.isroutine(self.default):
            return self.default()
        else:
            return self.default
                    
    def __get__(self, obj, type = None):
        if isinstance(obj, NoneType): return self
        try:
            return obj.__dict__[self.__name__]
            #return eval("obj.%s" % self.__name__)
        except:
            if hasattr(self, "default"):
                return self.preprocess(self.__get_default__(), obj)
            elif self.allow_none:
                return None
            else:
                raise AttributeError("Attribute %s of %s is not set, and no default value is specified" % (self.name, obj))
        
    def __set__(self, obj, value):
        # override this method to do type checking or something else
        if self.locked:
            raise AttributeError("Cannot assign to locked property of '%s'" % (type(obj).__name__))
        try:
            new_value = self.preprocess(value, obj)
        except:
            if not(self.allow_none and value == None):
                raise AttributeError("Invalid assignment for Property '%s' of '%s':" % (self.name, type(obj).__name__), str(value))
            new_value = value
        #object.__setattr__(obj, self.__name__, new_value)
        obj.__dict__[self.__name__] = new_value

    def bind_to_class(self, container, name):
        self.name = name
        if self.__name__ == None:
            self.__name__ = "__prop_%s__" % name
        
    def validate_definition(self, host_cls, name):
        if not (self.required or self.allow_none or hasattr(self, "default")) :
            raise AttributeError("Property '%s' of class '%s' should be required or have a default value" % (name, host_cls.__name__))
        
        
        
class restricted_property(property_descriptor):
    __allowed_keyword_arguments__ = property_descriptor.__allowed_keyword_arguments__ + ["restriction"]
    
    def __init__(self, internal_member_name = None, **kwargs):
        self.restriction = restrict_nothing()
        property_descriptor.__init__(self, internal_member_name, **kwargs)

    def __set__(self, obj, value):
        if self.locked:
            raise AttributeError("Cannot assign to locked property of '%s'" % (type(obj).__name__))
        try:
            new_value = self.preprocess(value, obj)
        except:
            if not(self.allow_none and value == None):
                raise AttributeError("Invalid assignment for Property '%s' of '%s':" % (self.name, type(obj).__name__), str(value))
            new_value = value
        if self.restriction(new_value, obj):
            #object.__setattr__(obj, self.__name__, new_value)
            obj.__dict__[self.__name__] = new_value
        elif self.allow_none and (new_value == None or value == None):
            #object.__setattr__(obj, self.__name__, new_value)
            obj.__dict__[self.__name__] = new_value

        else:
            raise RestrictionError("%s does not match restriction %s in property %s of %s in module %s" %(value, self.restriction, self.name, obj.__class__.__name__, obj.__class__.__module__))

class locked_property(property_descriptor):
    """ lock down a property so it cannot be set from outside the class. Use it as

        class B(A):
             p = locked_property(A, "p")
             
        where A is the parent class, and p is the name of the property in A """
    def __init__(self, parent_class = None, parent_property_name = None):
        self.locked = True
        self.required = False
        self.parent_property_name = parent_property_name
        self.parent_class = parent_class
        
       
    def __get__(self, obj, type = None):
        if isinstance(obj, NoneType): return self
        return self.parent_property.__get__(obj, type)
    
    def __set__(self, obj, value):
        if is_call_internal(obj):
            return self.parent_property.__set__(obj, value)
        else:            
            raise AttributeError("Cannot assign to locked property of '%s'" % (type(obj).__name__))

    def bind_to_class(self, container, name):
        import inspect
        self.name = name
        if None == self.parent_property_name:
            self.parent_property_name = name
        if None == self.parent_class:
            mro = inspect.getmro(container)
            for C in mro:
                if hasattr(C, name):
                    self.parent_class = C

        self.parent_property = object.__getattribute__(self.parent_class, self.parent_property_name)
    
    def validate_definition(self, host_cls, name):
        if not hasattr(self.parent_class, self.parent_property_name):
            raise AttributeError("Parent class of '%s' has no property '%s' to match locked_property '%s'" % (host_cls.__name__, self.parent_property_name, self.name))
    
class lock_and_convert_property(locked_property):
    """ lock down a property so it cannot be set from outside the class. 
        If someone tries to set it, the convert function is called first
        Use it as

        class B(A):
             def convert(self):
                 #do something, e.g. convert B back to A
                 self.__class__ = A
             p = locked_property(A, "p", convert)
             
        where A is the parent class, and p is the name of the property in A .
        convert is a method of B """
        
    def __init__(self, parent_class, parent_property_name, convert_method):
        locked_property.__init__(self,parent_class, parent_property_name)
        self.convert_method = convert_method
       
    def __get__(self, obj, type = None):
        if isinstance(obj, NoneType): return self
        return self.parent_property.__get__(obj, type)
    
    def __set__(self, obj, value):
        if not is_call_internal(obj):
            self.convert_method(obj)
        return self.parent_property.__set__(obj, value)
    
        
class function_property(base_property_descriptor):
    __allowed_keyword_arguments__ = ["required"]
    def __init__(self, fget , fset , **kwargs):
        self.fget = fget
        self.fset = fset
        self.locked = False
        base_property_descriptor.__init__(self, **kwargs)
        

    def __get__(self, obj, type = None):
        if isinstance(obj, NoneType): return self
        return self.fget(obj)
    def __set__(self, obj, value):
        return self.fset(obj, value)

class set_function_property(property_descriptor):
    def __init__(self, internal_member_name, fset , **kwargs):
        self.fset = fset
        self.preprocess = property_processor()
        property_descriptor.__init__(self, internal_member_name, **kwargs)

    def __set__(self, obj, value):
        return self.fset(obj, self.preprocess(value, obj))

class restricted_set_function_property(restricted_property, set_function_property):
    def __init__(self, internal_member_name, fset, **kwargs):
        self.restriction = restrict_nothing()
        set_function_property.__init__(self, internal_member_name, fset, **kwargs)

  
    def __set__(self, obj, value):
        new_value = self.preprocess(value, obj)
        if self.restriction(new_value, obj):
            set_function_property.__set__(self, obj, new_value)
        else:
            raise RestrictionError("%s does not match restriction %s in property %s" %(value, self.restriction, self.__name__))
        
class class_property(base_property_descriptor):
    """ property that gives access to properties of the class (not the instance) that can be defined in a metaclass """
    __allowed_keyword_arguments__ = ["locked", "preprocess", "doc"]
    def __init__(self, name = None, **kwargs):
        """ name of the property in the metaclass """
        self.name = name
        base_property_descriptor.__init__(self, **kwargs)
    def __get__(self, obj, type = None):
        if isinstance(obj, NoneType): return self
        return getattr(obj.__class__, self.name)

    def __set__(self, obj, value):
        return setattr(obj.__class__, value)

    def bind_to_class(self, container, name):
        self.name = name

    
class read_only_class_property(class_property):
    """ property that gives access to properties of the class (not the instance) that can be defined in a metaclass """
    __allowed_keyword_arguments__ = ["preprocess", "doc"]
    def __init__(self, name = None, **kwargs):
        self.locked = True
        class_property.__init__(self, name = name, **kwargs)
    def __set__(self, obj, value):
        raise AttributeError("Cannot assign to a read_only class_property of '%s'" % (type(obj).__name__))
    
class indirect_property(base_property_descriptor):
    """ property that gives access to properties of an attribute of the object. if property_name is omitted, it looks for a property with the same name """
    def __init__(self, attribute_name, property_name = None, **kwargs):
        self.attribute_name = attribute_name
        self.property_name = property_name
        base_property_descriptor.__init__(self, **kwargs)
    
    def __get__(self, obj, type = None):
        if isinstance(obj, NoneType): return self
        #return obj.__dict__[self.attribute_name].__getattr__(self.property_name)
        return eval("obj.%s.%s" % (self.attribute_name, self.property_name))

    def __set__(self, obj, value):
        return getattr(obj, self.attribute_name).__setattr__(self.property_name, value)
    
    def bind_to_class(self, container, name):
        self.name = name
        self.property_name = name

    def validate_definition(self, host_cls, name):
        if not hasattr(host_cls, self.attribute_name):
            raise AttributeError("Class of '%s' has no attribute '%s' to match indirect_property '%s'" % (host_cls.__name__, self.attribute_name, self.name))
        return True
    
class read_only_indirect_property(indirect_property):
    """ property that gives access to properties of an attribute of the object """
    __allowed_keyword_arguments__ = ["preprocess", "doc"]
    def __init__(self, attribute_name, property_name = None, **kwargs):
        self.locked = True
        indirect_property.__init__(self,
                                   attribute_name = attribute_name,
                                   property_name = property_name,
                                    **kwargs)

    def __set__(self, obj, value):
        raise AttributeError("Cannot assign to a read_only indirect_property of '%s'" % (type(obj).__name__))
    