import caldav
import vobject
from datetime import datetime, date, time, timedelta
import yaml
import os

import locale
import threading
from contextlib import contextmanager


"""CalDAV server url"""
server_url = "https://***REMOVED***
"""CalDAV calendar url"""
cal_url = "https://***REMOVED***
"""CalDAV username"""
cal_user = "tobias"
"""Category to filter events for. Only events that belong to this category will be processed."""
cal_category = "Veranstaltung"
"""Name of the file the YAML data will be writte to.""" 
result_file = "../content/pages/termine.md"
"""List of icalendar properties that will be passed on to Pelican. At this time, all properties are used except those regarding alarms.""" 
cal_properties = [ # taken from ical specification
  # descriptive  
  "attach", "categories", "class", "comment", "description", "geo", "location", "percent-complete", "priority", "resources", "status", "summary",
  # date and time
  "completed", "dtend", "due", "dtstart", "duration", "freebusy", "transp", 
  # timezone components
  "tzid", "tzname", "tzoffsetfrom", "tzoffsetto", "tzurl",
  # relationship
  "attendee", "contact", "organizer", "recurrence-id", "related-to", "url", "uid",
  # recurrence
  "exdate", "exrule", "rdate", "rrule",
  # alarm
#  "action", "repeat", "trigger",
  # change management
  "created", "dtstamp", "last-modified", "sequence"
]
"""List of icalendar properties that the vobject passes as either datetime.datetime or datetime.date objects. To be able to compare any two objects, the script makes each one aware and/or converts into datetime.datetime if necessary. Default timezone is the system timezone; default time is 23:59:59."""  
cal_prop_datetimes = ["dtend", "due", "dtstart", "duration", "dtstamp", "last-modified"]
"""Current time as an aware datetime.datetime object.""" 
now = datetime.now().astimezone()


"""Returns an aware datetime.datetime object based on the given datetime or date. Conversion happens only if necessary, i.e. if d is of type datetime.date or a naive datetime.datetime object."""
def fixDatetime(d, t=None, tz=None):
  if not tz:
    tz = now.tzinfo
  
  # datetime object is made aware if necessary
  if type(d) == datetime:
    if d.tzinfo:
      return d
    else: 
      return datetime(d.date(), d.time(), tz)
  
  # raise error if d is neither datetime nor date
  elif type(d) != date:
    raise TypeError("parameter must be a datetime.date or datetime.datetime object")

  # d is a date object
  if not t:
    t = time(23, 59, 59, tzinfo=tz)    # if no time parameter was passed, use 23:59:59 
  if not t.tzinfo:
    t.replace(tzinfo=tz)
  return datetime.combine(d, t)


"""Takes a list of events in vobject representation and converts each event into a dict of Python datatypes.""" 
def extractEventData(events):
  e_tmp = []

  # iterate over events
  for e in events:
    d = {}

    # iterate over event properties
    for k, v in e.contents.items():
      # we're only interested in a subset of properties
      if not k in cal_properties:
        continue

      # Usually, v is a list with a single item. We only need the item's value attribute 
      if len(v) == 1:
        d[k] = v[0].value
      # but sometimes the list has more than one item; this only ever happens for the property "categories"
      # and in this case, each list item's value is itself a one item list (which is stupid but that's how vobject handles categories)
      else:
        d[k] = [v[i].value[0] for i in range(len(v))]
    e_tmp.append(d)
  return e_tmp

# get password
with open(".caldav_pass", "r") as f:
  cal_pass = f.read().strip()

# create vobject calendar
vcal = vobject.newFromBehavior("vcalendar")

# establish connection to caldav server 
with caldav.DAVClient(url=server_url, username=cal_user, password=cal_pass) as dav_client:
  # establish connection to calendar
  dav_cal = dav_client.calendar(url=cal_url)

  # put all events from caldav calendar into vobject calendar
  for e in dav_cal.events():
    for ev in e.instance.contents["vevent"]:
      vcal.add(ev)
  
# we only want events belonging to a specific category 
events = [e for e in vcal.getChildren() if "categories" in e.contents.keys() and cal_category in map(lambda x: x.value[0], e.contents["categories"])]

# extract event data 
events = extractEventData(events)

# fix dates and datetimes
for e in events:
  for k in e.keys():
    if k in cal_prop_datetimes:
      # fix a CalDAV/vobject/icalendar (?) bug where a dtend value which is a date only (no time) automatically moves one day into the future 
      if k == "dtend" and type(e[k]) == date:
        e[k] = e[k] - timedelta(days=1)
      # fix all datetimes so they can be compared and sorted 
      e[k] = fixDatetime(e[k])

# keep only future events  
events = [e for e in events if e["dtstart"] >= now]

# sort by start date
events.sort(key=lambda e: e["dtstart"])

# read default metadata if present
default_metadata = None
if os.path.isfile(result_file + ".metadata"):
  with open(result_file + ".metadata", "r") as f:
    default_metadata = yaml.safe_load(f)

# set up a context manager in order to threadsafely format dates and times with the German locale 
# source: https://stackoverflow.com/a/24070673
LOCALE_LOCK = threading.Lock()

@contextmanager
def setlocale(name):
  with LOCALE_LOCK:
    saved = locale.setlocale(locale.LC_TIME)
    try:
      yield locale.setlocale(locale.LC_TIME, name)
    finally:
      locale.setlocale(locale.LC_TIME, saved)

# temporary set locale to de_DE (category "time" only) and format start date as well as start time or end date
with setlocale('de_DE.UTF-8'):
  for e in events:
    # start date
    e["startdate"] = e["dtstart"].strftime("%a, %d.%m.")
    # start time (if not the default time)
    if e["dtstart"].timetz() != time(23, 59, 59, tzinfo=now.tzinfo):
      e["starttime"] = e["dtstart"].strftime("%H:%M Uhr")
    # end date (if different from start date)
    if e["dtstart"].date() < e["dtend"].date():
      e["enddate"] = e["dtend"].date().strftime("%a, %d.%m.")

  # add current date to default metadata while we're at it
  if default_metadata:
    default_metadata["date"] = now.strftime("%Y-%m-%d %H:%M")

# write data as YAML
with open(result_file, 'w') as f:
  f.write("---\n")
  if default_metadata:
    yaml.dump(default_metadata, f)
  yaml.dump({"termine": events}, f)
  f.write("---\n")
  f.write("written at " + datetime.today().isoformat(" "))

