#!python3

"""
Inkycal iCalendar parsing module
Copyright by aceisace
"""
import urllib
import arrow
from urllib.request import urlopen
import logging
import time

import recurring_ical_events
from icalendar import Calendar

"""               ---info about iCalendars---
• all day events start at midnight, ending at midnight of the next day
• iCalendar saves all event timings in UTC -> need to be converted into local
  time
• Only non-all_day events or multi-day need to be converted to
  local timezone. Converting all-day events to local timezone is a problem!
"""

logger = logging.getLogger(__name__)


class iCalendar:
    """iCalendar parsing moudule for inkycal.
    Parses events from given iCalendar URLs / paths"""

    def __init__(self):
        self.icalendars = []
        self.parsed_events = []

    def load_url(self, url, username=None, password=None):
        """Input a string or list of strings containing valid iCalendar URLs
        example: 'URL1' (single url) OR ['URL1', 'URL2'] (multiple URLs)
        add username and password to access protected files
        """

        def auth_ical(url, uname, passwd):
            """Authorisation helper for protected ical files"""

            # Credit to Joshka
            password_mgr = urllib.request.HTTPPasswordMgrWithDefaultRealm()
            password_mgr.add_password(None, url, username, password)
            handler = urllib.request.HTTPBasicAuthHandler(password_mgr)
            opener = urllib.request.build_opener(handler)
            ical = Calendar.from_ical(str(opener.open(url).read().decode()))
            return ical

        if type(url) == list:
            if (username is None) and (password is None):
                ical = [Calendar.from_ical(str(urlopen(_).read().decode()))
                        for _ in url]
            else:
                ical = [auth_ical(each_url, username, password) for each_url in url]
        elif type(url) == str:
            if (username is None) and (password is None):
                ical = [Calendar.from_ical(str(urlopen(url).read().decode()))]
            else:
                ical = [auth_ical(url, username, password)]
        else:
            raise Exception(f"Input: '{url}' is not a string or list!")

        # Add the parsed icalendar/s to the self.icalendars list
        if ical: self.icalendars += ical
        logger.info('loaded iCalendars from URLs')

    def load_from_file(self, filepath):
        """Input a string or list of strings containing valid iCalendar filepaths
        example: 'path1' (single file) OR ['path1', 'path2'] (multiple files)
        returns a list of iCalendars as string (raw)
        """
        if isinstance(filepath, list):
            for path in filepath:
                with open(path, mode='r') as ical_file:
                    ical = (Calendar.from_ical(ical_file.read()))
                    self.icalendars.append(ical)

        elif isinstance(filepath, str):
            with open(filepath, mode='r') as ical_file:
                ical = (Calendar.from_ical(ical_file.read()))
                self.icalendars.append(ical)
        else:
            raise Exception(f"Input: '{filepath}' is not a string or list!")

        logger.info('loaded iCalendars from filepaths')

    def get_events(self, timeline_start, timeline_end, timezone=None):
        """Input an arrow (time) object for:
        * the beginning of timeline (events have to end after this time)
        * the end of the timeline (events have to begin before this time)
        * timezone if events should be formatted to local time
        Returns a list of events sorted by date
        """
        if type(timeline_start) == arrow.arrow.Arrow:
            if timezone is None:
                timezone = 'UTC'
            t_start = timeline_start
            t_end = timeline_end
        else:
            raise Exception('Please input a valid arrow (time) object!')

        # parse non-recurring events

        # Recurring events time-span has to be in this format:
        # "%Y%m%dT%H%M%SZ" (python strftime)
        fmt = lambda date: (date.year, date.month, date.day, date.hour,
                            date.minute, date.second)

        t_start_recurring = fmt(t_start)
        t_end_recurring = fmt(t_end)

        # Fetch recurring events
        recurring_events = (recurring_ical_events.of(ical).between(
            t_start_recurring, t_end_recurring)
            for ical in self.icalendars)

        events = (
            {
                'title': events.get('SUMMARY').lstrip(),

                'begin': arrow.get(events.get('DTSTART').dt).to(timezone) if (
                        arrow.get(events.get('dtstart').dt).format('HH:mm') != '00:00')
                else arrow.get(events.get('DTSTART').dt).replace(tzinfo=timezone),

                'end': arrow.get(events.get("DTEND").dt).to(timezone) if (
                        arrow.get(events.get('dtstart').dt).format('HH:mm') != '00:00')
                else arrow.get(events.get('DTEND').dt).replace(tzinfo=timezone)

            } for ical in recurring_events for events in ical)

        # if any recurring events were found, add them to parsed_events
        if events: self.parsed_events += list(events)

        # Sort events by their beginning date
        self.sort()

        return self.parsed_events

    def sort(self):
        """Sort all parsed events in order of beginning time"""
        if not self.parsed_events:
            logger.debug('no events found to be sorted')
        else:
            # sort events by date
            by_date = lambda event: event['begin']
            self.parsed_events.sort(key=by_date)

    def clear_events(self):
        """clear previously parsed events"""

        self.parsed_events = []

    @staticmethod
    def all_day(event):
        """Check if an event is an all day event.
        Returns True if event is all day, else False
        """
        if not ('end' and 'begin') in event:
            print('Events must have a starting and ending time')
            raise Exception('This event is not valid!')
        else:
            begin, end = event['begin'], event['end']
            duration = end - begin
            if (begin.format('HH:mm') == '00:00' and end.format('HH:mm') == '00:00'
                    and duration.days >= 1):
                return True
            else:
                return False

    @staticmethod
    def get_system_tz():
        """Get the timezone set by the system"""

        try:
            local_tz = time.tzname[1]
        except:
            print('System timezone could not be parsed!')
            print('Please set timezone manually!. Setting timezone to None...')
            local_tz = None
        return local_tz

    def show_events(self, fmt='DD MMM YY HH:mm'):
        """print all parsed events in a more readable way
        use the format (fmt) parameter to specify the date format
        see https://arrow.readthedocs.io/en/latest/#supported-tokens
        for more info tokens
        """

        if not self.parsed_events:
            logger.debug('no events found to be shown')
        else:
            line_width = max(len(_['title']) for _ in self.parsed_events)
            for events in self.parsed_events:
                title = events['title']
                begin, end = events['begin'].format(fmt), events['end'].format(fmt)
                print('{0} {1} | {2} | {3}'.format(
                    title, ' ' * (line_width - len(title)), begin, end))


if __name__ == '__main__':
    print(f'running {__name__} in standalone mode')