import re
import copy

from datetime import timedelta, datetime

from moodle import MoodleQuiz, MoodleHomework, MoodleLesson, MoodleFeedback, \
    MoodleChoice
from ics_calendar import Seminar, Practicum, Laboratory
from activity_loader import ActivityLoader
from common import InvalidSyntaxException


class AbsoluteTimeModifierException(Exception):
    """Raised if the absolute time has an invalid time format (24:00)"""
    def __init__(self, str):
        self.message = '\
Invalid absolute time modifier. Could not interpret value: "%s"' % str

    def __str__(self):
        return repr(self.message)


class InvalidModifiersException(InvalidSyntaxException):
    """Raised if modifiers could not be isolated or interpreted."""
    def __init__(self, str):
        self.message = '\
Invalid syntax while parsing modifiers from string "%s"' % str


class InvalidEventIdentifier(InvalidSyntaxException):
    """Raised if events could not be linked to content."""
    def __init__(self, str):
        self.message = '\
Unknown event id "%s". Please use keys from available activities.' % str

    def __str__(self):
        return repr(self.message)


class InvalidSubjectException(InvalidSyntaxException):
    """Raised if planning line contains a meeting as the subject."""
    def __init__(self, tokens):
        full_line = ' '.join(tokens)
        subject = tokens[0]
        self.message = 'Line "%s" refers to the meeting "%s" as the subject. \
Lines must start with an activity followed by meetings keys.' % \
            (full_line, subject)

    def __str__(self):
        return repr(self.message)


class Interpreter():

    modifiers_regex = re.compile(
        r'^[a-z]{1,2}[0-9]{1,2}(?P<end>[sf])?(?P<rel>[+-][0-9]+[wdhm])?' +
        r'(?:\@(?P<time>[0-9]{1,2}\:[0-9]{1,2}))?$', re.IGNORECASE)

    timedelta_regex = re.compile(
        r'^(?P<neg>\-)?\+?(:?(?P<weeks>[0-9]+)w)?(:?(?P<days>[0-9]+)d)?' +
        r'(:?(?P<hours>[0-9]+)h)?(:?(?P<minutes>[0-9]+)m)?$', re.IGNORECASE)

    planets_name_regex = re.compile(r'\"(?P<name>[\w\s]+)\"$', re.IGNORECASE)

    candidate_classes_dict = {
        'moodle': [MoodleQuiz, MoodleLesson, MoodleFeedback,
                   MoodleHomework, MoodleChoice],
        'calendar': [Seminar, Practicum],
        # 'user': [Exam, UserQuiz]
    }

    candidate_classes = [
        # Imported from  MBZ
        MoodleQuiz, MoodleLesson, MoodleFeedback,
        MoodleHomework, MoodleChoice,
        # Imported from calendar
        Seminar, Practicum, Laboratory
    ]

    def __init__(self, meetings, course):
        self.meetings = meetings
        self.course = course
        self.__build_candidates()

    def __build_candidates(self):
        self.candidates = {}

        for clazz in self.candidate_classes:
            self.__build_candidate(clazz)

        loader = ActivityLoader()
        for clazz in loader.get_activities_instances():
            self.__build_candidate(clazz)

    def __build_candidate(self, clazz):
        regex_str = '^%s(?P<id>[0-9]{1,2})([sf]?)' % clazz.key
        self.candidates[clazz.key] = (re.compile(regex_str, re.IGNORECASE),
                                      clazz)

    def get_new_event_from_string(self, string):
        tokens = self._split_line(string)
        event = self._parse_subject(tokens)

        # Try to get planets' name
        r = self.planets_name_regex.search(string)
        if r:
            event.show_planets = True
            event.planets_name = r.groupdict()['name']

            # Length of the name and quotations
            length = len(event.planets_name) + 2
            # Regenerate tokens without name
            tokens = self._split_line(string[:-length])

        date_tokens_len = len(tokens) - 1

        if date_tokens_len < event.minimum_dates_count or \
                date_tokens_len > event.maximum_dates_count:
            raise InvalidSyntaxException(
                message=('Activity "%s" must have between %d and %d dates' +
                         '. %d given.') %
                (event.__class__.__name__, event.minimum_dates_count,
                 event.maximum_dates_count, date_tokens_len))

        for i, token in enumerate(tokens[1:]):
            event._set_date_at_index(self._get_datetime_from_token(token), i)
        return event

    def _get_datetime_from_token(self, token):
        modifiers = self._get_modifiers_as_string(token)
        event = self._get_event_or_activity_from_token(token)

        if not event:
            return None

        datetime = event.get_end_datetime() \
            if modifiers[0] else event.get_start_datetime()

        relative_mod = self._interpret_relative_modifier(modifiers[1])
        time_mod = self._interpret_time_modifier(modifiers[2])

        return self._get_new_datetime(datetime, relative_mod, time_mod)

    def _get_event_or_activity_from_token(self, token):
        # TODO: find a more elegant solution
        try:
            event_clazz, event_id = self._detect_event_class_and_id(token)

            if event_clazz.is_activity():
                return self.course.get_activity_by_type_and_num(
                    event_clazz, event_id)
            if event_clazz.is_user_defined():
                instance = event_clazz
                instance.rel_id = event_id
                return instance
            return self.meetings[event_clazz][event_id - 1]
        except Exception:
            raise InvalidEventIdentifier(token)

    def _parse_subject(self, tokens):
        """Returns the event described by the first token of string
        """
        event_clazz, event_id = self._detect_event_class_and_id(tokens[0])

        if not event_clazz.is_activity():
            raise InvalidSubjectException(tokens)

        if event_clazz.is_user_defined():
            instance_copy = copy.deepcopy(event_clazz)
            instance_copy.rel_id = event_id
            return instance_copy

        if self.course:
            return self.course.get_activity_by_type_and_num(
                event_clazz, event_id)
        raise InvalidEventIdentifier(tokens[0])

    def _detect_event_class_and_id(self, string):
        """Returns a tuple of the class and the meeting id."""
        for key, (regex, clazz) in self.candidates.items():
            r = regex.search(string)

            if r and r.groupdict()['id']:
                return (clazz, int(r.groupdict()['id']))
        raise InvalidEventIdentifier(string)

    def _split_line(self, string):
        # Remove sequential spaces before trim
        return re.sub(' +', ' ', string).strip().split(' ')

    def _get_modifiers_as_string(self, string):
        """Returns tuple (at_end, relative_modifier_str, time_modifier_str)

        at_end: True if the modifiers should be applied to the end of the
                event. False if the modifiers should be applied to the start
                of the event.

        relative_modifier_str: The delta to apply to the event start or end as a
                           string. Supports +/- w/d/h/m for weeks, days,
                           hours and minutes.
                           ex: '-2w', '-1d', '+15m', '+4h'

        time_modifier_str: None or a modifier of the final time as a string.
                       Must be applied last.
                       ex: @23:55
        """
        r = self.modifiers_regex.search(string)
        if not r:
            raise InvalidModifiersException(string)

        at_end = str(r.groupdict()['end']).upper() == 'F'
        relative_modifier_str = r.groupdict()['rel']
        time_modifier_str = r.groupdict()['time']

        return (at_end, relative_modifier_str, time_modifier_str)

    def _interpret_time_modifier(self, time_modifier_str):
        if not time_modifier_str:
            return
        try:
            return datetime.strptime(time_modifier_str, '%H:%M').time()
        except Exception:
            raise AbsoluteTimeModifierException(time_modifier_str)

    def _interpret_relative_modifier(self, relative_modifier_str):
        if not relative_modifier_str:
            return

        r = self.timedelta_regex.search(relative_modifier_str)
        if not r:
            raise InvalidModifiersException(relative_modifier_str)

        negative_modifier = -1 if r.groupdict()['neg'] else 1

        weeks = int(r.groupdict()['weeks']) * negative_modifier \
            if r.groupdict()['weeks'] else 0

        days = int(r.groupdict()['days']) * negative_modifier \
            if r.groupdict()['days'] else 0

        hours = int(r.groupdict()['hours']) * negative_modifier \
            if r.groupdict()['hours'] else 0

        minutes = int(r.groupdict()['minutes']) * negative_modifier \
            if r.groupdict()['minutes'] else 0

        return timedelta(days=days, hours=hours, minutes=minutes, weeks=weeks)

    def _get_new_datetime(self, datetime, relative_mod, time_mod):
        """Build new datetime from relative and time modifiers."""
        if relative_mod:
            datetime += relative_mod

        if time_mod:
            return datetime.replace(hour=time_mod.hour, minute=time_mod.minute)

        return datetime
