from datetime import datetime, timedelta, date, time
import calendar
from django.db.models import Q, F
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from django.template.defaultfilters import date as _date
from django.http import Http404

from django.db.models import CharField
from django.db.models.functions import Lower

CharField.register_lookup(Lower)

import logging

logger = logging.getLogger(__name__)


def daterange(start, end, step=timedelta(1)):
    if end is None:
        yield start
    else:
        curr = start
        while curr <= end:
            yield curr
            curr += step


class DayInCalendar:
    midnight = time(0, 0, 0)
    cats = None

    def __init__(self, d, on_requested_interval=True):
        self.date = d
        now = date.today()
        self.week = d.isocalendar()[1]

        self.in_past = d < now
        self.today = d == now
        self.tomorrow = d == now + timedelta(days=+1)
        self.events = []
        self.on_requested_interval = on_requested_interval

        self.events_by_category = {}
        self.time_intervals = None
        self.id = d.strftime('%Y-%m-%d')

    def is_in_past(self):
        return self.in_past

    def is_today(self):
        return self.today

    def is_tomorrow(self):
        return self.tomorrow

    def is_ancestor_uuid_event_from_other(self, event):
        for e in self.events:
            if event.is_ancestor_by_uuid(e):
                return True
        return False

    def remove_event_with_ancestor_uuid_if_exists(self, event):
        removed = False
        for i, e in enumerate(self.events):
            if e.is_ancestor_by_uuid(event):
                # remove e from events_by_category
                for k, v in self.events_by_category.items():
                    if e in v:
                        self.events_by_category[k].remove(e)
                # remove e from the events
                del self.events[i]
                removed = True

        if removed:
            # remove empty events_by_category
            self.events_by_category = dict(
                [(k, v) for k, v in self.events_by_category.items() if len(v) > 0]
            )

    def add_event(self, event):
        if event.contains_date(self.date):
            if self.is_ancestor_uuid_event_from_other(event):
                # we do not add a generic event if a specific is already present
                pass
            else:
                self.remove_event_with_ancestor_uuid_if_exists(event)
                self._add_event_internal(event)

    def _add_event_internal(self, event):
        from .models import Category
        from copy import copy
        # copy event
        local_event = copy(event)

        # set values
        if local_event.start_day != self.date:
            local_event.start_day = self.date
            local_event.start_time = None
        if local_event.end_day != self.date:
            local_event.end_day = None
            local_event.end_time = None

        # add event to the day
        self.events.append(local_event)

        # add in its category
        if local_event.category is None:
            cat = Category.default_name
        else:
            cat = local_event.category.name
        if cat not in self.events_by_category:
            self.events_by_category[cat] = []
        self.events_by_category[cat].append(local_event)

    def filter_events(self):
        self.events.sort(
            key=lambda e: DayInCalendar.midnight
            if e.start_time is None
            else e.start_time
        )
        self.today_night = False
        if self.is_today():
            self.today_night = True
            now = timezone.now()
            nday = now.date()
            ntime = now.time()
            found = False
            for idx,e in enumerate(self.events):
                if (nday < e.start_day) or (nday == e.start_day and e.start_time and ntime <= e.start_time):
                    self.events[idx].is_first_after_now = True
                    found = True
                    break
            if found:
                self.today_night = False

    def is_today_after_events(self):
        return self.is_today() and self.today_night

    def events_by_category_ordered(self):
        from .models import Category
        if DayInCalendar.cats is None:
            DayInCalendar.cats = Category.objects.order_by('position')
        result = []
        for c in DayInCalendar.cats:
            if c.name in self.events_by_category:
                result.append((c.name, self.events_by_category[c.name]))
        return result

    def build_time_intervals(self, all_day_name, all_day_short_name, interval_names, interval_short_names, interval_markers):
        self.time_intervals = [IntervalInDay(self.date, i, n[0], n[1]) for i, n in 
            enumerate(zip([all_day_name] + interval_names, [all_day_short_name] + interval_short_names))]

        for e in self.events:
            if e.start_time is None:
                self.time_intervals[0].add_event(e)
            else:
                dt = datetime.combine(e.start_day, e.start_time)
                ok = False
                for i in range(len(interval_markers)):
                    if dt < interval_markers[i]:
                        self.time_intervals[i + 1].add_event(e)
                        ok = True
                        break
                if not ok:
                    self.time_intervals[-1].add_event(e)

    def get_time_intervals(self):
        if self.time_intervals is None:
            if self.is_today():
                all_day_name = _('All day today')
                interval_names = [_('This morning'), _('This noon'), _('This afternoon'), _('This evening')]                
            elif self.is_tomorrow():
                name = _("Tomorrow")
                all_day_name = _('All day tomorrow')
                interval_names = [_('%s morning') % name, _('%s noon') % name, _('%s afternoon') % name, _('%s evening') % name]                
            else:
                name = _date(self.date, "l")
                all_day_name = _('All day %s') % name
                interval_names = [_('%s morning') % name, _('%s noon') % name, _('%s afternoon') % name, _('%s evening') % name]                
            all_day_short_name = _('All day')
            interval_short_names = [_('Morning'), _('Noon'), _('Afternoon'), _('Evening')]                
            interval_markers = [datetime.combine(self.date, time(h, m)) for h, m in [(11, 30), (13, 0), (18, 0)]]
            self.build_time_intervals(all_day_name, all_day_short_name, interval_names, interval_short_names, interval_markers)

        return self.time_intervals


class IntervalInDay(DayInCalendar):

    def __init__(self, d, id, name, short_name):
        self.name = name
        self.short_name = short_name
        super().__init__(d)
        self.id = self.id + '-' + str(id)

class CalendarList:
    def __init__(self, firstdate, lastdate, filter=None, exact=False, ignore_dup=None, qs=None):
        self.firstdate = firstdate
        self.lastdate = lastdate
        self.now = date.today()
        self.filter = filter
        self.ignore_dup = ignore_dup
        self.qs = qs

        if exact:
            self.c_firstdate = self.firstdate
            self.c_lastdate = self.lastdate
        else:
            # start the first day of the first week
            self.c_firstdate = firstdate + timedelta(days=-firstdate.weekday())
            # end the last day of the last week
            self.c_lastdate = lastdate + timedelta(days=6 - lastdate.weekday())

        self.calendar_days = None

    def is_today(self):
        return self.firstdate == date.today()

    def build_internal(self):
        # create a list of DayInCalendars
        self.create_calendar_days()

        # fill DayInCalendars with events
        self.fill_calendar_days()

        # finally, sort each DayInCalendar
        for i, c in self.calendar_days.items():
            c.filter_events()

    def get_calendar_days(self):
        if self.calendar_days is None:
            self.build_internal()
        
        return self.calendar_days

    def today_in_calendar(self):
        return self.firstdate <= self.now and self.lastdate >= self.now

    def all_in_past(self):
        return self.lastdate < self.now

    def fill_calendar_days(self):
        if self.filter is None:
            if self.qs is None:
                from .models import Event

                qs = Event.objects.all()
            else:
                qs = self.qs
        else:
            qs = self.filter.qs

        if self.ignore_dup:
            qs = qs.exclude(other_versions=self.ignore_dup)
        startdatetime = timezone.make_aware(datetime.combine(self.c_firstdate, time.min), timezone.get_default_timezone())
        lastdatetime = timezone.make_aware(datetime.combine(self.c_lastdate, time.max), timezone.get_default_timezone())
        qs = qs.filter(
            (Q(recurrences__isnull=False) & 
                (Q(recurrence_dtend__isnull=True) & Q(recurrence_dtstart__isnull=False) & Q(recurrence_dtstart__lte=lastdatetime))
                | (
                    Q(recurrence_dtend__isnull=False)
                    & ~(
                        Q(recurrence_dtstart__gt=lastdatetime)
                        | Q(recurrence_dtend__lt=startdatetime)
                    )
                )
            )
            | (Q(start_day__lte=self.c_lastdate) & # start before the end of the desired period
                ((Q(end_day__isnull=True) & Q(start_day__gte=self.c_firstdate)) # end after the begining of desired period
                 | (Q(end_day__isnull=False) & Q(end_day__gte=self.c_firstdate))))
        ).filter(
            Q(other_versions__isnull=True) |
            Q(other_versions__representative=F('pk')) |
            Q(other_versions__representative__isnull=True)
        ).order_by("start_time", "title__unaccent__lower")
        
        qs = qs.select_related("category").select_related("other_versions").select_related("other_versions__representative")
        self.events = qs

        firstdate = datetime.fromordinal(self.c_firstdate.toordinal())
        if firstdate.tzinfo is None or firstdate.tzinfo.utcoffset(firstdate) is None:
            firstdate = timezone.make_aware(firstdate, timezone.get_default_timezone())

        lastdate = datetime.fromordinal(self.c_lastdate.toordinal())
        if lastdate.tzinfo is None or lastdate.tzinfo.utcoffset(lastdate) is None:
            lastdate = timezone.make_aware(lastdate, timezone.get_default_timezone())

        for e in self.events:
            if e.recurrences is None:
                if e.end_day is None:
                    if e.start_day >= self.firstdate and e.start_day <= self.lastdate:
                        self.calendar_days[e.start_day.__str__()].add_event(e)
                else:
                    for d in daterange(max(e.start_day, self.firstdate), min(e.end_day, self.lastdate)):
                        self.calendar_days[d.__str__()].add_event(e)
            else:
                for e_rec in e.get_recurrences_between(firstdate, lastdate):
                    end = e_rec.start_day if e_rec.end_day is None else e_rec.end_day
                    for d in daterange(max(e_rec.start_day, self.firstdate), min(end, self.lastdate)):
                        self.calendar_days[d.__str__()].add_event(e_rec)

    def create_calendar_days(self):
        # create daylist
        self.calendar_days = {}
        for d in daterange(self.c_firstdate, self.c_lastdate):
            self.calendar_days[d.strftime("%Y-%m-%d")] = DayInCalendar(
                d, d >= self.firstdate and d <= self.lastdate
            )

    def is_single_week(self):
        return hasattr(self, "week")

    def is_full_month(self):
        return hasattr(self, "month")

    def calendar_days_list(self):
        return list(self.get_calendar_days().values())

    def nb_days(self):
        return (self.c_lastdate - self.c_firstdate).days + 1

    def time_intervals_list(self, onlyfirst=False):
        ds = self.calendar_days_list()
        result = []
        for d in ds:
            tis = d.get_time_intervals()
            for t in tis:
                if len(t.events) > 0:
                    result.append(t)
                    if onlyfirst:
                        break
        return result

    def time_intervals_list_first(self):
        return self.time_intervals_list(True)

    def export_to_ics(self, request):
        from .models import Event
        events = [event for day in self.get_calendar_days().values() for event in day.events]
        return Event.export_to_ics(events, request)


class CalendarMonth(CalendarList):
    def __init__(self, year, month, filter, qs=None):
        self.year = year
        self.month = month
        r = calendar.monthrange(year, month)

        first = date(year, month, 1)
        last = date(year, month, r[1])

        super().__init__(first, last, filter, qs)

    def get_month_name(self):
        return self.firstdate.strftime("%B")

    def next_month(self):
        return self.lastdate + timedelta(days=7)

    def previous_month(self):
        return self.firstdate + timedelta(days=-7)


class CalendarWeek(CalendarList):
    def __init__(self, year, week, filter, qs=None):
        self.year = year
        self.week = week

        try:
            first = date.fromisocalendar(self.year, self.week, 1)
            last = date.fromisocalendar(self.year, self.week, 7)
        except:
            raise Http404()

        super().__init__(first, last, filter, qs)

    def next_week(self):
        return self.firstdate + timedelta(days=7)

    def previous_week(self):
        return self.firstdate + timedelta(days=-7)


class CalendarDay(CalendarList):
    def __init__(self, date, filter=None, qs=None):
        super().__init__(date, date, filter=filter, qs=qs, exact=True)

    def get_events(self):
        return self.calendar_days_list()[0].events
