# reminder - A maubot plugin to remind you about things.
# Copyright (C) 2020 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.
from typing import NamedTuple, Union, Pattern, Match, Dict, Type, Optional, TYPE_CHECKING
from datetime import datetime
from abc import ABC, abstractmethod
import re

from dateutil.relativedelta import MO

WeekdayType = type(MO)

if TYPE_CHECKING:
    from typing import TypedDict


    class RelativeDeltaParams(TypedDict):
        year: int
        month: int
        day: int
        hour: int
        minute: int
        second: int
        microsecond: int

        years: Union[int, float]
        months: Union[int, float]
        weeks: Union[int, float]
        days: Union[int, float]
        hours: Union[int, float]
        minutes: Union[int, float]
        seconds: Union[int, float]
        microseconds: Union[int, float]

        weekday: Union[int, WeekdayType]

        leapdays: int
        yearday: int
        nlyearday: int


class MatcherReturn(NamedTuple):
    params: 'RelativeDeltaParams'
    unconsumed: str


class Matcher(ABC):
    @abstractmethod
    def match(self, val: str) -> Optional[MatcherReturn]:
        pass


def int_or_float(val: str) -> Union[int, float]:
    if "," in val:
        return float(val.replace(",", "."))
    elif "." in val:
        return float(val)
    return int(val)


class RegexMatcher(Matcher):
    regex: Pattern
    value_type: Type

    def __init__(self, pattern: str, value_type: Type = int_or_float) -> None:
        self.regex = re.compile(pattern, re.IGNORECASE)
        self.value_type = value_type

    def match(self, val: str) -> Optional[MatcherReturn]:
        match = self.regex.match(val)
        if match and match.end() > 0 and len(match.groups()) > 0:
            return MatcherReturn(params=self._convert_match(match), unconsumed=val[match.end():])
        return None

    def _convert_match(self, match: Match) -> 'RelativeDeltaParams':
        return self._convert_groups(match.groupdict())

    def _convert_groups(self, groups: Dict[str, str]) -> 'RelativeDeltaParams':
        return {key: self.value_type(value) for key, value in groups.items() if value}


class TimeMatcher(RegexMatcher):
    def _convert_match(self, match: Match) -> 'RelativeDeltaParams':
        groups = match.groupdict()
        try:
            meridiem = groups.pop("meridiem").lower()
        except (KeyError, AttributeError):
            meridiem = None
        params = self._convert_groups(groups)
        if meridiem == "pm":
            params["hour"] += 12
        elif meridiem == "am" and params["hour"] == 12:
            params["hour"] = 0
        return params


class ShortYearMatcher(RegexMatcher):
    def _convert_match(self, match: Match) -> 'RelativeDeltaParams':
        params = super()._convert_match(match)
        if params["year"] < 100:
            year = datetime.now().year
            current_century = year // 100
            if params["year"] < year % 100:
                current_century += 1
            params["year"] = (current_century * 100) + params["year"]
        return params


class WeekdayMatcher(Matcher):
    regex: Pattern
    map: Dict[str, Union[int, WeekdayType]]
    substr: int

    def __init__(self, pattern: str, map: Dict[str, Union[int, WeekdayType]], substr: int) -> None:
        self.regex = re.compile(pattern, re.IGNORECASE)
        self.map = map
        self.substr = substr

    def match(self, val: str) -> Optional[MatcherReturn]:
        match = self.regex.match(val)
        if match and match.end() > 0:
            weekday = self.map[match.string[:self.substr].lower()]
            if isinstance(weekday, int):
                weekday = (datetime.now().weekday() + weekday) % 7
            return MatcherReturn(params={"weekday": weekday}, unconsumed=val[match.end():])
        return None


class Locale(Matcher):
    name: str
    timedelta: Matcher
    date: Matcher
    weekday: Matcher
    time: Matcher

    def __init__(self, name: str, timedelta: Matcher, date: Matcher, weekday: Matcher,
                 time: Matcher) -> None:
        self.name = name
        self.timedelta = timedelta
        self.date = date
        self.weekday = weekday
        self.time = time

    def replace(self, name: str, timedelta: Matcher = None, date: Matcher = None,
                weekday: Matcher = None, time: Matcher = None) -> 'Locale':
        return Locale(name=name, timedelta=timedelta or self.timedelta, date=date or self.date,
                      weekday=weekday or self.weekday, time=time or self.time)

    def match(self, val: str) -> Optional[MatcherReturn]:
        found_delta = self.timedelta.match(val)
        if found_delta:
            params, val = found_delta
        else:
            params = {}
            found_day = self.weekday.match(val)
            if found_day:
                params, val = found_day
            else:
                found_date = self.date.match(val)
                if found_date:
                    params, val = found_date

            found_time = self.time.match(val)
            if found_time:
                params = {**params, **found_time.params}
                val = found_time.unconsumed
        return MatcherReturn(params=params, unconsumed=val) if len(params) > 0 else None


Locales = Dict[str, Locale]