# -*- coding: utf-8 -*-
#######################################################################################
#   Plinn - http://plinn.org                                                          #
#   Copyright (C) 2005-2007  Benoît PIN <benoit.pin@ensmp.fr>                         #
#                                                                                     #
#   This program is free software; you can redistribute it and/or                     #
#   modify it under the terms of the GNU General Public License                       #
#   as published by the Free Software Foundation; either version 2                    #
#   of the License, or (at your option) any later version.                            #
#                                                                                     #
#   This program is distributed in the hope that it will be useful,                   #
#   but WITHOUT ANY WARRANTY; without even the implied warranty of                    #
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the                     #
#   GNU General Public License for more details.                                      #
#                                                                                     #
#   You should have received a copy of the GNU General Public License                 #
#   along with this program; if not, write to the Free Software                       #
#   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.   #
#######################################################################################
""" Plinn implementation of CMFBTree



"""


from Products.BTreeFolder2.BTreeFolder2 import BTreeFolder2Base
from Products.ZCatalog.Lazy import LazyMap
from BTrees.IOBTree import IOBTree
from BTrees.OIBTree import OIBTree
from Folder import PlinnFolder
from zope.event import notify
try :
    from zope.app.container.contained import notifyContainerModified
except ImportError :
    ## Zope-2.13 compat
    from zope.container.contained import notifyContainerModified
from events import ObjectPositionModified
from zope.component.factory import Factory
from Products.CMFCore.permissions import AddPortalFolders, \
                                         ManageProperties, \
                                         AccessContentsInformation
from AccessControl import ClassSecurityInfo
from Globals import InitializeClass
from types import StringType


class HugePlinnFolder(BTreeFolder2Base, PlinnFolder) :
    """ Plinn Folder for large set of objects
    """
    
    security = ClassSecurityInfo()
    
    __getitem__ = PlinnFolder.__getitem__
    
    def __init__(self, id, title='') :
        PlinnFolder.__init__(self, id, title)
        BTreeFolder2Base.__init__(self, id)

    def _initBTrees(self):
        super(HugePlinnFolder, self)._initBTrees()
        self._pos2id_index = IOBTree()
        self._id2pos_index = OIBTree()
    
    def _checkId(self, id, allow_dup=0) :
        PlinnFolder._checkId(self, id, allow_dup)
        BTreeFolder2Base._checkId(self, id, allow_dup)
    
    security.declareProtected(AddPortalFolders, 'manage_addHugePlinnFolder')
    def manage_addHugePlinnFolder(self, id, title='', REQUEST=None) :
        """ Add new a new HugePlinnFolder object with id *id*.
        """
        ob = HugePlinnFolder(id, title)
        self._setObject(id, ob)
        if REQUEST is not None :
            return self.folder_contents(self, REQUEST, portal_status_message='Folder added')
    
    def _setOb(self, id, object):
        super(HugePlinnFolder, self)._setOb(id, object)
        pos = self.objectCount() - 1
        self._pos2id_index[pos] = id
        self._id2pos_index[id] = pos
    
    def _delOb(self, id):
        pos = self._id2pos_index[id]
        self._id2pos_index.pop(id)
            
        for p in xrange(pos+1, self.objectCount()) :
            ident = self._pos2id_index[p]
            self._pos2id_index[p-1] = ident
            self._id2pos_index[ident] = p-1
        
        self._pos2id_index.pop(self.objectCount()-1)

        super(HugePlinnFolder, self)._delOb(id)
            
    security.declareProtected(AccessContentsInformation, 'objectIds')
    def objectIds(self, spec=None) :
        if spec is not None :
            return super(HugePlinnFolder, self).objectIds(spec)
        
        pos2id = lambda pos : self._pos2id_index[pos]
        return LazyMap(pos2id, xrange(self.objectCount()))
        


    security.declareProtected(ManageProperties, 'moveObjectsByDelta')
    def moveObjectsByDelta(self, ids, delta, subset_ids=None,
                           suppress_events=False):
        """ Move specified sub-objects by delta.
        """
        if isinstance(ids, StringType):
            ids = (ids,)

        id2pos = self._id2pos_index
        pos2id = self._pos2id_index
        for id in ids :
            oldPosition = id2pos[id]
            newPosition = max(oldPosition + delta, 0)
            
            shift = delta > 0 and 1 or -1
            for p in xrange(oldPosition, newPosition, shift) :
                ident = pos2id[p+shift]
                pos2id[p] = ident
                id2pos[ident] = p
                if not suppress_events :
                    notify(ObjectPositionModified(self[ident], self, p))
            
            id2pos[id] = newPosition
            pos2id[newPosition] = id
            if not suppress_events :
                notify(ObjectPositionModified(self[id], self, newPosition))
        
        if not suppress_events :
            notifyContainerModified(self)
    
    security.declareProtected(ManageProperties, 'moveObjectsAfter')
    def moveObjectsAfter(self, ids, targetId, suppress_events=False):
        assert targetId not in ids

        id2pos = self._id2pos_index
        pos2id = self._pos2id_index
        targetPos = id2pos[targetId]
        minMovedPos = min([id2pos[id] for id in ids])
        maxMovedPos = max([id2pos[id] for id in ids])
        
        for id in ids :
            assert id == pos2id.pop(id2pos.pop(id))
        
        id2posUpdate = {}
        pos2idUpdate = {}

        if targetPos < minMovedPos :
            # selection moved before the first item position
            for i, id in enumerate(ids) :
                pos = i + targetPos + 1
                id2posUpdate[id] = pos
                pos2idUpdate[pos] = id

            for id in IndexIterator(pos2id, targetPos+1, maxMovedPos):
                pos = pos + 1
                id2posUpdate[id] = pos
                pos2idUpdate[pos] = id
            
        elif targetPos > minMovedPos and targetPos < maxMovedPos :
            # selection moved between the first and last item positions
            pos = minMovedPos
            # move items placed between the first item position and the target position
            for id in IndexIterator(pos2id, minMovedPos+1, targetPos) :
                id2posUpdate[id] = pos
                pos2idUpdate[pos] = id
                pos += 1
            # move selected items
            for id in ids :
                id2posUpdate[id] = pos
                pos2idUpdate[pos] = id
                pos += 1
            # move items positioned between the target position and the moved item max position
            for id in IndexIterator(pos2id, targetPos+1, maxMovedPos) :
                id2posUpdate[id] = pos
                pos2idUpdate[pos] = id
                pos += 1

        else :
            # selection moved after the last item position
            pos = minMovedPos
            for id in IndexIterator(pos2id, minMovedPos+1, targetPos) :
                id2posUpdate[id] = pos
                pos2idUpdate[pos] = id
                pos += 1
            
            pos = targetPos - len(ids) + 1
            for id in ids :
                id2posUpdate[id] = pos
                pos2idUpdate[pos] = id
                pos +=1
        
        id2pos.update(id2posUpdate)
        pos2id.update(pos2idUpdate)

        if not suppress_events :
            for id, pos in id2posUpdate.items() :
                notify(ObjectPositionModified(self[id], self, pos))
                
            notifyContainerModified(self)
    
    def getObjectPosition(self, id):
        """ Get the position of an object by its id.
        """
        try :
            return self._id2pos_index[id]
        except KeyError :
            raise ValueError('The object with the id "%s" does not exist.' % id)


class IndexIterator :
    def __init__(self, d, start, stop) :
        self.d = d
        self.pos = start
        self.stop = stop
    
    def __iter__(self) :
        return self
    
    def next(self) :
        try :
            if self.pos > self.stop :
                raise StopIteration
            v = self.d[self.pos]
            self.pos = self.pos + 1
            return v
        except KeyError :
            self.pos = self.pos + 1
            return self.next()
    

InitializeClass(HugePlinnFolder)
HugePlinnFolderFactory = Factory(HugePlinnFolder)
manage_addHugePlinnFolder = HugePlinnFolder.manage_addHugePlinnFolder.im_func
