'''
Task Coach - Your friendly task manager
Copyright (C) 2004-2012 Task Coach developers <developers@taskcoach.org>

Task Coach 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 3 of the License, or
(at your option) any later version.

Task Coach 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, see <http://www.gnu.org/licenses/>.
'''

from taskcoachlib import patterns
from taskcoachlib.domain import date, task
import effortlist, composite


class EffortAggregator(patterns.SetDecorator, 
                       effortlist.EffortUICommandNamesMixin):
    ''' This class observes an TaskList and aggregates the individual effort
        records to CompositeEfforts, e.g. per day or per week. Whenever a 
        CompositeEffort becomes empty, for example because effort is deleted,
        it sends an 'empty' event so that the aggregator can remove the
        (now empty) CompositeEffort from itself. '''
        
    def __init__(self, *args, **kwargs):
        self.__composites = {}
        aggregation = kwargs.pop('aggregation')
        assert aggregation in ('day', 'week', 'month')
        aggregation = aggregation.capitalize()
        self.startOfPeriod = getattr(date.DateTime, 'startOf%s'%aggregation)
        self.endOfPeriod = getattr(date.DateTime, 'endOf%s'%aggregation)
        super(EffortAggregator, self).__init__(*args, **kwargs)
        patterns.Publisher().registerObserver(self.onCompositeEmpty,
            eventType='effort.composite.empty')
        patterns.Publisher().registerObserver(self.onEffortAddedToTask, 
            eventType='task.effort.add')
        patterns.Publisher().registerObserver(self.onChildAddedToTask,
            eventType=task.Task.addChildEventType())
        patterns.Publisher().registerObserver(self.onEffortStartChanged, 
            eventType='effort.start')
    
    @patterns.eventSource    
    def extend(self, efforts, event=None): # pylint: disable-msg=W0221
        for effort in efforts:
            effort.task().addEffort(effort, event=event)

    @patterns.eventSource            
    def removeItems(self, efforts, event=None): # pylint: disable-msg=W0221
        for effort in efforts:
            effort.task().removeEffort(effort, event=event)
            
    def extendSelf(self, tasks, event=None):
        ''' extendSelf is called when an item is added to the observed
            list. The default behavior of extendSelf is to add the item
            to the observing list (i.e. this list) unchanged. We override 
            the default behavior to first get the efforts from the task
            and then group the efforts by time period. '''
        newComposites = []
        for task in tasks: # pylint: disable-msg=W0621
            newComposites.extend(self.createComposites(task, task.efforts()))
        super(EffortAggregator, self).extendSelf(newComposites, event)

    @patterns.eventSource
    def removeItemsFromSelf(self, tasks, event=None):
        ''' removeItemsFromSelf is called when an item is removed from the 
            observed list. The default behavior of removeItemsFromSelf is to 
            remove the item from the observing list (i.e. this list)
            unchanged. We override the default behavior to remove the 
            tasks' efforts from the CompositeEfforts they are part of. '''
        compositesToRemove = []
        for task in tasks: # pylint: disable-msg=W0621
            compositesToRemove.extend(self.compositesToRemove(task))
        super(EffortAggregator, self).removeItemsFromSelf(compositesToRemove, 
                                                          event=event)
        
    def onEffortAddedToTask(self, event):
        newComposites = []
        for task in event.sources(): # pylint: disable-msg=W0621
            if task in self.observable():
                efforts = event.values(task)
                newComposites.extend(self.createComposites(task, efforts))
        super(EffortAggregator, self).extendSelf(newComposites)
        
    def onChildAddedToTask(self, event):
        newComposites = []
        for task in event.sources(): # pylint: disable-msg=W0621
            if task in self.observable():
                child = event.value(task)
                newComposites.extend(self.createComposites(task,
                    child.efforts(recursive=True)))
        super(EffortAggregator, self).extendSelf(newComposites)

    def onCompositeEmpty(self, event):
        # pylint: disable-msg=W0621
        composites = [composite for composite in event.sources() if \
                      composite in self]
        keys = [self.keyForComposite(composite) for composite in composites]
        # A composite may already have been removed, e.g. when a
        # parent and child task have effort in the same period
        keys = [key for key in keys if key in self.__composites]
        for key in keys:
            del self.__composites[key]
        super(EffortAggregator, self).removeItemsFromSelf(composites)
        
    def onEffortStartChanged(self, event):
        newComposites = []
        for effort in event.sources():
            key = self.keyForEffort(effort)
            task = effort.task() # pylint: disable-msg=W0621
            if (task in self.observable()) and (key not in self.__composites):
                newComposites.extend(self.createComposites(task, [effort]))
        super(EffortAggregator, self).extendSelf(newComposites)
            
    def createComposites(self, task, efforts): # pylint: disable-msg=W0621
        newComposites = []
        for effort in efforts:
            newComposites.extend(self.createCompositesForTask(effort, task))
            newComposites.extend(self.createCompositeForPeriod(effort))
        return newComposites

    def createCompositesForTask(self, anEffort, task): # pylint: disable-msg=W0621
        newComposites = []
        for eachTask in [task] + task.ancestors():
            key = self.keyForEffort(anEffort, eachTask)
            if key in self.__composites:
                continue
            newComposite = composite.CompositeEffort(*key) # pylint: disable-msg=W0142
            self.__composites[key] = newComposite
            newComposites.append(newComposite)
        return newComposites
    
    def createCompositeForPeriod(self, anEffort):
        key = self.keyForPeriod(anEffort)
        if key in self.__composites:
            self.__composites[key].addEffort(anEffort)
            return []
        newCompositePerPeriod = composite.CompositeEffortPerPeriod(key[0], key[1], self.observable(), anEffort)
        self.__composites[key] = newCompositePerPeriod
        return [newCompositePerPeriod]

    def compositesToRemove(self, task): # pylint: disable-msg=W0621
        efforts = task.efforts()
        taskAndAncestors = [task] + task.ancestors()
        compositesToRemove = []
        for effort in efforts:
            for task in taskAndAncestors:
                compositesToRemove.extend(self.compositeToRemove(effort, task))
        return compositesToRemove
        
    def compositeToRemove(self, anEffort, task): # pylint: disable-msg=W0613,W0621
        key = self.keyForEffort(anEffort, task)
        # A composite may already have been removed, e.g. when a
        # parent and child task have effort in the same period
        return [self.__composites.pop(key)] if key in self.__composites else []

    def maxDateTime(self):
        stopTimes = [effort.getStop() for compositeEffort in self for effort
                     in compositeEffort if effort.getStop() is not None]
        return max(stopTimes) if stopTimes else None

    @staticmethod
    def keyForComposite(compositeEffort):
        if compositeEffort.task().__class__.__name__ == 'Total':
            return (compositeEffort.getStart(), compositeEffort.getStop())
        else:
            return (compositeEffort.task(), compositeEffort.getStart(), 
                    compositeEffort.getStop())
    
    def keyForEffort(self, effort, task=None): # pylint: disable-msg=W0621
        task = task or effort.task()
        effortStart = effort.getStart()
        return (task, self.startOfPeriod(effortStart), 
            self.endOfPeriod(effortStart))
        
    def keyForPeriod(self, effort):
        key = self.keyForEffort(effort)
        return key[1], key[2]
    
    @classmethod
    def sortEventType(class_):
        return 'this event type is not used' # pragma: no cover

