# -*- encoding: utf-8 -*-
#   Copyright 2007-2009 Agile42 GmbH - Andrea Tomasini 
#
#   Licensed under the Apache License, Version 2.0 (the "License");
#   you may not use this file except in compliance with the License.
#   You may obtain a copy of the License at
#
#       http://www.apache.org/licenses/LICENSE-2.0
#
#   Unless required by applicable law or agreed to in writing, software
#   distributed under the License is distributed on an "AS IS" BASIS,
#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#   See the License for the specific language governing permissions and
#   limitations under the License.
#   Authors: 
#       - Andrea Tomasini <andrea.tomasini_at_agile42.com>
#       - Felix Schwarz <felix.schwarz_at_agile42.com>

from datetime import datetime, time, timedelta

from trac.util import datefmt
from trac.util.translation import _

from agilo.api import controller, validator
from agilo.api.controller import ValueObject
from agilo.scrum.sprint.model import SprintModelManager, Sprint
from agilo.scrum.team.model import TeamModelManager
from agilo.ticket import TicketController
from agilo.utils import Key, Status, log

__all__ = ['SprintController']


class SprintController(controller.Controller):
    """Take care of processing any command related to a Sprint"""
    
    def __init__(self):
        """Initialize the component, sets some references to needed
        Model Managers"""
        self.sp_manager = SprintModelManager(self.env)
        self.tm_manager = TeamModelManager(self.env)
    
    
    class ListSprintsCommand(controller.ICommand):
        """Command to fetch a list of Sprints fulfilling certain 
        criteria"""
        parameters = {'criteria': validator.DictValidator, 
                      'order_by': validator.IterableValidator, 
                      'limit': validator.IntValidator}
        
        def _execute(self, sp_controller, date_converter, as_key):
            """Execute the listing command, returns a list of sprints, 
            if the set criteria is None, it returns all the sprints, 
            otherwise only the sprints matching the criteria"""
            result = []
            sprints = sp_controller.sp_manager.select(criteria=self.criteria,
                                                      order_by=self.order_by or \
                                                      ['start'],
                                                      limit=self.limit)
            for sprint in sprints:
                result.append(self.return_as_value_object(sprint, 
                                                  date_converter, 
                                                  as_key))
            return result
    
    
    class GetSprintCommand(controller.ICommand):
        """Command to get a sprint for a given name"""
        parameters = {'sprint': validator.MandatorySprintValidator}
        
        def _execute(self, sp_controller, date_converter, as_key):
            """Returns the sprint for the given name if existing or None"""
            return self.return_as_value_object(self.sprint, date_converter, as_key)
    
    
    class DeleteSprintCommand(GetSprintCommand):
        """Command to delete a sprint"""
        def _execute(self, sp_controller, date_converter, as_key):
            # AT: the delete on PersistentObject can still raise an
            # UnableToDeletePersistentObject Exception, that will be
            # caught by the Controller.process_command and wrapped
            # into a CommandError.
            if not sp_controller.sp_manager.delete(self.sprint):
                raise self.CommandError("Error deleting Sprint: %s" % \
                                        self.sprint)
    
    
    class CreateSprintCommand(controller.ICommand):
        """Command to create a new Sprint"""
        
        parameters = {'name': validator.MandatoryStringValidator, 
                      'milestone': validator.MandatoryStringValidator, 
                      'start': validator.MandatoryUTCDatetimeValidator, 
                      'end': validator.UTCDatetimeValidator, 
                      'duration': validator.IntValidator, 
                      'team': validator.TeamValidator, 
                      'description': validator.StringValidator}
        
        def consistency_validation(self, env):
            """
            Validate the consistency of dates and duration together,
            the individual parameter format validation has already
            occurred, now we check the relation between them, in
            particular:
                start < end
                if end is not present, duration must be
                if duration is not present, end must be
            """
            if self.end and self.start and self.start >= self.end:
                self.not_valid("Start date after end date?", 
                               'Consistency error', self.start)
            if not self.end and not self.duration:
                self.not_valid("Either end or a duration must "\
                               "be specified", 'Consistency Error', 
                               self.end)
            
        def _prepare_params(self):
            """Prepares the params to be send to the model manager"""
            params = dict()
            params['save'] = getattr(self, 'save', True)
            
            for attr_name in ('name', 'description', 'milestone',
                              # We assume dates are valid and have been 
                              # converted by the validation.
                              'start', 'end', 
                              # Duration and team have been converted
                              # by the validators
                              'duration', 'team'):
                # There should always be either name or sprint, not
                # both, but to the manager we need to give a name
                value = getattr(self, attr_name, None)
                if attr_name == 'name' and not value:
                    value = getattr(self, 'sprint', None)
                params[attr_name] = value
            return params

        def _execute(self, sp_controller, date_converter, as_key):
            """Creates a new Sprint object, if there is the save 
            option, is also saving it, otherwise not"""
            params = self._prepare_params()
            sprint = sp_controller.sp_manager.create(**params)
            return self.return_as_value_object(sprint, date_converter, as_key)


    class SaveSprintCommand(controller.ICommand):
        """Command to save a sprint"""
        parameters = {'sprint': validator.SprintValidator,
                      'old_sprint': validator.SprintValidator,
                      'milestone': validator.MandatoryStringValidator, 
                      'start': validator.MandatoryUTCDatetimeValidator, 
                      'end': validator.UTCDatetimeValidator, 
                      'duration': validator.IntValidator, 
                      'team': validator.TeamValidator, 
                      'description': validator.StringValidator}
        
        def consistency_validation(self, env):
            """We need to check that either sprint or old_sprint are
            set and are valid sprints"""
            if not isinstance(self.sprint, Sprint) and \
                    not isinstance(self.old_sprint, Sprint):
                self.not_valid("No valid sprint found", 
                               "sprint", 
                               (self.sprint, self.old_sprint))
            # check that the duration and end date are not both
            # present in which case consider only the changed value
            if self.end is not None and self.duration is not None:
                sprint = self.old_sprint or self.sprint
                if self.end != sprint.end:
                    # It is changed, so remove the duration
                    self.duration = None
                elif self.duration != sprint.duration:
                    # it is changed, so remove the end
                    self.end = None
                else:
                    # nothing changed so remove both
                    self.duration = self.end = None
            
        def _execute(self, sp_controller, date_converter, as_key):
            """Saves the given sprint, first try to fetch it, than set 
            the values and save"""
            # check if the sprint has been renamed or not
            sprint = self.old_sprint or self.sprint
            
            for attr_name in self.parameters:
                if hasattr(sprint, attr_name):
                    sprint_attr_value = getattr(sprint, attr_name)
                    if not callable(sprint_attr_value):
                        value = getattr(self, attr_name)
                        if value:
                            setattr(sprint, attr_name, value)
            return sp_controller.sp_manager.save(sprint)
    
    
    class CalculateRemainingTimeUSPRatioCommand(controller.ICommand):
        """
        Calculate the Remaining Time/User Story Point ratio for the sprint
        by using the given stories. Return the ratio as float.
        Does only include stories in the calculation which have story points.
        Returns None if no ratio could be calculated.
        """
        
        parameters = {'sprint': validator.MandatorySprintValidator, 
                      'stories': validator.IterableValidator, }
        
        def _execute(self, sp_controller, date_converter, as_key):
            def convert_to_number(value):
                try:
                    return float(value)
                except:
                    return None
            
            ratio = None
            total_story_points = 0
            sum_total_remaining_time = 0
            for story in self.stories:
                sp = convert_to_number(story[Key.STORY_POINTS])
                trt = story[Key.TOTAL_REMAINING_TIME]
                if sp != None and trt != None:
                    total_story_points += sp
                    sum_total_remaining_time += trt
            if total_story_points != 0:
                ratio = sum_total_remaining_time / total_story_points
            return ratio
    
    
    class RetargetTicketsCommand(controller.ICommand):
        """Command to retarget all the open tickets from a Sprint to 
        another. Tickets to the specified sprint (all tickets with a 
        status different from 'closed' are considered as incomplete).
        If specified, 'author' will be used as username who did the 
        change."""
        parameters = {'sprint': validator.MandatorySprintValidator, 
                      'retarget': validator.MandatorySprintValidator, 
                      'author': validator.StringValidator}
        
        def _execute(self, sp_controller, date_converter, as_key):
            """Execute the command"""
            from agilo.scrum.backlog import BacklogModelManager
            bmm = BacklogModelManager(sp_controller.env)
            sprint_backlog = bmm.get(name=Key.SPRINT_BACKLOG, 
                                     scope=self.sprint.name)
            for bi in sprint_backlog:
                if (bi[Key.SPRINT] != self.sprint.name) or \
                        (bi[Key.STATUS] == Status.CLOSED):
                    continue
                else:
                    log.info(sp_controller.env, 
                             u'Retargeting ticket %d to sprint %s' % \
                             (bi.ticket.id, self.retarget.name))
                    bi[Key.SPRINT] = self.retarget.name
            sprint_backlog.save(author=self.author)


    class GetTicketsStatisticsCommand(controller.ICommand):
        """
        Returns the ticket statistics for a given sprint, in the form
        of a dictionary with key the ticket types and as value a tuple
        with planned and closed. E.g.:
        
            {'story': (12, 8), 'task': (40, 24)}
        
        If the option totals is set to True, than only a tuple with
        the total tickets count is returned:
        
            (12, 8) # open, closed
        
        """
        parameters = {'sprint': validator.MandatorySprintValidator, 
                      'totals': validator.BoolValidator}
        
        def _execute(self, sp_controller, date_converter, as_key):
            """Fetch the tickets and prepare the statistics"""
            tickets = dict()
            from agilo.scrum.backlog import BacklogModelManager
            backlog_manager = BacklogModelManager(sp_controller.env)
            #from agilo.ticket.model import AgiloTicketModelManager
            #ticket_manager = AgiloTicketModelManager(sp_controller.env)
            #fetched_tickets = ticket_manager.select(criteria={'sprint': self.sprint.name})
            fetched_tickets = [b.ticket for b in \
                               backlog_manager.get(name=Key.SPRINT_BACKLOG,
                                                   scope=self.sprint.name)]
            # Used for global counting, in case self.totals
            nr_planned, nr_closed = (0, 0)
            for t in fetched_tickets:
                if not self.totals:
                    nr_planned, nr_closed = (0, 0)
                    t_type = t.get_type()
                    if t_type in tickets:
                        nr_planned, nr_closed = tickets[t_type]
                if t[Key.STATUS] == Status.CLOSED:
                    nr_closed += 1
                else:
                    nr_planned += 1
                if not self.totals:
                    tickets[t_type] = (nr_planned, nr_closed)
            # Returns the tuple or the dictionary
            if self.totals:
                return nr_planned, nr_closed
            else:
                return tickets


    class ListTicketsHavingPropertiesCommand(TicketController.ListTicketsCommand):
        """
        Returns a list of all the tickets belonging to a Sprint, and
        having a specified list of properties.
        """
        parameters = {'sprint': validator.MandatorySprintValidator, 
                      'properties': validator.IterableValidator, 
                      'criteria': validator.DictValidator, 
                      'order_by': validator.IterableValidator, 
                      'limit': validator.IntValidator}
        
        def _execute(self, sp_controller, date_converter, as_key):
            """Execute the query on the TicketController"""
            if not self.criteria:
                self.criteria = {}
            self.with_attributes = self.properties
            self.criteria.update({'sprint': self.sprint.name})
            # Now return the superclass execute, giving it the right
            # controller
            return super(self.__class__, 
                         self)._execute(TicketController(sp_controller.env))
    
    
    class GetRemainingTimeSeriesForTicketsInSprintCommand(controller.ICommand):
        """This is some kind of 'abstract' super class for different commands.
        The public functional build_remaining_time_series_for_interval returns
        a dictionary (task -> [(day, remaining time)]) which is used by
        other commands to produce some aggregated views without replicating
        much of the tedious work every time.
        """
        parameters = {}
        
        def _get_tickets_with_attribute(self, tc, env, backlog, attribute):
            cmd = TicketController.FilterTicketsWithAttribute(env, 
                      tickets=backlog, attribute_name=attribute)
            cmd.native = True
            return tc.process_command(cmd)
        
        def _get_orphan_tasks(self, tc, env, backlog):
            cmd = TicketController.FindOrphanTasks(env, 
                                                   tickets=backlog)
            cmd.native = True
            return tc.process_command(cmd)
        
        def _midnight(self, day):
            """Return a datetime for midnight of the given datetime 
            (keeps the timezone)"""
            tz = (day.tzinfo or datefmt.localtz)
            return datetime.combine(day, time(tzinfo=tz))
        
        def _get_remaining_time_series_for_ticket(self, start, end, today, 
                                             get_remaining_time, cut_to_today):
            """Return the remaining time series for this ticket within the 
            specified interval. Today is the date of today so that his does not
            have to be computed every time (prevents tz-related errors).
            
            get_remaining_time is a callable which returns the remaining time
            as number for the passed datetime."""
            rt_series = [(start, get_remaining_time(start))]
            current_time = self._midnight(start + timedelta(days=1))
            while current_time <= end:
                remaining = get_remaining_time(current_time)
                rt_series.append((current_time, remaining))
                if current_time.date() == today.date() and \
                        current_time < today and today < end and cut_to_today:
                    # In case the date is today, we also add the current 
                    # remaining time and exit the loop
                    remaining = get_remaining_time()
                    # dates are used as keys later so we have to use a datetime
                    # with exactly the same microsecond attribute!
                    rt_series.append((today, remaining))
                    break
                current_time += timedelta(days=1)
            return rt_series
        
        def _get_remaining_time_series_for_task(self, env, start, end, today, 
                                                task, cut_to_today):
            """Return the remaining time series for this task within the 
            specified interval. Today is the date of today so that his does not
            have to be computed every time (prevents tz-related errors)."""
            # TODO: Fixme - use some kind of ModelManager?
            from agilo.scrum import RemainingTime
            rt_store = RemainingTime(env, task)
            return self._get_remaining_time_series_for_ticket(start, end, today, 
                                     rt_store.get_remaining_time, cut_to_today)
        
        def _get_remaining_times_series_for_tasks(self, env, tasks, start, end, 
                                                  today, cut_to_today):
            rt_series_by_task = {}
            for task in tasks:
                series = self._get_remaining_time_series_for_task(env, start, 
                                                end, today, task, cut_to_today)
                rt_series_by_task[task] = series
            return rt_series_by_task
        
        def _get_remaining_time_series_for_stories(self, stories, start, end, 
                                                   today, cut_to_today):
            rt_series = {}
            for story in stories:
                # AT: this is not enough we have to check that the tasks linked
                # to the story are either planned for the current sprint or not
                # planned at all
                story_is_broken_down = (len([task for task in story.get_outgoing() if \
                                             task[Key.SPRINT] in (story[Key.SPRINT], '')]) > 0)
                # If a story has some tasks in other sprints, do not use
                # ESTIMATED_REMAINING_TIME - even if none of these tasks is 
                # planned for this sprint
                if not story_is_broken_down:
                    estimated_remaining_time = story[Key.ESTIMATED_REMAINING_TIME]
                    get_remaining = lambda x=None: estimated_remaining_time or 0
                    series = self._get_remaining_time_series_for_ticket(start, 
                                       end, today, get_remaining, cut_to_today)
                    rt_series[story] = series
            return rt_series
        
        def _get_backlog(self, env, sprint):
            from agilo.scrum.backlog import BacklogModelManager
            backlog = BacklogModelManager(env).get(name=Key.SPRINT_BACKLOG, 
                                                   scope=sprint.name,
                                                   reload=True)
            return backlog
        
        def _get_tasks_for_story(self, env, sp_controller, 
                                 sprint_name, story, backlog):
            """Returns the list of tasks for the given story, that are in 
            the current sprint backlog"""
            cmd = SprintController.GetReferencedTasksInThisSprintCommand(env,
                          story=story, sprint=sprint_name, tickets=backlog)
            cmd.native = True
            tasks = sp_controller.process_command(cmd)
            return tasks
        
        def build_remaining_time_series_for_interval(self, env, start, end, 
                                     sprint, sp_controller, cut_to_today=True):
            """Return a dictionary of tuples
                  ticket -> (datetime, remaining time)
               for the tickets in the given sprint.
            """
            tc = TicketController(env)
            # AT: we have to check if the command received tickets or
            # not. In case there are no tickets we will get the full
            # backlog
            backlog = self.tickets or self._get_backlog(env, sprint)
            stories = self._get_tickets_with_attribute(tc, env, backlog, 
                                                Key.ESTIMATED_REMAINING_TIME)
            orphan_tasks = self._get_orphan_tasks(tc, env, backlog)
            get_tasks = lambda story=None: self._get_tasks_for_story(env, 
                            sp_controller, sprint.name, story, backlog)
            story_tasks = [get_tasks(story) for story in stories]
            all_tasks = orphan_tasks + reduce(lambda x, y: x+y, 
                                              story_tasks, [])
            
            today = datefmt.parse_date('now')
            
            rt_series_by_task = \
                self._get_remaining_times_series_for_tasks(env, all_tasks, 
                                               start, end, today, cut_to_today)
            rt_series_by_story = \
                self._get_remaining_time_series_for_stories(stories, start, 
                                                      end, today, cut_to_today)
            rt_series_by_task.update(rt_series_by_story)
            return rt_series_by_task
    
    
    class GetRemainingTimesCommand(GetRemainingTimeSeriesForTicketsInSprintCommand):
        """Returns a list of remaining time for each day of the sprint 
        until the end of the sprint.
        
        If cut_to_today is True (default), the list will not contain 
        any items for days in the future.
        
        If commitment is given, the remaining time for the first 
        sprint day is set to the specified commitment.
        
        If tickets are given, this command won't fetch the tickets for 
        the sprint but uses just this list of tickets (performance 
        optimization)."""
        parameters = {'sprint': validator.MandatorySprintValidator, 
                      'cut_to_today': validator.BoolValidator, 
                      'commitment': validator.IntOrFloatValidator, 
                      'tickets': validator.IterableValidator}
        
        def is_filtered_backlog(self):
            return (getattr(self.tickets, 'filter_by', None) is not None)
        
        def _get_remaining_times_for_interval(self, env, start, end, sprint, 
                                              sp_controller, commitment):
            rt_series_by_task = \
                self.build_remaining_time_series_for_interval(env, start, end, 
                                                    sprint, sp_controller)
            remaining_time_by_day = {}
            for task, days in rt_series_by_task.items():
                for day, remaining_time in days:
                    if day not in remaining_time_by_day:
                        remaining_time_by_day[day] = 0
                    remaining_time_by_day[day] += remaining_time
            
            remaining_times = [remaining_time_by_day[day] for day in \
                               sorted(remaining_time_by_day)]
            
            if (commitment is not None) and not self.is_filtered_backlog():
                remaining_times[0] = commitment
            return remaining_times
        
        def _execute(self, sp_controller, date_converter, as_key):
            commitment = self.commitment
            
            env = sp_controller.env
            today = datefmt.parse_date('now')
            sprint = self.sprint
            start = sprint.start
            
            end = sprint.end
            if self.cut_to_today:
                end = min(end, self._midnight(today + timedelta(days=1)))
            remaining_times = self._get_remaining_times_for_interval(env, 
                                 start, end, sprint, sp_controller, commitment)
            return remaining_times
    
    
    class GetTotalRemainingTimeCommand(GetRemainingTimesCommand):
        """Returns the total current remaining time for this sprint, 
        summing up the remaining time of estimated tasks."""
        parameters = {'sprint': validator.MandatorySprintValidator,
                      'day': validator.UTCDatetimeValidator,
                      'commitment': validator.IntOrFloatValidator,
                      'tickets': validator.IterableValidator}
        
        def _execute(self, sp_controller, date_converter, as_key):
            #AT: we have to use parse_date instead of to_datetime cause
            # parse_date is UTC and not local timezone
            today = datefmt.parse_date('now') 
            day = self.day or today
            remaining_times = \
                self._get_remaining_times_for_interval(sp_controller.env, day, 
                        day, self.sprint, sp_controller, self.commitment)
            return sum(remaining_times)
    
    
    class GetReferencedTasksInThisSprintCommand(controller.ICommand):
        """Returns the referenced tasks planned in this sprint related
        to the given story"""
        
        parameters = {'sprint': validator.MandatorySprintValidator,
                      'story': validator.MandatoryTicketValidator,
                      'tickets': validator.IterableValidator}
        
        def get_referenced_tickets(self, parent):
            linked_tickets = parent.get_outgoing()
            if self.tickets is not None:
                tickets_for_story = []
                linked_ids = [t.id for t in linked_tickets]
                for ticket_or_bi in self.tickets:
                    if not getattr(ticket_or_bi, 'is_visible', True):
                        continue
                    ticket = getattr(ticket_or_bi, 'ticket', ticket_or_bi)
                    if ticket.id in linked_ids:
                        tickets_for_story.append(ticket)
            else:
                tickets_for_story = linked_tickets
            return tickets_for_story
        
        def _execute(self, sp_controller, date_converter, as_key):
            """Return a list of tasks (tickets with remaining time) 
            which are referenced by 'story' and which are not planned 
            or planned for the given sprint. 
            If the tickets parameter was given, only return tasks 
            which are also in this list of tickets."""
            # FS: In case that we need to filter the backlog by an 
            # additional attribute ('component backlog'), we need to 
            # check bi.is_visible in order not to duplicate the whole 
            # filtering logic here. We already extended 
            # get_tickets_with_attribute to return only tickets that 
            # should be displayed. So we take advantage of this here 
            # by passing the backlog (all_tickets).
            tasks = []
            for ticket in self.get_referenced_tickets(self.story):
                if ticket.is_readable_field(Key.REMAINING_TIME) and \
                    ticket[Key.SPRINT] in (None, '', self.sprint.name):
                    tasks.append(ticket)
            return tasks
    
    
    class GetSprintOptionListCommand(ListSprintsCommand):
        """Returns 3 lists containing the sprints which have been 
        closed, the one currently running and finally the one still to 
        start. Normally used to prepare the field option group to show 
        the sprint grouped by status."""
        
        parameters = {'criteria': validator.DictValidator, 
                      'order_by': validator.IterableValidator, 
                      'limit': validator.IntValidator,
                      'sprint_names': validator.IterableValidator}
        
        def _execute(self, sp_controller, date_converter, as_key):
            """Returns 3 lists containing the sprints which have been 
            closed, the one currently running and finally the one 
            still to start. This method is normally used to prepare 
            the field option group to show the sprint grouped."""
            closed = list()
            running = list()
            to_start = list()
            sprint_names = getattr(self, "sprint_names", None)
            criteria = getattr(self, "criteria", None)
            if sprint_names:
                if not criteria:
                    criteria = {}
                criteria.update({'name': 'in %s' % sprint_names})
            # check the given sprints
            sprints = super(self.__class__, self)._execute(sp_controller,
                                                           date_converter,
                                                           as_key)
            for s in sprints:
                if s.is_currently_running:
                    running.append(s.name)
                elif s.is_closed:
                    closed.append(s.name)
                else:
                    to_start.append(s.name)
            return closed, running, to_start
    
    
    class GetResourceLoadForDevelopersInSprintCommand(GetRemainingTimeSeriesForTicketsInSprintCommand):
        """Return a list of developers with information about their 
        load (based on the remaining time of their accepted tasks) for 
        every day in the sprint."""
        
        parameters = {'sprint': validator.MandatorySprintValidator,
                      'tickets': validator.IterableValidator}
        
        def _extrapolate_load_data_for_single_developer(self, developer, end):
            if developer.load is not None:
                one_day = timedelta(days=1)
                
                dummy_load = developer.load[-1].copy()
                dummy_load.day = None
                dummy_load.is_overloaded = None
                dummy_load.is_working_day = None
                
                day = developer.load[-1].day
                while day < end:
                    day = min(day + one_day, end)
                    load_info = dummy_load.copy()
                    load_info.day = day
                    developer.load.append(load_info)
        
        def _extrapolate_load_data_for_developers(self, developers, 
                                                  end):
            """Extrapolate the load data for the rest of the sprint for all
            developers."""
            for developer in developers:
                self._extrapolate_load_data_for_single_developer(developer, end)
        
        def _create_new_developer(self, name, rt_series):
            developer = ValueObject(dict(name=name, load=[]))
            for day, foo in rt_series:
                day_load = ValueObject(dict(day=day, remaining_time=0, is_working_day=None, is_overloaded=None))
                developer.load.append(day_load)
            return developer
        
        def _add_remaining_time_for_day_to_developer_load(self, developer, day, time_per_developer):
            for load_data in developer.load:
                if load_data.day == day:
                    load_data.remaining_time += time_per_developer
                    return
            raise AssertionError('Day %s not found in load data' % day)
        
        def _map_remaining_time_to_developers(self, rt_series_by_task, start, end):
            """Given the remaining time for all tasks to consider, calculate the
            resource load for all involved developers."""
            all_developers = {}
            
            for ticket, rt_series in rt_series_by_task.items():
                developers = ticket.get_resource_list(include_owner=True)
                if len(developers) == 0:
                    developers.append(u'not assigned')
                
                for day, remaining_time in rt_series:
                    time_per_developer = float(remaining_time) / len(developers)
                    
                    for name in developers:
                        if name not in all_developers:
                            developer = self._create_new_developer(name, rt_series)
                            all_developers[name] = developer
                        
                        developer = all_developers[name]
                        self._add_remaining_time_for_day_to_developer_load(developer, day, time_per_developer)
            return all_developers
        
        def _calculate_capacity_for_developer(self, dev, hours_by_day):
            dev.calendar = hours_by_day
            day_sequence = sorted(hours_by_day.keys())
            capacity_hours = [hours_by_day[day] for day in day_sequence]
            dev.total_capacity = sum(capacity_hours)
            if dev.load is not None:
                for i, (day, load) in enumerate(zip(day_sequence, dev.load)):
                    remaining_capacity = sum(capacity_hours[i:])
                    load.is_overloaded = (load.remaining_time > remaining_capacity)
        
        def _add_capacity_info_for_single_developer(self, dev, member, start, end):
            if dev is None:
                dev = ValueObject(dict(name=member.name, load=None))
            dev.full_name = member.full_name
            dev.email = member.email
            
            hours_by_day = member.calendar.get_hours_for_interval(start, end)
            self._calculate_capacity_for_developer(dev, hours_by_day)
            return dev
        
        def _build_dummy_calendar(self, start, end):
            hours_by_day = dict()
            day = start.date()
            while day <= end.date():
                hours_by_day[day] = 6
                day += timedelta(days=1)
            return hours_by_day
        
        def _add_capacity_information(self, env, team_name, developers_by_name, start, end):
            # TODO:
            from agilo.scrum import TeamController
            cmd = TeamController.GetTeamCommand(env, team=team_name)
            cmd.native = True
            team = TeamController(env).process_command(cmd)
            for member in team.members:
                dev = developers_by_name.get(member.name)
                dev = self._add_capacity_info_for_single_developer(dev, member, start, end)
                developers_by_name[member.name] = dev
            
            for developer in developers_by_name.values():
                if developer.name == 'not assigned':
                    continue
                if not hasattr(developer, 'calendar'):
                    hours_by_day = self._build_dummy_calendar(start, end)
                    self._calculate_capacity_for_developer(developer, hours_by_day)
        
        def get_load_series_for_interval(self, env, start, end, sp_controller):
            sprint = self.sprint
            rt_series_by_task = \
                self.build_remaining_time_series_for_interval(env, start, end, 
                                     sprint, sp_controller, cut_to_today=False)
            developers_by_name = \
                self._map_remaining_time_to_developers(rt_series_by_task,
                                                       start, end)
            self._add_capacity_information(env, sprint.team.name, 
                                           developers_by_name, start, end)
            developers = developers_by_name.values()
            self._extrapolate_load_data_for_developers(developers, end)
            return developers
        
        def _calculate_total_load_per_day(self, developers):
            totals_by_day = {}
            for developer in developers:
                if developer.load is not None:
                    for load in developer.load:
                        if load.day not in totals_by_day:
                            totals_by_day[load.day] = 0
                        totals_by_day[load.day] += load.remaining_time
            return [totals_by_day[day] for day in sorted(totals_by_day)]
        
        def _execute(self, sp_controller, date_converter, as_key):
            assert self.sprint.start != None
            assert self.sprint.end != None
            assert self.sprint.start <= self.sprint.end
            developers = self.get_load_series_for_interval(sp_controller.env, 
                            self.sprint.start, self.sprint.end, sp_controller)
            load_totals = self._calculate_total_load_per_day(developers)
            return ValueObject(developers=developers, load_totals=load_totals)
    
    
    class GetResourceStatsCommand(GetResourceLoadForDevelopersInSprintCommand):
        """Get the resources statistics for this Sprint, taking care
        of calculating the relative load, compared to the total 
        remaining time for the sprint"""
        
        def _execute(self, sp_controller, date_converter, as_key):
            env = sp_controller.env
            
            cmd = SprintController.GetTotalRemainingTimeCommand
            cmd_total_rt = cmd(env, sprint=self.sprint, 
                               tickets=self.tickets)
            total_remaining_time = sp_controller.process_command(cmd_total_rt)
            
            load_per_developer = {}
            if total_remaining_time > 0:
                today = datefmt.parse_date('now')
                developers = self.get_load_series_for_interval(env, 
                                                               today, 
                                                               today, 
                                                               sp_controller)
                
                for dev in developers:
                    if dev.load is not None:
                        remaining = dev.load[0].remaining_time
                        percentage = int(round(float(remaining) / \
                                               float(total_remaining_time) * \
                                               100))
                        load_per_developer[dev.name] = percentage
            
            return ValueObject(load_per_developer)
    
    
    class GetSprintIdealCapacityCommand(controller.ICommand):
        """Get the Sprint ideal capacity based on the assigned team, 
        without removing the Contingent planned for this sprint."""
        
        parameters = {'sprint': validator.MandatorySprintWithTeamValidator}
        
        def _calculate_capacity(self):
            capacity = 0
            for member in self.sprint.team.members:
                capacity_hours = member.calendar.get_hours_for_interval(self.sprint.start, 
                                                                        self.sprint.end)
                capacity += sum(capacity_hours.values())
            return capacity
        
        def _execute(self, sp_controller, date_converter, as_key):
            return {'capacity': self._calculate_capacity()}
    
    
    class GetSprintNetCapacityCommand(GetSprintIdealCapacityCommand):
        """Get the Sprint net capacity, removing also the already planned
        contingent for this sprint."""
        
        def _execute(self, sp_controller, date_converter, as_key):
            from agilo.scrum.contingent import ContingentController
            ideal_capacity = self._calculate_capacity()
            get_contingent_total = ContingentController.GetSprintContingentTotalsCommand(sp_controller.env,
                                                                                         sprint=self.sprint)
            contingent = ContingentController(sp_controller.env).process_command(get_contingent_total)
            net_capacity = ideal_capacity - contingent.amount
            return net_capacity

