"""Schedule System Models
"""
from django.db import models
from django.contrib.sites.models import Site
from django.conf import settings
from django.contrib.auth.models import User, AnonymousUser, Group
from django.db.models import Q
from django.utils.html import escape
from django.core.urlresolvers import reverse
from django.dispatch import dispatcher
from django import forms
from pycon.propmgr.models import Proposal
from pycon.core import *
from django.core.cache import cache
from pycon.core.validators import *
from django.core.validators import *

from bisect import bisect
import cPickle
import os
import glob

from datetime import datetime, timedelta
import types, re

EVENT_TYPE_CHOICES = Choices([
    ('E', 'Event'),
    ('P', 'Plenary Session'),
    ('B', 'Break or Service'),
    ('M', 'Meeting or Lab'),
    ('S', 'Social Event'),
    ('T', 'Tutorial'),
    ('O', 'Open Space'), ## experimental
    ('X', 'Sprint'),     ## experimental
])

EVENT_TYPES_NON_CHAIRED = ['B', 'M', 'S', 'O', 'T', 'X', 'P']
SELECTED_EVENT_STATUS_CHOICES = Choices([
    (0, 'De-Selected'),
    (1, 'Selected'),
    (2, 'Author'),
    (3, 'Session Chair'),
])


class Room(models.Model):
    """A named room space. Can be a sub-division of another room.
    """
    name = models.CharField(maxlength=50, unique=True)
    parents = models.ManyToManyField('self', null=True, blank=True,
                limit_choices_to={ 'divided': True }, symmetrical=False,
                validator_list=[IllegalIfOtherFieldEquals('divided', 'on',
                                    'A divisable room can not have a parent')],
                related_name='room_divisions_set')
    divided = models.BooleanField(default=False)

    class Admin:
        list_display = ('name', 'divided')
        #ordering = ('parent', 'name', )
        list_filter = ('parents',)

    def __unicode__(self):
        return self.name

class TimeTable(models.Model):
    """Timetable for a given day.

    Provide an ordered listing of rooms and a given date to generate
    a schedule table from.
    """
    name = models.SlugField(maxlength=50, unique=True)
    title = models.CharField(maxlength=500)
    date = models.DateField()
    after = models.TimeField(null=True, blank=True,
                             help_text=('limit to events after this time'))
    before = models.TimeField(null=True, blank=True,
                              help_text=('limit to events before this time'))
    nullroom = models.BooleanField("Include 'null' (break) entries",
                                   help_text=('Some plenary events do not '
                                        'occur in a specified room, include '
                                        'these events.'),
                                   default=True)
    public = models.BooleanField(default=True)

    class Admin:
        list_display = ('name', 'date')
        date_hierarchy = 'date'
    class Meta:
        ordering = ('date', 'name')

class TimeTableColumn(models.Model):
    """Private model for dealing with inlined, ordered room timetable.

    We are using a trick with the admin where the tabular inline edit
    field is garenteed to be ordered, and is ordered properly in the admin.
    """
    time_table=models.ForeignKey(TimeTable,
                                 related_name='room_divisions_set',
                                 edit_inline=models.TABULAR)
    ## add custom validator
    include_parents = models.BooleanField(default=False)
    room = models.ForeignKey(Room, core=True)

    class Meta:
        ordering = ('id',)
        unique_together = (("time_table", "room"),)

class Presenter(models.Model):
    """For Events which have no proposals, but do have presenters.

    (i.e. Keynotes, Tutorials, etc.)
    Either you select a user account in one of the Author based groups,
    or you fill in the information. This way a full django account is not
    needed to have a presenter.

    Makes for some anoying code later on.
    """
    user = models.ForeignKey(User, unique=True, null=True, blank=True,
                             limit_choices_to={'groups__name__in':
                                        ['Authors', 'Presenters', 'Teachers']})
    name = models.CharField(maxlength=30, null=True, blank=True)
    affiliation = models.CharField(maxlength=30, null=True, blank=True)
    url = models.URLField(blank=True, null=True)
    bio = models.TextField(blank=True)
    img = models.URLField(blank=True, null=True)

    class Admin: pass

    def __eq__(self, other):
        if isinstance(other, User) and self.user is not None:
            return self.user == other
        return super(Presenter).__eq__(other)
    @property
    def long_name(self):
        return unicode(self)

    @property
    def html_name(self):
        if self.user:
            return self.user.html_name
        base = escape(self.name)
        url_fmt = r'%s'
        if self.url:
            url_fmt = '<a href="' + self.url + '">%s</a>'
        if self.affiliation:
            return base + " (" + url_fmt%escape(self.affiliation) + ")"
        return url_fmt % base

    def __unicode__(self):
        if self.user:
            return unicode(self.user)
        return self.name

class Event(models.Model):
    """
    """
    type = models.CharField('event type', maxlength=1,
                            choices=EVENT_TYPE_CHOICES,
                            default=EVENT_TYPE_CHOICES.default)
    ## Accepted Talk Event
    proposal = models.ForeignKey(Proposal, null=True, blank=True, unique=True,
                                 validator_list=[IllegalIfOtherFieldsGiven(
                                        ['title', 'duration', 'summary', 'presenters'],
                                        'You cannot supply a proposal if the non-proposed event data is supplied'),
                                    IllegalIfOtherFieldDoesNotEqual('type', 'E',
                                        'Proposals can only be assigned to Basic Event types.')],
                                 limit_choices_to={'status':'A', 'published': True})

    ## Non-Proposal based event
    _require   = RequiredIfOtherFieldNotGiven('proposal',
                    'Required field if proposal is not assigned')
    _illegal   = IllegalIfOtherFieldGiven('proposal',
                    'Only supply if a proposal is not assigned')
    _title     = models.CharField('title', maxlength=150,  blank=True,
                                  validator_list=[_require, _illegal])
    _duration  = models.PositiveIntegerField('duration', blank=True, null=True,
                                             validator_list=[_require, _illegal])
    _summary   = models.TextField('summary', blank=True,
                                  validator_list=[_require, _illegal])
    url        = models.URLField('url override', maxlength=150, blank=True,
                                  validator_list=[_illegal])
    presenters = models.ManyToManyField(Presenter, null=True, blank=True,
                                        validator_list=[_illegal])
    created    = models.DateTimeField("created on", auto_now_add=True)
    updated    = models.DateTimeField("last updated on", auto_now=True)

    class Admin:
        fields = (
            ( None, { 'fields': ('type', 'url') }),
            ('Accepted Talk Event', {
                'classes': 'wide',
                'fields': ('proposal',)
            }),
            ('Other Event', {
                'classes': 'wide',
                'fields' : ('_title', '_duration', '_summary', 'presenters')
            }),
        )

        list_display = ('title', 'type',)
        list_filter = ('type',)

    @property
    def last_updated(self):
        if self.proposal_id is not None and self.proposal.updated > self.updated:
            return self.proposal.updated
        return self.updated

    @property
    def title(self):
        return unicode(self)

    @property
    def duration(self):
        if self.proposal_id:
            return self.proposal.duration
        return self._duration

    @property
    def summary(self):
        if self.proposal_id:
            return self.proposal.summary
        return self._summary

    @property
    def authors(self):
        if hasattr(self, '_authors'): return self._authors
        if self.proposal is not None:
            self._authors = [ self.proposal.submitter ]
            self._authors.extend( self.proposal.coauthors.all() )
        else:
            self._authors = list(self.presenters.all())
        return self._authors

    @property
    def presenters_listing(self):
        return u', '.join( unicode(auth) for auth in self.authors )

    @property
    def has_release_form(self):
        import os
        import glob
        root = os.path.abspath(os.path.normpath(settings.MEDIA_ROOT))
        dir = os.path.join(root, settings.EVENT_UPLOAD_TO, '%.3d'%self.id)
        if not glob.glob1(dir,'*.stx'):
            return True ## no asx means something is hosed...
        return bool(glob.glob1(dir, 'Release*'))

    @property
    def zope_link(self):
        return settings.ZOPE_TALKLINK_SEARCH + '%.3d'%self.id

    def get_absolute_url(self):
        if self.url: return self.url
        return reverse('schedule-event', None, (), {'eid': str(self.id)})

    def get_site_absolute_url(self):
        url = self.get_absolute_url()
        if url.startswith('http://'): return url
        site = Site.objects.get_current()
        return 'http://' + site.domain + url

    def get_tooltip_url(self):
        return reverse('schedule-tooltip', None, (), {'eid': str(self.id)})

    def public_attached_files(self, user):
        if user.is_anonymous() or (not user.is_superuser and
                                   user not in self.authors):
            return self.attachedfile_set.exclude(file__contains='Release')
        return self.attachedfile_set.all()

    def __unicode__(self):
        if self.proposal:
            return u"%s (#%d)" % (self.proposal.title, self.proposal.id)
        return self._title

class RelatedFileField(models.FileField):
    """
    """
    def __init__(self, **kwargs):
        # Flag to auto-fill the path if it is empty
        self.basedir = ''
        self.null_basedir = 'upload_to' not in kwargs or not kwargs['upload_to']
        if self.null_basedir: kwdargs['upload_to'] = 'None'
        else: self.basedir = kwargs['upload_to']
        self.related_field = kwargs['related_field']
        del kwargs['related_field']
        if 'valid_extensions' in kwargs:
            if kwargs['valid_extensions']:
                class ValidExtensionExtension(object):
                    def __init__(self, extensions):
                        self.extensions = [ ext.lower() for ext in extensions ]
                        self.always_test = True
                    def __call__(self, field_data, all_data):
                        name = ''
                        try: name = field_data['filename'].lower()
                        except: return
                        for ext in self.extensions:
                            if name.endswith(ext): return
                        raise ValidationError, (
                            "Only the following extensions are allowed: " +
                            unicode(self.extensions)[1:-1])
                validators = kwargs.setdefault('validator_list', list())
                validators.insert(0, ValidExtensionExtension(kwargs['valid_extensions']))
            del kwargs['valid_extensions']
        super(RelatedFileField, self).__init__(**kwargs)

    def _post_init(self, instance=None):
        if instance is None: return
        subdir = 'None'
        if self.related_field=='pk':
            subdir = "%.3d" % instance.id
        else:
            relation = getattr(instance, self.related_field)
            if relation is not None:
                subdir = "%.3d" % relation.id
        setattr(self, 'upload_to', os.path.join(self.basedir, subdir, ''))

    def contribute_to_class(self, cls, name):
        super(RelatedFileField, self).contribute_to_class(cls, name)
        dispatcher.connect(self._post_init, models.signals.post_init, sender=cls)

    def get_internal_type(self):
        return 'FileField'


class AttachedFile(models.Model):
    """Files attached to Events
    """
    EXTENSIONS = ['.doc', '.ppt', '.odt', '.odp',     ## office suites
                  '.pdf', '.htm', '.html', '.txt',    ## web docs
                  '.gif', '.jpg', '.jpeg', '.png',    ## images
                  '.rst', '.restructuredtext', '.s5', ## distutils
                  '.zip', '.tgz', '.tar']             ## archives

    event     = models.ForeignKey(Event, edit_inline=models.TABULAR)
    file      = RelatedFileField(upload_to=settings.EVENT_UPLOAD_TO,
                                 related_field='event',
                                 valid_extensions=EXTENSIONS)
    comment   = models.CharField(maxlength=100, core=True)
    uploaded  = models.DateTimeField("uploaded on", auto_now_add=True)

    class Admin:
        list_display = ('get_file_short_name', 'event', 'comment')
        search_fields = ('comment',)
    class Meta:
        ordering = ('uploaded',)
        permissions = (("can_upload_material",   "Can upload event materials"),)

    @short_description("name")
    def get_file_short_name(self):
        return self.file.replace("\\", "/").split("/")[-1]

    def __unicode__(self):
        return unicode(self.file)

def validate_session_chair(field_data, all_data):
    if not field_data: return
    pred = ''
    if 'scheduledevent.0.id' in all_data: pred='scheduledevent.0.'

    user = User.objects.get(pk=field_data)
    start = forms.DatetimeField.html2python(all_data[pred+'start_date'] + ' ' +
                                            all_data[pred+'start_time'])
    if all_data[pred+'stop_date']:
        stop = forms.DatetimeField.html2python(all_data[pred+'stop_date'] + ' ' +
                                               all_data[pred+'stop_time'])
    else:
        if pred:
            if all_data['proposal']:
                dur = Proposal.objects.get(pk=all_data['proposal']).duration
            else:
                dur = int(all_data['_duration'])
        else:
            dur = Event.objects.get(pk=all_data['event']).duration
        stop = start + timedelta(minutes=int(dur))
    query = user.events_session_chairing_set.filter(start__lt=stop, stop__gt=start)
    if pred+'id' in all_data and all_data[pred+'id']:
        query = query.exclude(pk=all_data[pred+'id'])
    if query.count():
        raise ValidationError, "User is a session chair for an overlaping slot"

def validate_room(field_data, all_data):
    if 'scheduledevent.0.id' in all_data:
        typ = all_data['type']
    else:
        typ = Event.objects.get(pk=all_data['event']).type
    if not field_data and typ not in ['P', 'B']:
        raise ValidationError, ("A room is required unless the Event is a "
                                "Break/Service or Plenary.")

def validate_starttime(field_data, all_data):
    if not field_data: return
    pred = ''
    if 'scheduledevent.0.id' in all_data: pred='scheduledevent.0.'
    if not all_data[pred+'start_time']: return

    start = forms.DatetimeField.html2python(all_data[pred+'start_date'] + ' ' +
                                            all_data[pred+'start_time'])
    if not all_data[pred+'stop_date'] or not all_data[pred+'stop_time']:
        if pred:
            if all_data['proposal']:
                dur = Proposal.objects.get(pk=all_data['proposal']).duration
            else:
                dur = int(all_data['_duration'])
            stop = start + timedelta(minutes=dur)
        else:
            event = Event.objects.get(pk=all_data['event'])
            stop = start + timedelta(minutes=int(event.duration))
    else:
        stop = forms.DatetimeField.html2python(all_data[pred+'stop_date'] + ' ' +
                                               all_data[pred+'stop_time'])

    query = ScheduledEvent.objects.filter(start__lt=stop, stop__gt=start)
    if all_data.get(pred+'id', False):
        this = ScheduledEvent.objects.get(pk=all_data[pred+'id'])
        if this.event.type == 'T':
            scheds = list(this.event.scheduledevent_set.all())
            query = query.exclude(pk__in=[ sch.id for sch in scheds])
        else:
            query = query.exclude(pk=all_data[pred+'id'])

    if not all_data[pred+'room']:
        ## plenary no overlap irreguardless of room
        if query.count():
            raise ValidationError, ("Overlaps with: " + unicode(query[0]))
    else:
        room = Room.objects.get(pk=all_data[pred+'room'])
        roomids = [ r['id'] for r in room.parents.values('id') ]
        roomids.append(room.id)
        inroom = query.filter(room__in=roomids)
        if inroom.count():
            raise ValidationError, ("Overlaps with: " + unicode(inroom[0]))
        plen = query.filter(event__type__in = ['B', 'P'], room__isnull=True)
        if plen.count():
            raise ValidationError, ("Overlaps with plenary/break: " +
                                    unicode(plen[0]))

def validate_stoptime(field_data, all_data):
    if not field_data: return
    pred = ''
    if 'scheduledevent.0.id' in all_data: pred='scheduledevent.0.'
    if not all_data[pred+'stop_date'] or not all_data[pred+'stop_time']: return
    srt = forms.DatetimeField.html2python(all_data[pred+'start_date'] + ' ' +
                                          all_data[pred+'start_time'])
    end = forms.DatetimeField.html2python(all_data[pred+'stop_date'] + ' ' +
                                          all_data[pred+'stop_time'])
    if srt >= end:
        raise ValidationError, "End time must be later than start time"
    if not pred:
        evt = Event.objects.get(pk=all_data['event'])
        dur = evt.duration
        typ = evt.type
    elif all_data['proposal']:
        prop = Proposal.objects.get(pk=all_data['proposal'])
        dur = prop.duration
        typ = all_data['type']
    else:
        dur = int(all_data['_duration'])
        typ = all_data['type']
    if typ in [ 'B', 'T' ]: return
    if end-srt != timedelta(minutes=dur):
        raise ValidationError, ("Stop time must be start+duration unless the "
                                "event type is Break/Service or Tutorial")

def validate_event(field_data, all_data):
    if 'scheduledevent.0.id' in all_data: return ## we are in an inline edit
    base = ScheduledEvent.objects.filter(event=field_data)
    if 'id' in all_data and all_data['id'] is not None:
        base = base.exclude(pk=all_data['id'])
    typ = Event.objects.get(pk=field_data).type
    if typ != 'T':
        if not base.count(): return
        raise ValidationError, "Event is already scheduled for another time"
    ## tutorials can have 2 schedules, but must be in teh same room
    rid = int(all_data['room'])
    if len(base) > 1:
        raise ValidationError, "Tutorials can have at most 2 scheduled times"
    if len(base) != 0 and base[0].room_id != rid:
        raise ValidationError, "Split Tutorial schedules must be in the same room"

class ScheduledEvent(models.Model):
    ## replace default add/change manipulators for smart setting of stop
    event = models.ForeignKey(Event, edit_inline=models.STACKED,
                              validator_list=[validate_event,],
                              num_extra_on_change=0, num_in_admin=2)
    room  = models.ForeignKey(Room, blank=True, null=True,
                              validator_list=[validate_room,],)
    start = models.DateTimeField('start time', core=True,
                                 validator_list=[validate_starttime,],)
    stop  = models.DateTimeField('stop time', blank=True,
                                 validator_list=[validate_stoptime,],
                                 help_text='This field will be auto filled if override is not supplied')
    updated       = models.DateTimeField("last updated on", auto_now=True)
    session_chair = models.ForeignKey(User, null=True, blank=True,
                                validator_list=[validate_session_chair,],
                                related_name='events_session_chairing_set',
                                limit_choices_to={'groups__name': 'Session Chairs'})

    class Admin:
        list_display   = ('event', 'room', 'start', 'stop', 'session_chair')
        list_filter    = ('room', 'session_chair',)
        date_hierarchy = 'start'

    class Meta:
        permissions = (("can_assign_self_as_chair",   "Can assign self as session chair"),
                       ("can_unassign_self_as_chair", "Can unassign self as session chair"),
                       ("view_session_chairs",        "Can view session chairs"),)

    def __unicode__(self):
        return unicode(self.event)

    @property
    def last_updated(self):
        if self.updated < self.event.last_updated:
            return self.event.last_updated
        return self.updated

    def save(self):
        if not self.stop:
            self.stop = self.start + timedelta(minutes=self.event.duration)
        return super(ScheduledEvent, self).save()

    @staticmethod
    def get_session_slots():
        slotsdump = cache.get('schedule.models.ScheduledEvent.get_session_slots', None)
        if slotsdump is not None:
            starting, stopping = cPickle.loads(slotsdump)
        else:
            starting = []
            stopping = []
            breaks = ScheduledEvent.objects.filter(event__type='B'
                                    ).order_by('start').values('start', 'stop')
            last = None
            for ent in breaks:
                start, stop = ent['start'], ent['stop']
                if last is not None and last.day == start.day:
                    starting.append(last)
                    stopping.append(start)
                starting.append(start)
                stopping.append(stop)
                last = stop
            cache.set('schedule.views.get_session_slots',
                      cPickle.dumps((starting,stopping),
                                    cPickle.HIGHEST_PROTOCOL), 60*60*12)
        return starting, stopping

    def get_session(self):
        starting, stopping = self.get_session_slots()
        start, stop = self.start, self.stop
        base = bisect(starting, self.start)
        if not base: stop = starting[0]
        elif base == len(starting) and self.start > stopping[base-1]:
            start = stopping[base-1]
        else:
            start, stop = starting[base-1], stopping[base-1]
            if start.date() < self.start.date():
                start = stop
                stop = self.stop
        return start, stop

    @property
    def same_session_set(self):
        start, stop = self.get_session()
        query = ScheduledEvent.objects.filter(start__gte=start, stop__lte=stop)
        if self.room is not None:
            rooms = [ rm.id for rm in self.room.parents.all()]
            rooms.append(self.room.id)
            return query.filter(room__in=rooms)
        return query.filter(room__isnull=True)

class SelectedEvent(models.Model):
    """For keeping stats on talks people are interested in.
    """
    event = models.ForeignKey(Event)
    ## because session id's cycle and time out, we dont use a FK here.
    ## there is also no reason to link the information in the admin
    sessionid = models.CharField(maxlength=40, blank=True)
    status = models.PositiveIntegerField(choices=SELECTED_EVENT_STATUS_CHOICES,
                                         default=1)
    user = models.ForeignKey(User, blank=True, null=True)

    class Meta:
        permissions = (("can_view_selected_events",   "Can view selected event information"),)

"""
class ZopeLink(models.Model):
    event = models.ForeignKey(Event, edit_inline=models.STACKED)
    url = models.URLField()
    ## Release form
    release_form = models.FileField(upload_to=settings.EVENT_RELEASE_UPLOAD_TO)
"""
