import itertools
import math
import datetime
import collections
from django.conf import settings
from django.utils.datastructures import SortedDict
from django.utils.timezone import now

from ..conference import models as conference_models

from . import models

from django.core.cache import cache


def proposal_is_scheduled(proposal):
    """
    Checks if a given proposal already has a session associated with it.

    Warning: This internally caches the result for 10 seconds.
    """
    proposal_pks = cache.get('proposal_pks_with_session')
    if proposal_pks is None:
        proposal_pks = set([o['proposal__pk'] for o in models.Session.objects.values('proposal__pk')])
        cache.set('proposal_pks_with_session', proposal_pks, 10)
    return proposal.pk in proposal_pks


def create_schedule(row_duration=30, uncached=None, merge_sections=False):
    """
    Creates a schedule for each section of the conference.

    @param row_duration duration represented by a row in minutes
    """
    if uncached is None:
        uncached = not getattr(settings, 'SCHEDULE_CACHE_SCHEDULE', True)
    conf = conference_models.current_conference()
    cache_key = 'schedule:{0}:{1}'.format(conf.pk, row_duration)
    if not uncached:
        result = cache.get(cache_key)
        if result:
            return result
    result = SortedDict()
    if merge_sections:
        section_schedule = create_section_schedule(None,
            row_duration=row_duration, uncached=uncached)
        result['merged'] = section_schedule
    else:
        for section in conference_models.Section.objects.order_by('order', 'start_date').all():
            section_schedule = create_section_schedule(section,
                row_duration=row_duration, uncached=uncached)
            result[section] = section_schedule
    if not uncached:
        cache.set(cache_key, result, settings.SCHEDULE_CACHE_TIMEOUT)
    return result


def create_section_schedule(section, row_duration=30, uncached=False):
    """
    Creates a schedule for a given section.

    @param section section for which the schedule should be generated.
    @param row_duration number of minutes a single row should represent
    """
    if section is None:
        schedule_cache_key = 'section_schedule:__merged__:{0}'.format(row_duration)
    else:
        schedule_cache_key = 'section_schedule:{0}:{1}'.format(section.pk, row_duration)

    if not uncached:
        section_schedule = cache.get(schedule_cache_key)
        if section_schedule:
            return section_schedule

    if section is None:
        sessions = models.Session.objects
        side_events = models.SideEvent.objects
    else:
        sessions = section.sessions
        side_events = section.side_events

    sessions = sessions.select_related('audience_level',
                                       'track',
                                       'kind',
                                       'speaker__user__profile',
                                       'conference') \
                       .prefetch_related('additional_speakers__user__profile',
                                         'location') \
                       .filter(released=True, start__isnull=False, end__isnull=False) \
                       .order_by('start') \
                       .all()
    side_events = side_events.prefetch_related('location') \
                             .filter(start__isnull=False, end__isnull=False) \
                             .order_by('start') \
                             .all()

    evt_cache = {}

    locations = set()
    start_time = None
    end_time = None
    for session in sessions:
        start_time = session.start if start_time is None else min(start_time, session.start)
        end_time = session.end if end_time is None else max(end_time, session.end)
        if session.is_global:
            continue
        locations |= set(session.location.all())
    for evt in side_events:
        start_time = evt.start if start_time is None else min(start_time, evt.start)
        end_time = evt.end if end_time is None else max(end_time, evt.end)
        # Global events span all session locations and therefor the location
        # should not be included in the columns list
        if evt.is_global:
            continue
        locations |= set(evt.location.all())
    locations = sorted(locations, cmp=lambda a, b: a.order - b.order)

    # This allows overriding sessions (e.g. posters) with side events in the
    # schedule table but still have them listed in the general lists.
    events = sorted(itertools.chain(sessions, side_events),
        cmp=_evt_start_cmp)

    if not events:
        return {}

    # As a first step we build a grid with the respective row start time as
    # key and fill it with events starting at that time.
    grid = _create_base_grid(start_time, end_time, row_duration)
    for evt in events:
        cell = GridCell(evt, int((evt.end - evt.start).total_seconds() / (row_duration * 60)))
        evt_cache[repr(cell)] = cell
        grid[evt.start].append(cell)

    # Convert this grid into a list of grid rows sorted by the row's start time.
    new_grid = []
    for k, v in grid.iteritems():
        new_grid.append(GridRow(k, k + datetime.timedelta(0, row_duration * 60), v))
    new_grid.sort(cmp=lambda a, b: cmp(a.start, b.start))

    # Split the whole section grid into days
    current_day = None
    days = []
    prev_row = None
    for row in new_grid:
        _pad_row_for_locations(row, prev_row, locations)

        if current_day is None:
            current_day = SectionDay(row.start.date(), [])
        elif current_day.day < row.start.date():
            days.append(current_day)
            current_day = SectionDay(row.start.date(), [])
        current_day.rows.append(row)
        # Propagate pause rows if necessary
        if prev_row is not None:
            if prev_row.is_pause_row() and not row.events:
                if prev_row.events:
                    prev_row.pause_until = prev_row.events[0].end
                if prev_row.pause_until is not None and prev_row.pause_until >= row.end:
                    row.pause = True
                    row.pause_until = prev_row.pause_until

        prev_row = row
    if current_day and current_day.rows:
        days.append(current_day)