#
# Copyright (c) 2009 Nokia Corporation and/or its subsidiary(-ies).
# All rights reserved.
# This component and the accompanying materials are made available
# under the terms of "Eclipse Public License v1.0"
# which accompanies this distribution, and is available
# at the URL "http://www.eclipse.org/legal/epl-v10.html".
#
# Initial Contributors:
# Nokia Corporation - initial contribution.
#
# Contributors:
#
# Description: 
#

"""
Container classes.
Mainly internal classed that the public data model uses internally.
"""

import re
import pickle
import logging
import utils, exceptions

def object_container_filter(obj,**kwargs):
    """ Create a list of filter functions for each argument """ 
    filters=[]
    if kwargs.has_key('name'):
        filters.append(lambda x: re.match(kwargs.get('name'), x._name))
    if kwargs.has_key('path'):
        filters.append(lambda x: re.match(kwargs.get('path'), x._path()))
    if kwargs.has_key('type'):
        filters.append(lambda x: isinstance(x, kwargs.get('type')))
    if kwargs.has_key('filters'):
        filters += kwargs.get('filters')
    ret = []
    for sobj in utils.get_list(obj):
        if utils.filter(obj,filters):
            ret.append(sobj)
        
    return ret

def _apply_filter(obj,filters):
    """ Create a list of filter functions for each argument """ 
    if utils.filter(obj,filters):
        return [obj]
    else:
        return []

""" object container adding policies """
REPLACE = 0
APPEND = 1
PREPEND = 2
ERROR = 3

class DataContainer(object):
    """
    Class for data containers. 
    Container is a data storage that can hold several keys, where each key is unique. Each key however 
    can hold several values, where the active value is the last one added. 
    
    Example:
    data = {'key1' :[1,2,3,4], 
            'key2' :['foo','bar],
            'key3' :['testing'],
            'path/to/key' :['some','value','in','here','too']}
    
    The active values for keys are the last ones in the array. E.g. key1 = 4.
    """
    def __init__(self):
        self.data = {}
        
    def list_keys(self):
        """
        List all keys of the DataStorage.
        """
        return self.data.keys()
    
    def add_value(self,key,value):
        """
        Add the value as a topmost item for the given key.
        @param key: name for the key to store the data.
        @param value: the value to store.  
        """
        if self.data.has_key(key):
            self.data[key].append(value)
        else:
            self.data[key] = [value]
        return
    
    def remove_value(self,key,value):
        """
        remove individual value of the key value array
        """
        self.data[key].remove(value)
        return 
    
    def remove_key(self,key):
        del self.data[key]
        return
    
    def get_value(self,key):
        """
        self.data = {'key1' :[1,2,3,4], 
                    'key2' :['foo','bar],
                    'key3' :['testing'],
                    'path/to/key' :['some','value','in','here','too']}
        self.get_value('key1')
        4
        """
        return self.data[key][-1]
        
    def get_values(self,key):
        """
        return a copy of data values inside the container
        """
        values = []
        values.extend(self.data[key])
        return values

    def flatten(self):
        """
        return a new dictionary of the DataContainer data with only single values for each key, 
        instead of the array of values.
        """
        rest = {}
        for key in self.data.keys():
            rest[key] = self.get_value(key)
        return rest

    def clear(self):
        """
        Remove all data from the container.
        """
        return self.data.clear()

class ContainerBase(object):
    
    def _set_parent(self, newparent):
        """
        @param newparent:  The new parent object
        @return: None
        """
        self._parent = newparent

    def _get_parent(self):
        """
        @return: existing parent object
        """
        return self._parent
    
    def _del_parent(self):
        """
        Set the current parent to None
        """
        self._parent = None

    parent = property(_get_parent, _set_parent,_del_parent)


class ObjectProxy(ContainerBase):
    """
    An object proxy class. The ObjectProxy overrides the python builtin methdo __getattr__
    to redirect any function/member access to the subobject.  
    """
    def __init__(self,obj=None):
        """
        """
        self._obj  = obj
        self._parent = None
    
    def __getattr__(self,name):
        """
        direct all not found attribute calls to the sub object getattr
        """
        return getattr(self._obj,name)

    
#    def _set_parent(self, newparent):
#        """
#        @param newparent:  The new parent object
#        @return: None
#        """
#        self._parent = newparent
#        if isinstance(self._obj, ContainerBase):
#            self._obj._set_parent(newparent)

class LoadInterface(ContainerBase):
    def load(self,ref):
        file = open(ref,"r")
        self._parent = None
        return pickle.load(file)

    def unload(self,ref, obj):
        """
        unload or release
        """
        file = open(ref,"w")
        pickle.dump(obj,file)
        file.close()

    def get_path(self):
        """
        Return the path of the configuration resource
        """
        return ""

class LoadProxy(ContainerBase):
    """
    This class is meant for loading & unloading an object, when it need.
    """
    def __init__(self, path, store_interface=None):
        """
        @param path: the path which is used in loadin
        @param store_interface: the loading interface object, which is used. 
        Expects load(path) and dump(obj) functions  
        """
        self.set('_obj', None)
        self.set('_parent', None)
        self.set('path', path)
        self.set('_storeint', store_interface)

    def __getattr__(self,name):
        """
        direct all not found attribute calls to the sub object getattr
        """
        if not self._obj: 
            self._load()
        return getattr(self._obj,name)

    def __setattr__(self, name, value):
        """
        direct attribute setting calls to the sub object setattr
        """
        if not self._obj: 
            self._load()
        setattr(self._obj,name,value)

    def __delattr__(self, name):
        """
        direct attribute setting calls to the sub object setattr
        """
        if not self._obj: 
            self._load()
        delattr(self._obj,name)

    def _set_parent(self, newparent):
        """
        @param newparent:  The new parent object
        @return: None
        """
        self.set('_parent',newparent)
        if self._obj:
            self._obj._parent = self._parent

    def _set_obj(self, obj):
        self.set('_obj',obj)
        # set the same _parent for the actual object as is stored for the proxy
        self._obj._parent = self._parent
        self._obj.set_path(self.path)

    def _get_obj(self):
        if not self._obj: 
            self._load()
        return self._obj

    def _load(self):
        # Should the loading of layer external resources be supported?
        # E.g. resources with absolute path relative to the storage (starts with slash)
        """ If the loading of the object fails => Raise an InvalidObject exception """ 
        try:
            obj = self._store_interface().load(self.fullpath)
            self._set_obj(obj)
            obj.set_ref(utils.resourceref.to_objref(self.path))
        except exceptions.NotResource,e:
            logging.getLogger('cone').warning("Loading %s from parent %s failed! %s" % (self.path,self.get_parent_path(), e))
            raise exceptions.InvalidObject("Invalid configuration object %s" % self.path)

    def _unload(self):
        if self._obj:
            self._store_interface().unload(self.fullpath, self._obj)
            self.set('_obj',None)

    def _store_interface(self):
        if not self._storeint:
            self.set('_storeint',self._parent.get_project())
        return self._storeint

    def set(self,name,value):
        """
        Proxy has a specific attribute setting function, because by default all attributes are 
        stored to the actual proxy object  
        """
        self.__dict__[name] = value

    def get(self,name):
        """
        Proxy has also a specific attribute getting function, because by default all attributes are 
        stored to the actual proxy object  
        """
        return self.__dict__[name]

    def save(self):
        if hasattr(self._obj,'save'):
            self._obj.save()
        self._unload()

    def close(self):
        if hasattr(self._obj,'close'):
            self._obj.close()

    def get_parent_path(self):
        """
        Return the path of the configuration resource
        """
        if self._parent:
            return utils.resourceref.get_path(self._parent.get_path())
        else:
            return ""

    def get_path(self):
        """
        Return the path of the configuration resource
        """
        if self._obj:
            return self._obj.get_path()
        else:
            return self.path

    @property
    def fullpath(self):
        """
        Return the path of the configuration resource
        """
        try:
            return self._obj.get_full_path() 
        except AttributeError:
            parent_path = self.get_parent_path() 
            return utils.resourceref.join_refs([parent_path,self.path])

class ObjectContainer(ContainerBase):
    """
    An object container class. The ObjectContainer is actually a Tree data structure. Any ObjectContainer 
    instance can include any number of children, that must be instances of ObjectContainer.  
    """
    def __init__(self,name="",**kwargs):
        """
        """
        if len(name.split(".")) > 1 or len(name.split("/")) > 1:
            raise exceptions.InvalidRef("Illegal name for ObjectContainer %s" % name)
        self._name = name
        self._parent = None
        self._order = []
        self._children = {}
        for arg in kwargs.keys():
            setattr(self, arg, kwargs.get(arg))

    def __getattr__(self,name):
        """
        direct all not found attribute calls to the sub object getattr
        """
        try:
            return self.__dict__['_children'][name]
        except KeyError:
            return getattr(super(ObjectContainer),name)

    def _path(self, toparent=None):
        """
        Get the path to this ObjectContainer.
        @param toparent: the _parent object up to which the path is relative. Default value is None.,
        which gives the fully qualified path
        @return: The path to the ObjectContainer from toparent
        """
        if self == toparent:
            return ""
        elif self._parent and self._parent != toparent:
            # return the path with list index if the given element is in a list
            if utils.is_list(self.parent._get(self._name)):
                return self._parent._path(toparent)+"."+"%s[%s]" % (self._name,self.get_index())
            else:
                return self._parent._path(toparent)+"."+self._name
        else:
            return self._name
    
    def _add(self, child, policy=REPLACE):
        """
        Add a child object. 
        @param child: The child object to add. The child needs to be an instance of ObjectContainer. 
        @param policy: The policy which is used when an object with same name exists already  
        """
        # check that the child is a supported type
        if not self._supported_type(child):
            raise exceptions.IncorrectClassError("Cannot add instance of %s to %s." % (child.__class__,self.__class__))
        if policy == REPLACE:
            self._replace(child)
        elif policy == ERROR:
            self._error(child)
        elif policy == APPEND:
            self._append(child)
        elif policy == PREPEND:
            self._prepend(child)

    def _append(self, child):
        """
        Add the given child to the proper key. Create a list entry if necessary
        """
        child._set_parent(self)
        if not self._children.has_key(child._name):
            # skip all internal objects (that start with _)
            if not child._name.startswith('?'):
                self._order.append(child._name)
            self._children[child._name] = child
        else:
            """ Create a list under the child name """
            self._children[child._name] = utils.add_list(self._children[child._name], child)
        return

    def _prepend(self, child):
        """
        Add the given child to the proper key. Create a list entry if necessary
        """
        child._set_parent(self)
        if not self._children.has_key(child._name):
            # skip all internal objects (that start with _)
            if not child._name.startswith('?'):
                self._order.insert(0,child._name)
            self._children[child._name] = child
        else:
            """ Create a list under the child name """
            self._children[child._name] = utils.prepend_list(self._children[child._name], child)
        return

    def _replace(self, child):
        """
        If the given child already exists => Replace the child, 
        but maintain the current children of that child 
        """
        child._set_parent(self)
        # skip all internal objects (that start with _)
        if not self._children.has_key(child._name):
            if not child._name.startswith('?'):
                self._order.append(child._name)
        else:
            """ if the existing child is a instance of ObjectContainer, 
                add all children of the existing contianer to this new object """
            existingchild = self._children[child._name]
            if isinstance(existingchild, ObjectContainer):
                for subchild in existingchild._objects():
                     child._add(subchild)
        
        self._children[child._name] = child
        return

    def _error(self, child):
        """
        If the given child already exists => raise an exception.
        @raise exceptions.AlreadyExists: 
        """
        child._set_parent(self)
        if not self._children.has_key(child._name):
            # skip all internal objects (that start with _)
            if not child._name.startswith('?'):
                self._order.insert(0,child._name)
            self._children[child._name] = child
        else:
            raise exceptions.AlreadyExists('Child %s already exists' % child._name)
        return

    def _add_to_path(self, path, child, policy=REPLACE):
        """
        Add a child object.
        @param path: the path for the object
        @param child: The child object to add 
        @param namespace: The namespace of the object, which defines where the object is created  
        """
        # check that the child is a supported type
        if not self._supported_type(child):
            raise exceptions.IncorrectClassError("Cannot add instance of %s to %s Container" % (child.__class__,self.__class__))
        # ensure that the elements to the namespace exist  
        curelem = self
        for ppath in utils.dottedref.split_ref(path):
            
            if not curelem._children.has_key(ppath):
                # Create missing elem
                curelem._add(self._default_object(ppath))
            curelem = curelem._get(ppath)
        curelem._add(child,policy)

    def _get(self, path):
        """
        Get a child object by it path.
        @return: The child object if it is found. 
        @raise NotFound: when object is not found from the children.  
        """
        
        try:
            # traverse to the actual child element 
            curelem = self
            for pathelem in utils.dottedref.split_ref(path):
                if utils.dottedref.get_index(pathelem) == None:
                    curelem = curelem._children[pathelem]
                else:
                    # If the given pathelem is referring to a list 
                    name = utils.dottedref.get_name(pathelem)
                    index = utils.dottedref.get_index(pathelem)
                    curelem = utils.get_list(curelem._children[name])[index]
            return curelem
        # Catch the KeyError exception from dict and IndexError from list
        except (KeyError,IndexError): 
            raise exceptions.NotFound("Child %s not found!" % path)

    def _has(self, path):
        """
        Returns True if an element under the path is found.
        @return: Boolean value. 
        """
        
        try:
            # traverse to the actual child element 
            curelem = self
            for pathelem in utils.dottedref.split_ref(path):
                curelem = curelem._children[pathelem]
            return True
        except KeyError: 
            return False

    def _remove(self, path):
        """
        Remove a child object by it path.
        """
        # if the patherence is a long patherence (dotted name)
        # first get the _parent object and call the remove to the _parent
        (parentref,name) = utils.dottedref.psplit_ref(path)
        if parentref != "":
            self._get(parentref)._remove(name)
        elif utils.dottedref.get_index(path) != None and \
             self._get(utils.dottedref.get_name(path)):
            # Delete If the given pathelem is referring to a list 
            name = utils.dottedref.get_name(path)
            index = utils.dottedref.get_index(path)
            del self._children[name][index]
            if len(self._children[name]) == 0:
                del self._order[self._order.index(name)]
        elif self._get(path) != None: # delete if the child is found
            del self._children[path]
            del self._order[self._order.index(path)]
            
        else:
            raise exceptions.NotFound("Child %s not found!" % path)

    def _list_traverse(self,**kwargs):
        """
        Return a list of all children paths. This function calls internally __traverse__, see it for 
        more details.
        @return: an unordered list of children paths. The path is relative to this node. 
        """
        return [child._path(self) for child in self._traverse(**kwargs)]

    def _traverse(self, **kwargs):
        """
        The traverse goes recursively through the tree of children of this node and returns a result set as list.
        Arguments can be passed to it to filter out elements of the result set. All arguments are 
        given as dict, so they must be given with name. E.g. _traverse(name='test')
        @param name: The node name or part of name which is used as a filter. This is a regular expression (uses internally re.match()) 
        @param path: The path name or part of name which is used as a filter. This is a regular expression (uses internally re.match())
        @param filters: A list of predefined filters can be given as lambda functions. E.g. filters=[lambda x: isinstance(x._obj, FooClass)]  
        @return: a list of ObjectContainer objects.
        """
        filterlist=[]
        if kwargs.has_key('ref'):
            filterlist.append(lambda x: re.match(kwargs.get('ref'), x.ref))
        if kwargs.has_key('name'):
            filterlist.append(lambda x: re.match(kwargs.get('name'), x._name))
        if kwargs.has_key('path'):
            filterlist.append(lambda x: re.match(kwargs.get('path'), x._path()))
        if kwargs.has_key('type'):
            filterlist.append(lambda x: isinstance(x, kwargs.get('type')))
        if kwargs.has_key('filters'):
            filterlist += kwargs.get('filters')

        ret = []
        for child in self._objects():
            subchildren = child._tail_recurse(_apply_filter,filters=filterlist)
            ret += subchildren
        return ret
    
    def _find_leaves(self, **kwargs):
        """
        Find all leaf nodes in the tree that satisfy the given filtering criteria.
        
        For possible keyword arguments see _traverse().
        
        @return: A list of ObjectContainer objects.
        """
        # Find all children
        nodes = self._traverse(**kwargs)
        
        # Filter out non-leaves
        return filter(lambda node: len(node._objects()) == 0, nodes)

    def _tail_recurse(self, function, **kwargs):
        """
        Run a tail recursion on all container children and execute the given function.
        1. function will receive self as argument to it.
        2. function will receive all kwargs as argument to it.
        3. tail recursion means that the function is executed first and then the 
        recursion continues.
        @param function: the function which is executed
        @param kwargs: a list of arguments as dict
        @return: an list of objects, which can be anything that the funtion returns   
        """
        
        ret = []
        ret += function(self,**kwargs)
        for child in self._objects():
            try:
                # We wont add the object to the ret until we know that it is a valid object
                subchildren = child._tail_recurse(function,**kwargs)
                #ret += function(child,**kwargs)
                ret += subchildren
            except exceptions.InvalidObject,e:
                # remove the invalid object from this container
                logging.getLogger('cone').warning('Removing invalid child because of exception %s' % e)
                self._remove(child._name)
                continue
        return ret

    def _head_recurse(self, function,**kwargs):
        """
        Run a tail recursion on all container children and execute the given function.
        1. function will receive self as argument to it.
        2. function will receive all kwargs as argument to it.
        3. head recursion means that the recursion continues to the leaf nodes and then the 
        execution of the function begins.
        @param function: the function which is executed
        @param kwargs: a list of arguments as dict
        @return: an list of objects, which can be anything that the funtion returns   
        """
        ret = []
        for child in self._objects():
            try:
                ret += child._head_recurse(function,**kwargs)
                ret += function(child,**kwargs)
            except exceptions.InvalidObject,e:
                # remove the invalid object from this container
                logging.getLogger('cone').warning('Removing invalid child because of exception %s' % e)
                self._remove(child._name)
                continue
        return ret

    def _list(self):
        """
        Return a array of immediate children names.
        @return: an unordered list of immediate children path-references
        """
        # skip all internal objects (that start with _)
        return [name for name in self._order if not name.startswith('?')] 

    def _objects(self, **kwargs):
        """
        Return a array of immediate children.
        @return: an unordered list of immediate children
        """
        ret = []
        for cname in self._order:
            try:
                if object_container_filter(self._children[cname], **kwargs):
                    ret += utils.get_list(self._children[cname])
            except exceptions.InvalidObject,e:
                # remove the invalid object from this container
                logging.getLogger('cone').warning('Removing invalid child because of exception %s' % e)
                self._remove(cname)
                continue
        return ret

    def _get_index(self, name):
        """
        Get the index of a child object by its name. The index matches the index
        of the child object in the _children array. 
        @return: integer. 
        @raise NotFound: when object is not found from the children.  
        """
        
        try:
            return self._order.index(name)
        except KeyError:
            raise exceptions.NotFound("Child %s not found!" % name)

    def _supported_type(self,obj):
        """
        An internal function to check that the given object is a supported for this Tree. 
        This is used in every __add__ operation to check whether the object can be added to the tree.
        This function should be overloaded by a subclass if the supported types need to be changed.
        @return: True if object is supported, otherwise false.  
        """
        return isinstance(obj, ObjectContainer)

    def _default_object(self,name):
        """
        An internal function to create a default object for this container in case of __add_to_path__, which
        creates the intermediate objects automatically. 
        This function should be overloaded by a subclass if the default object need to be changed.
        @return: A new object.  
        """
        return ObjectContainer(name)

    def _find_parent(self, **kwargs):
        """
        find a _parent object by arguments. You can define any number of object attributes that 
        have to match to the object. 
        Example1:
           _find_parent(foobar=True) searches for a _parent
          object which has a member attribute foobar and its value is True. 
        Example2:
           _find_parent(name="test") searches for a _parent
          object which has a member attribute name and its value is "test". 
        Example3: type is a special case
           _find_parent(type=Configuration) searches for a _parent
          object which is an instance of Configuration (checked with isinstance). 
        @param kwargs: 
        @return: The object that matches the arguments
        @raise exceptions.NotFound: When no matching parent is found
        """
        type = kwargs.get('type', None)
        if hasattr(self,'_parent') and self._parent != None:
            found = True
            for key in kwargs.keys():
                try:
                    # handle type as a special case
                    if key == 'type':
                        if not isinstance(self._parent, kwargs.get(key)):
                            found = False
                            break
                    elif key == 'match':
                        if not self._parent == kwargs.get(key):
                            found = False
                            break
                    elif not getattr(self._parent, key) == kwargs.get(key):
                        found = False
                        break
                except AttributeError:
                    found = False
                    break
            if found:
                return self._parent
            else:
                return self._parent._find_parent(**kwargs)
        else:
            raise exceptions.NotFound("Parent not found!")

    def _find_parent_or_default(self, default=None,**kwargs):
        """
        Calls internally the find parent function, which is encapsulated with try except 
        returns the given default value if find parent raises NotFound exception. 
        """
        try:
            return self._find_parent(**kwargs)
        except exceptions.NotFound:
            return default

    def set_ref(self,ref):
        """
        @param ref: The new reference of the object
        """
        self._name = ref
        self.ref = ref

    def get_ref(self):
        """
        @return: The reference of the object.
        """
        return self.ref

class ObjectProxyContainer(ObjectProxy,ObjectContainer):
    """
    Combines the Container and Proxy classes to one.
    """
    def __init__(self,obj=None,name=""):
        """
        """
        ObjectContainer.__init__(self,name)
        ObjectProxy.__init__(self,obj)

    def __getattr__(self,name):
        """
        First check if the requested attr is a children then 
        direct all not found attribute calls to the sub object getattr
        """
        try:
            return self.__dict__['_children'][name] 
        except KeyError:
            return getattr(self._obj,name)
