#@+leo-ver=4
#@+node:@file resource.py
#@@language python
#@<< Copyright >>
#@+node:<< Copyright >>
############################################################################
#   Copyright (C) 2005, 2006, 2007, 2008 by Reithinger GmbH
#   mreithinger@web.de
#
#   This file is part of faces.
#
#   faces 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.
#
#   faces 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.,
#   59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
############################################################################

#@-node:<< Copyright >>
#@nl
#@<< Imports >>
#@+node:<< Imports >>
import pcalendar
import datetime
import utils
import string
import bisect
import plocale
#@-node:<< Imports >>
#@nl

_is_source = True
_to_datetime = pcalendar.to_datetime
_ = plocale.get_gettext()

#@+others
#@+node:_isattrib
#@+doc
#@nonl
# is used to find snapshot attributes
#@-doc
#@@code
def _isattrib(obj, a):
    return a[0] != "_" \
           and not callable(getattr(obj, a)) \
           and not a.endswith("_members") \
           and a not in ("name")
#@-node:_isattrib
#@+node:class ResourceCalendar
class ResourceCalendar(object):
    """
    The resource calendar saves the load time of a resource.
    Is ia sequence of time intervals of loads. An example of
    such a sequence is:
        [ (datetime.min, 0),
          (2006/1/1, 1.0),
          (2006/1/10, 0.5),
          (2006/1/15, 0) ]

    That means the resource:
        is free till january the first 2006
        is fully booked from january the first to january 10th
        is half booked from january 10th to january 15th
        is free since january 15th
    """

    #@	@+others
    #@+node:__init__
    def __init__(self, src=None):
        if src:
            self.bookings = list(src.bookings)
        else:
            self.bookings = [ (datetime.datetime.min, 0) ]
    #@-node:__init__
    #@+node:__str__
    def __str__(self):
        return str(self.bookings)
    #@-node:__str__
    #@+node:__repr__
    def __repr__(self):
        return "<ResourceCalendar %s>" % (str(self))
    #@-node:__repr__
    #@+node:add_load
    def add_load(self, start, end, load):
        start = _to_datetime(start)
        end = _to_datetime(end)

        bookings = self.bookings

        # the load will be converted in an integer to avoid
        # rouning problems
        load = int(load * 10000)

        start_item = (start, 0)
        start_pos = bisect.bisect_left(bookings, start_item)

        left_load = 0
        left_load = bookings[start_pos - 1][1]

        if start_pos < len(bookings) and bookings[start_pos][0] == start:
            prev_load = bookings[start_pos][1]
            if prev_load + load == left_load:
                del bookings[start_pos]
            else:
                bookings[start_pos] = (start, prev_load + load)
                start_pos += 1
        else:
            bookings.insert(start_pos, (start, load + left_load))
            start_pos += 1

        item = (datetime.datetime.min, 0)
        for i in range(start_pos, len(bookings)):
            end_pos = i
            item = bookings[i]
            if item[0] >= end: break
            bookings[i] = (item[0], item[1] + load)
        else:
            end_pos = len(bookings)

        left_load = bookings[end_pos - 1][1]
        if item[0] == end:
            if item[1] == left_load:
                del bookings[end_pos]
        else:
            bookings.insert(end_pos, (end, left_load - load))
    #@-node:add_load
    #@+node:end_of_booking_interval
    def end_of_booking_interval(self, date):
        date = _to_datetime(date)
        bookings = self.bookings
        date_item = (date, 999999)
        date_pos = bisect.bisect_left(bookings, date_item) - 1
        next_date = datetime.datetime.max
        load = 0

        try:
            book_item = bookings[date_pos]
            load = bookings[date_pos][1] / 10000.0
            next_date = bookings[date_pos + 1][0]
        except:
            pass

        return next_date, load
    #@-node:end_of_booking_interval
    #@+node:find_free_time
    def find_free_time(self, start, length, load, max_load):
        bookings = self.bookings

        if isinstance(start, datetime.datetime):
            adjust_date = _to_datetime
        else:
            adjust_date = start.calendar.EndDate

        start = _to_datetime(start)
        load = int(load * 10000)
        max_load = int(max_load * 10000)
        lb = len(bookings)

        def next_possible(index):
            while index < lb:
                sd, lo = bookings[index]
                if lo + load <= max_load:
                    break

                index += 1

            sd = adjust_date(max(start, sd))
            ed = sd + length
            end = _to_datetime(ed)

            index += 1
            while index < lb:
                date, lo = bookings[index]

                if date >= end:
                    #I found a good start date
                    return None, sd

                if lo + load > max_load:
                    return index + 1, None

                index += 1

            return None, sd

        start_item = (start, 1000000)
        i = bisect.bisect_left(bookings, start_item) - 1

        next_start = None
        while not next_start and i < lb:
            i, next_start = next_possible(i)

        assert(next_start is not None)
        return next_start
    #@-node:find_free_time
    #@+node:get_bookings
    def get_bookings(self, start, end):
        start = _to_datetime(start)
        end = _to_datetime(end)
        bookings = self.bookings
        start_item = (start, 0)
        start_pos = bisect.bisect_left(bookings, start_item)
        if start_pos >= len(bookings) or bookings[start_pos][0] > start:
            start_pos -= 1

        end_item = (end, 0)
        end_pos = bisect.bisect_left(bookings, end_item)
        return start_pos, end_pos, bookings
    #@-node:get_bookings
    #@+node:get_load
    def get_load(self, date):
        date = _to_datetime(date)
        bookings = self.bookings
        item = (date, 100000)
        pos = bisect.bisect_left(bookings, item) - 1
        return bookings[pos][1] / 10000.0
    #@-node:get_load
    #@-others
#@-node:class ResourceCalendar
#@+node:class _ResourceBase
class _ResourceBase(object):
    pass

#@-node:class _ResourceBase
#@+node:class _MetaResource
class _MetaResource(type):
    doc_template = """
    A resource class. The resources default attributes can
    be changed when the class ist instanciated, i.e.
    %(name)s(max_load=2.0)

    @var max_load:
    Specify the maximal allowed load sum of all simultaneously
    allocated tasks of a resource. A ME{max_load} of 1.0 (default)
    means the resource may be fully allocated. A ME{max_load} of 1.3
    means the resource may be allocated with 30%% overtime.

    @var title:
    Specifies an alternative more descriptive name for the task.

    @var efficiency:
    The efficiency of a resource can be used for two purposes. First
    you can use it as a crude way to model a team. A team of 5 people
    should have an efficiency of 5.0. Keep in mind that you cannot
    track the member of the team individually if you use this
    feature. The other use is to model performance variations between
    your resources.

    @var vacation:
    Specifies the vacation of the resource. This attribute is
    specified as a list of date literals or date literal intervals.
    Be aware that the end of an interval is excluded, i.e. it is
    the first working date.
    """

    #@	@+others
    #@+node:__init__
    def __init__(self, name, bases, dict_):
        super(_MetaResource, self).__init__(name, bases, dict_)
        self.name = name
        self.title = dict_.get("title", name)
        self._calendar = { None: ResourceCalendar() }
        self._tasks = { }
        self.__set_vacation()
        self.__add_resource(bases[0])
        self.__doc__ = dict_.get("__doc__", self.doc_template) % locals()
    #@-node:__init__
    #@+node:__or__
    def __or__(self, other):
        return self().__or__(other)
    #@-node:__or__
    #@+node:__and__
    def __and__(self, other):
        return self().__and__(other)
    #@-node:__and__
    #@+node:__cmp__
    def __cmp__(self, other):
        return cmp(self.name, getattr(other, "name", None))
    #@-node:__cmp__
    #@+node:__repr__
    def __repr__(self):
        return "<Resource %s>" % self.name
    #@-node:__repr__
    #@+node:__str__
    def __str__(self):
        return repr(self)
    #@-node:__str__
    #@+node:__set_vacation
    def __set_vacation(self):
        vacation = self.vacation

        if isinstance(vacation, (tuple, list)):
            for v in vacation:
                if isinstance(v, (tuple, list)):
                    self.add_vacation(v[0], v[1])
                else:
                    self.add_vacation(v)
        else:
            self.add_vacation(vacation)
    #@-node:__set_vacation
    #@+node:__add_resource
    def __add_resource(self, base):
        if issubclass(base, _ResourceBase):
            members = getattr(base, base.__name__ + "_members", [])
            members.append(self)
            setattr(base, base.__name__ + "_members", members)
    #@-node:__add_resource
    #@+node:get_members
    def get_members(self):
        return getattr(self, self.__name__ + "_members", [])
    #@-node:get_members
    #@+node:add_vacation
    def add_vacation(self, start, end=None):
        start_date = _to_datetime(start)

        if not end:
            end_date = start_date.replace(hour=23, minute=59)
        else:
            end_date = _to_datetime(end)

        for cal in self._calendar.itervalues():
            cal.add_load(start_date, end_date, 1)

        tp = Booking()
        tp.start = start_date
        tp.end = end_date
        tp.book_start = start_date
        tp.book_end = end_date
        tp.work_time = end_date - start_date
        tp.load = 1.0
        tp.name = tp.title = _("(vacation)")
        tp._id = ""
        self._tasks.setdefault("", []).append(tp)
    #@-node:add_vacation
    #@+node:calendar
    def calendar(self, scenario):
        try:
            return self._calendar[scenario]
        except KeyError:
            cal = self._calendar[scenario] = ResourceCalendar(self._calendar[None])
            return cal

    #@-node:calendar
    #@-others
#@-node:class _MetaResource
#@+node:make_team
def make_team(resource):
    members = resource.get_members()
    if not members:
        return resource

    result = make_team(members[0])
    for r in members[1:]:
        result = result & make_team(r)

    return result
#@-node:make_team
#@+node:class Booking
class Booking(object):
    """
    A booking unit for a task.
    """
    #@	<< declarations >>
    #@+node:<< declarations >>
    book_start = datetime.datetime.min
    book_end = datetime.datetime.max
    actual = False
    _id = ""

    #@-node:<< declarations >>
    #@nl
    #@	@+others
    #@+node:__init__
    def __init__(self, task=None):
        self.__task = task
    #@-node:__init__
    #@+node:__cmp__
    def __cmp__(self, other):
        return cmp(self._id, other._id)
    #@-node:__cmp__
    #@+node:path
    def path(self):
        first_dot = self._id.find(".")
        return "root" + self._id[first_dot:]

    path = property(path)
    #@nonl
    #@-node:path
    #@+node:_idendity_
    def _idendity_(self):
        return self._id
    #@-node:_idendity_
    #@+node:__getattr__
    def __getattr__(self, name):
        if self.__task:
            return getattr(self.__task, name)

        raise AttributeError("'%s' is not a valid attribute" % (name))
    #@-node:__getattr__
    #@-others
#@-node:class Booking
#@+node:class ResourceList
class ResourceList(list):
    #@	@+others
    #@+node:__init__
    def __init__(self, *args):
        if args: self.extend(args)
    #@-node:__init__
    #@-others
#@-node:class ResourceList
#@+node:class Resource
class Resource(_ResourceBase):
    #@	<< declarations >>
    #@+node:<< declarations >>
    __metaclass__ = _MetaResource
    __attrib_completions__ = {\
        "max_load": 'max_load = ',
        "title":  'title = "|"',
        "efficiency": 'efficiency = ',
        "vacation": 'vacation = [("|2002-02-01", "2002-02-05")]' }

    __type_image__ = "resource16"

    max_load = None # the maximum sum load for all task
    vacation = ()
    efficiency = 1.0


    #@-node:<< declarations >>
    #@nl
    #@	@+others
    #@+node:__init__
    def __init__(self, **kwargs):
        for k, v in kwargs.iteritems():
            setattr(self, k, v)
    #@-node:__init__
    #@+node:_idendity_
    def _idendity_(cls):
        return "resource:" + cls.__name__

    _idendity_ = classmethod(_idendity_)
    #@-node:_idendity_
    #@+node:__repr__
    def __repr__(self):
        return "<Resource %s>" % self.__class__.__name__
    #@-node:__repr__
    #@+node:__str__
    def __str__(self):
        return repr(self)
    #@-node:__str__
    #@+node:__call__
    def __call__(self):
        return self
    #@-node:__call__
    #@+node:__hash__
    def __hash__(self):
        return hash(self.__class__)
    #@-node:__hash__
    #@+node:__cmp__
    def __cmp__(self, other):
        return cmp(self.name, other.name)
    #@-node:__cmp__
    #@+node:__or__
    def __or__(self, other):
        if type(other) is _MetaResource:
            other = other()

        result = Resource()
        result._subresource = _OrResourceGroup(self, other)
        return result
    #@-node:__or__
    #@+node:__and__
    def __and__(self, other):
        if type(other) is _MetaResource:
            other = other()

        result = Resource()
        result._subresource = _AndResourceGroup(self, other)
        return result
    #@-node:__and__
    #@+node:_permutation_count
    def _permutation_count(self):
        if hasattr(self, "_subresource"):
            return self._subresource._permutation_count()

        return 1
    #@-node:_permutation_count
    #@+node:_get_resources
    def _get_resources(self, state):
        if hasattr(self, "_subresource"):
            result = self._subresource._get_resources(state)

            if self.name != "Resource":
                result.name = self.name

            if self.title != "Resource":
                result.title = self.title

            return result

        result = ResourceList(self)
        return result
    #@-node:_get_resources
    #@+node:all_members
    def all_members(self):
        if hasattr(self, "_subresource"):
            return self._subresource.all_members()

        return [ self.__class__ ]
    #@-node:all_members
    #@+node:unbook_tasks_of_project
    def unbook_tasks_of_project(cls, project_id, scenario):
        try:
            task_list = cls._tasks[scenario]
        except KeyError:
            return

        add_load = cls.calendar(scenario).add_load
        for task_id, bookings in task_list.items():
            if task_id.startswith(project_id):
                for item in bookings:
                    add_load(item.book_start, item.book_end, -item.load)

                del task_list[task_id]

        if not task_list:
            del cls._tasks[scenario]

    unbook_tasks_of_project = classmethod(unbook_tasks_of_project)
    #@-node:unbook_tasks_of_project
    #@+node:unbook_task
    def unbook_task(cls, task):
        identdity = task._idendity_()
        scenario = task.scenario

        try:
            task_list = cls._tasks[scenario]
            bookings = task_list[identdity]
        except KeyError:
            return

        add_load = cls.calendar(scenario).add_load
        for b in bookings:
            add_load(b.book_start, b.book_end, -b.load)

        del task_list[identdity]
        if not task_list:
            del cls._tasks[scenario]

    unbook_task = classmethod(unbook_task)
    #@-node:unbook_task
    #@+node:correct_bookings
    def correct_bookings(cls, task):
        #correct the booking data with the actual task data
        try:
            tasks = cls._tasks[task.scenario][task._idendity_()]
        except KeyError:
            return

        for t in tasks:
            t.start = task.start.to_datetime()
            t.end = task.end.to_datetime()

    correct_bookings = classmethod(correct_bookings)
    #@-node:correct_bookings
    #@+node:book_task
    def book_task(cls, task, start, end, load, work_time, actual):
        if not work_time: return

        start = _to_datetime(start)
        end = _to_datetime(end)

        identdity = task._idendity_()
        task_list = cls._tasks.setdefault(task.scenario, {})
        bookings = task_list.setdefault(identdity, [])
        add_load = cls.calendar(task.scenario).add_load

        tb = Booking(task)
        tb.book_start = start
        tb.book_end = end
        tb._id = identdity
        tb.load = load
        tb.start = _to_datetime(task.start)
        tb.end = _to_datetime(task.end)
        tb.title = task.title
        tb.name = task.name
        tb.work_time = int(work_time)
        tb.actual = actual
        bookings.append(tb)
        result = add_load(start, end, load)
        return result

    book_task = classmethod(book_task)
    #@-node:book_task
    #@+node:length_of
    def length_of(cls, task):
        cal = task.root.calendar
        bookings = cls.get_bookings(task)
        return sum(map(lambda b: task._to_delta(b.work_time).round(), bookings))

    length_of = classmethod(length_of)
    #@-node:length_of
    #@+node:done_of
    def done_of(self, task):
        cal = task.root.calendar
        now = cal.now
        bookings = self.get_bookings(task)

        if task.__dict__.has_key("effort"):
            efficiency = self.efficiency * task.efficiency
        else:
            efficiency = 1

        def book_done(booking):
            if booking.book_start >= now:
                return 0

            factor = 1
            if booking.book_end > now:
                start = task._to_start(booking.book_start)
                end = task._to_end(booking.book_end)
                cnow = task._to_start(now)
                factor = float(cnow - start) / ((end - start) or 1)

            return factor * booking.work_time * efficiency

        return task._to_delta(sum(map(book_done, bookings)))
    #@-node:done_of
    #@+node:todo_of
    def todo_of(self, task):
        cal = task.root.calendar
        now = cal.now

        bookings = self.get_bookings(task)
        if task.__dict__.has_key("effort"):
            efficiency = self.efficiency * task.efficiency
        else:
            efficiency = 1

        def book_todo(booking):
            if booking.book_end <= now:
                return 0

            factor = 1
            if booking.book_start < now:
                start = task._to_start(booking.book_start)
                end = task._to_end(booking.book_end)
                cnow = task._to_start(now)
                factor = float(end - cnow) / ((end - start) or 1)

            return factor * booking.work_time * efficiency

        return task._to_delta(sum(map(book_todo, bookings)))
    #@-node:todo_of
    #@+node:get_bookings
    def get_bookings(cls, task):
        return cls._tasks.get(task.scenario, {}).get(task._idendity_(), ())

    get_bookings = classmethod(get_bookings)
    #@-node:get_bookings
    #@+node:get_bookings_at
    def get_bookings_at(cls, start, end, scenario):
        result = []

        try:
            items = cls._tasks[scenario].iteritems()
        except KeyError:
            return ()

        for task_id, bookings in items:
            result += [ booking for booking in bookings
                        if booking.book_start < end
                        and booking.book_end > start ]

        vacations = cls._tasks.get("", ())
        result += [ booking for booking in vacations
                    if booking.book_start < end
                    and booking.book_end > start ]

        return result

    get_bookings_at = classmethod(get_bookings_at)
    #@-node:get_bookings_at
    #@+node:find_free_time
    def find_free_time(cls, start, length, load, max_load, scenario):
        return cls.calendar(scenario).find_free_time(start, length, load, max_load)

    find_free_time = classmethod(find_free_time)
    #@-node:find_free_time
    #@+node:get_load
    def get_load(cls, date, scenario):
        return cls.calendar(scenario).get_load(date)

    get_load = classmethod(get_load)
    #@-node:get_load
    #@+node:end_of_booking_interval
    def end_of_booking_interval(cls, date, task):
        return cls.calendar(task.scenario).end_of_booking_interval(date)

    end_of_booking_interval = classmethod(end_of_booking_interval)
    #@-node:end_of_booking_interval
    #@+node:snapshot
    def snapshot(self):
        from task import _as_string
        def isattrib(a):
            if a == "max_load" and self.max_load is None: return False
            if a in ("name", "title", "vacation"): return False
            return _isattrib(self, a)

        attribs = filter(isattrib, dir(self))
        attribs = map(lambda a: "%s=%s" % (a, _as_string(getattr(self, a))),
                      attribs)

        return self.name + "(%s)" % ", ".join(attribs)
    #@-node:snapshot
    #@-others
#@-node:class Resource
#@+node:class _ResourceGroup


class _ResourceGroup(object):
    #@	@+others
    #@+node:__init__
    def __init__(self, *args):
        self.resources = []
        for a in args:
            self.__append(a)
    #@-node:__init__
    #@+node:all_members
    def all_members(self):
        group = reduce(lambda a, b: a + b.all_members(),
                       self.resources, [])
        group = map(lambda r: (r, True), group)
        group = dict(group)
        group = group.keys()
        return group
    #@-node:all_members
    #@+node:_permutation_count
    def _permutation_count(self):
        abstract
    #@-node:_permutation_count
    #@+node:_refactor
    def _refactor(self, arg):
        pass
    #@-node:_refactor
    #@+node:__append
    def __append(self, arg):
        if isinstance(arg, self.__class__):
            self.resources += arg.resources
            for r in arg.resources:
                self._refactor(r)
            return
        elif isinstance(arg, Resource):
            subresources = getattr(arg, "_subresource", None)
            if subresources:
                self.__append(subresources)
                return
            else:
                self.resources.append(arg)
        else:
            assert(isinstance(arg, _ResourceGroup))
            self.resources.append(arg)

        self._refactor(arg)
    #@-node:__append
    #@+node:__str__
    def __str__(self):
        op = lower(self.__class__.__name__[0:-13])
        return "(" + \
               string.join([str(r) for r in self.resources],
                           " " + op + " ") + \
               ")"
    #@-node:__str__
    #@-others
#@-node:class _ResourceGroup
#@+node:class _OrResourceGroup


class _OrResourceGroup(_ResourceGroup):
    #@	@+others
    #@+node:_get_resources
    def _get_resources(self, state):
        for r in self.resources:
            c = r._permutation_count()
            if c <= state:
                state -= c
            else:
                return r._get_resources(state)

        assert(0)
    #@-node:_get_resources
    #@+node:_permutation_count
    def _permutation_count(self):
        return sum([ r._permutation_count() for r in self.resources])
    #@-node:_permutation_count
    #@-others
#@-node:class _OrResourceGroup
#@+node:class _AndResourceGroup


class _AndResourceGroup(_ResourceGroup):
    #@	@+others
    #@+node:__init__
    def __init__(self, *args):
        self.factors = [ 1 ]
        _ResourceGroup.__init__(self, *args)
    #@-node:__init__
    #@+node:_refactor
    def _refactor(self, arg):
        count = arg._permutation_count()
        self.factors = [ count * f for f in self.factors ]
        self.factors.append(1)
    #@-node:_refactor
    #@+node:_permutation_count
        #print "AndResourceGroup", count, arg, self.factors


    def _permutation_count(self):
        return self.factors[0]
    #@-node:_permutation_count
    #@+node:_get_resources
    def _get_resources(self, state):
        """delivers None when there are duplicate resources"""
        result = []
        for i in range(1, len(self.factors)):
            f = self.factors[i]
            substate = state / f
            state %= f
            result.append(self.resources[i - 1]._get_resources(substate))

        result = ResourceList(*list(utils.flatten(result)))
        dupl_test = { }
        for r in result:
            if dupl_test.has_key(r):
                return None
            else:
                dupl_test[r] = 1

        return result
    #@-node:_get_resources
    #@+node:_has_duplicates
    def _has_duplicates(self, state):
        resources = self._get_resources(state)
        tmp = { }
        for r in resources:
            if tmp.has_key(r):
                return True

            tmp[r] = 1

        return False
    #@-node:_has_duplicates
    #@-others
#@-node:class _AndResourceGroup
#@-others
#@-node:@file resource.py
#@-leo

# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
