# Calendaring is a simple CMF/Plone calendaring implementation.
# Copyright (C) 2004 Enfold Systems
#
# 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
# Some portions of this module are Copyright Shuttleworth Foundation.
# The original copyright statement is reproduced below.
#
# SchoolTool - common information systems platform for school administration
# Copyright (c) 2003 Shuttleworth Foundation
#
# 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
"""
$Id: calendar.py,v 1.4 2005/01/25 01:03:49 dreamcatcher Exp $
"""
from Products.CMFCalendar.CalendarTool import calendar
from OFS.PropertyManager import PropertyManager

from datetime import datetime, date
from types import ListType
from rfc822 import parseaddr, dump_address_pair
from BTrees.OOBTree import OOSet

from Globals import InitializeClass
from Acquisition import aq_base
from AccessControl import ClassSecurityInfo, Unauthorized

from Products.CMFCore.utils import getToolByName
from Products.CMFCore.WorkflowCore import WorkflowException
from Products.CMFPlone.CalendarTool import CalendarTool as BaseTool
from Products.Calendaring.parser import ICalReader, \
     ICalParseError, ical_text, ical_duration, Person
from Products.Calendaring.common import dt2DT
from Products.Calendaring.model import getView
from Products.Calendaring.model.traversal import ModelViewMixin

UID_ATTR = '_event_uid'

def generateUID(event):
    uid = getattr(aq_base(event), UID_ATTR, None)
    if uid is not None:
        return uid
    id = event.getId()
    if id.endswith('RID'):
        return id
    h_event = abs(hash(repr(event)))
    h_id = abs(hash(id))
    return "%s-%s-RID" % (h_event, h_id)

def generateId(event):
    uid = event.getProp('uid', None)
    if uid:
        return uid
    title = event.summary.lower().replace(' ', '_')
    time = ''.join(tuple(map(str, event.dtstart.timetuple()[:5])))
    return '_'.join((title[:15], time))

def person2text(value):
    text = value.name
    if value.mail:
        text = dump_address_pair((text, value.mail))
    return text

def convert(value):
    if type(value) is not ListType:
        return _convert(value)
    return map(_convert, value)

def _convert(value):
    for t, c in converters.items():
        if isinstance(value, t):
            return c(value)
    return value


status_mapping = {'TENTATIVE':('submit', 'reject', 'retract', 'show'),
                  'CONFIRMED':('publish',),
                  'CANCELLED':('hide',),
                  }
re_status_mapping = {'published':'CONFIRMED',
                     'pending':'TENTATIVE',
                     'visible':'TENTATIVE',
                     'private':'CANCELLED'}

converters = {datetime : dt2DT,
              date : dt2DT,
              Person : person2text}


kw_mappings = {'cmf':{'dtstart':'start_date',
                      'dtend':'end_date',
                      'summary':'title',
                      'description':'description'},
               'atct':{'dtstart':'startDate',
                       'dtend':'endDate',
                       'summary':'title',
                       'description':'description',
                       'location':'location',
                       'attendee':'attendees',
                       'categories':'subject'}
               }

class CalendarTool(ModelViewMixin, BaseTool, PropertyManager):
    """ A Calendar Tool implementation
    for import/export of events.
    """
    security = ClassSecurityInfo()

    _kw_map = kw_mappings['atct']
    event_type = 'Event'
    _properties = getattr(BaseTool, '_properties', ()) + (
        {'id':'event_type', 'type': 'string', 'mode':'w'},
        {'id':'published_states', 'type': 'lines', 'mode':'w'},)

    manage_options = BaseTool.manage_options + PropertyManager.manage_options

    def __init__(self):
        if hasattr(BaseTool, '__init__'):
            # Plone 2.1 doesn't seem to have a __init__ anymore.
            BaseTool.__init__(self)
        self._registry = OOSet()

    def manage_afterAdd(self, item, container):
        item.manage_changeProperties({'published_states' : ['published']})

    security.declarePrivate('setMapping')
    def setMapping(self, name):
        self._kw_map = kw_mappings[name]

    security.declarePublic('getDestination')
    def getDestination(self):
        mt = getToolByName(self, 'portal_membership')
        return mt.getHomeFolder()

    security.declarePublic('importCalendar')
    def importCalendar(self, stream, dest=None, kw_map=None, do_action=False):
        items = []
        mt = getToolByName(self, 'portal_membership')
        if mt.isAnonymousUser():
            raise Unauthorized
        wt = getToolByName(self, 'portal_workflow')
        if dest is None:
            dest = self.getDestination()
        reader = ICalReader(stream)
        if kw_map is None:
            kw_map = self._kw_map
        for ev in reader.iterEvents():
            id = generateId(ev)
            info = {'id': id}
            for k, v in kw_map.items():
                value = getattr(ev, k, ev.getProp(k, default=None))
                if value is None:
                    continue
                value = convert(value)
                info[v] = value

            if id in dest.objectIds():
                obj = dest._getOb(id)
                obj.update(**info)
            else:
                dest.invokeFactory(self.event_type, **info)
                obj = dest._getOb(id)
            uid = getattr(ev, 'uid', None)
            if uid is None:
                uid = generateUID(obj)
            setattr(obj, UID_ATTR, uid)
            items.append((id, obj))

            if do_action:
                # XXX NAAAAASTY Code. We need to create a special
                # workflow for Event.
                status = ev.getOne('status', default=None)
                actions = status_mapping.get(status, 'publish')
                for action in actions:
                    try:
                        wt.doActionFor(obj, action)
                    except WorkflowException:
                        # Try to revert to visible
                        for a in ('retract', 'show'):
                            try:
                                wt.doActionFor(obj, 'retract')
                            except WorkflowException:
                                pass
                        try:
                            wt.doActionFor(obj, action)
                        except WorkflowException:
                            pass
                        else:
                            break
                    else:
                        break
            obj.reindexObject(idxs=['Title',
                                    'start',
                                    'end'])
        return items


    def exportCalendar(self, events=None, REQUEST=None):
        wf = getToolByName(self, 'portal_workflow')
        ut = getToolByName(self, 'portal_url')
        if events is None:
            events = []
        dtstamp = datetime.utcnow().strftime("%Y%m%dT%H%M%SZ")
        uid_suffix = "%s" % ut()
        result = [
            "BEGIN:VCALENDAR",
            "PRODID:-//Runyaga.org/NONSGML Calendaring Tool//EN",
            "VERSION:1.0",
        ]

        uid_hash = None
        events.sort()
        for event in events:
            uid_hash = abs(hash(event))
            state = wf.getInfoFor(event, 'review_state')
            status = re_status_mapping.get(state, 'TENTATIVE')
            result += [
                "BEGIN:VEVENT",
                "UID:%s" % generateUID(event),
                "SUMMARY:%s" % ical_text(event.Title()),
                "DESCRIPTION:%s" % ical_text(event.Description())
                ]
            location = event.getLocation()
            if location:
                result += [
                    "LOCATION:%s" % ical_text(location)
                    ]
            if hasattr(aq_base(event), 'getAttendees'):
                for att in event.getAttendees():
                    name, email = parseaddr(att)
                    if not name:
                        name = email
                        email = ''
                    if email:
                        email = 'mailto:%s' % email
                    else:
                        email = 'invalid:nomail'
                    result += [
                        "ATTENDEE;CN=\"%s\":%s" % (
                        ical_text(name), email)
                        ]
            if event.duration == date.resolution:
                # All day event.
                result += [
                    "DTSTART;VALUE=DATE:%s" % event.start_date.strftime('%Y%m%d'),
                    "DTEND;VALUE=DATE:%s" % event.end_date.strftime('%Y%m%d'),
                    ]
            else:
                # Normal event
                result += [
                    "DTSTART:%s" % event.start_date.strftime('%Y%m%dT%H%M%S'),
                    "DTEND:%s" % event.end_date.strftime('%Y%m%dT%H%M%S'),
                    ]
            result += [
                "DTSTAMP:%s" % dtstamp,
                "STATUS:%s" % status,
                "END:VEVENT",
                ]
        if uid_hash is None:
            # There were no events.  iCalendar spec (RFC 2445) requires
            # VCALENDAR to have at least one subcomponent.  Let's create
            # a fake event.
            # NB Mozilla Calendar produces a 0-length file when publishing
            # empty calendars.  Sadly it does not then accept them
            # (http://bugzilla.mozilla.org/show_bug.cgi?id=229266).
            result += [
                "BEGIN:VEVENT",
                "UID:placeholder-%s" % uid_suffix,
                "SUMMARY:%s" % ical_text("Empty calendar"),
                "DTSTART:%s" % dtstamp[:-1],
                "DTSTAMP:%s" % dtstamp,
                "END:VEVENT",
            ]
        result.append("END:VCALENDAR")
        data = "\r\n".join(result)
        if REQUEST is not None:
            REQUEST.RESPONSE.setHeader('Content-Type',
                                       'text/calendar; charset=UTF-8')
            REQUEST.RESPONSE.write(data)
        return data

    security.declarePublic('getPublishedStates')
    def getPublishedStates(self):
        return self.getProperty('published_states')

    # catalog_getevents is copied verbatim from CMFCalendar
    # the only thing changed is the possiblility to set a published state
    security.declarePublic('catalog_getevents')
    def catalog_getevents(self, year, month):
        """ given a year and month return a list of days that have events """
        year=int(year)
        month=int(month)
        last_day=calendar.monthrange(year, month)[1]
        first_date=self.getBeginAndEndTimes(1, month, year)[0]
        last_date=self.getBeginAndEndTimes(last_day, month, year)[1]

        published = self.getPublishedStates()

        query=self.portal_catalog(portal_type=self.calendar_types,
                                  review_state=published,
                                  start={'query': last_date,
                                         'range': 'max'},
                                  end={'query': first_date,
                                       'range': 'min'},
                                  sort_on='start')

        # compile a list of the days that have events
        eventDays={}
        for daynumber in range(1, 32): # 1 to 31
            eventDays[daynumber] = {'eventslist':[], 'event':0, 'day':daynumber}
        includedevents = []
        for result in query:
            if result.getRID() in includedevents:
                break
            else:
                includedevents.append(result.getRID())
            event={}
            # we need to deal with events that end next month
            if  result.end.month() != month:  # doesn't work for events that last ~12 months - fix it if it's a problem, otherwise ignore
                eventEndDay = last_day
                event['end'] = None
            else:
                eventEndDay = result.end.day()
                event['end'] = result.end.Time()
            # and events that started last month
            if result.start.month() != month:  # same as above re: 12 month thing
                eventStartDay = 1
                event['start'] = None
            else:
                eventStartDay = result.start.day()
                event['start'] = result.start.Time()
            event['title'] = result.Title or result.id
            if eventStartDay != eventEndDay:
                allEventDays = range(eventStartDay, eventEndDay+1)
                eventDays[eventStartDay]['eventslist'].append({'end':None, 'start':result.start.Time(), 'title':result.Title})
                eventDays[eventStartDay]['event'] = 1
                for eventday in allEventDays[1:-1]:
                    eventDays[eventday]['eventslist'].append({'end':None, 'start':None, 'title':result.Title})
                    eventDays[eventday]['event'] = 1
                eventDays[eventEndDay]['eventslist'].append({'end':result.end.Time(), 'start':None, 'title':result.Title})
                eventDays[eventEndDay]['event'] = 1
            else:
                eventDays[eventStartDay]['eventslist'].append(event)
                eventDays[eventStartDay]['event'] = 1
            # This list is not uniqued and isn't sorted
            # uniquing and sorting only wastes time
            # and in this example we don't need to because
            # later we are going to do an 'if 2 in eventDays'
            # so the order is not important.
            # example:  [23, 28, 29, 30, 31, 23]
        return eventDays

InitializeClass(CalendarTool)
