# Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018  Rickard Lindberg, Roger Lindberg
#
# This file is part of Timeline.
#
# Timeline is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Timeline 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Timeline.  If not, see <http://www.gnu.org/licenses/>.


from datetime import datetime
import re

import wx

from timelinelib.calendar.generic.timepicker.periodpicker import PeriodPicker
from timelinelib.calendar.gregorian.gregorian2julian import gregorian_datetime_to_julian_day_and_seconds
from timelinelib.canvas.data import time_period_center
from timelinelib.wxgui.dialogs.timeeditor.view import open_time_editor_dialog
from timelinelib.canvas.data import TimeOutOfRangeLeftError
from timelinelib.canvas.data import TimeOutOfRangeRightError
from timelinelib.canvas.data import TimePeriod
from timelinelib.calendar.gregorian.timetype.durationformatter import DurationFormatter
from timelinelib.calendar.coptic.time import SECONDS_IN_DAY
from timelinelib.calendar.gregorian.timetype.durationtype import YEARS, DAYS, HOURS, MINUTES, SECONDS


class TimeType:

    DURATION_TYPE_HOURS = _('Hours')
    DURATION_TYPE_WORKDAYS = _('Workdays')
    DURATION_TYPE_DAYS = _('Days')
    DURATION_TYPE_MINUTES = _('Minutes')
    DURATION_TYPE_SECONDS = _('Seconds')
    DURATION_TYPE_1 = _('Divide by 1')
    DURATION_TYPE_2 = _('Divide by 10')
    DURATION_TYPE_3 = _('Divide by 100')
    DURATION_TYPE_4 = _('Divide by 1000')
    DURATION_TYPE_5 = _('Divide by 10000')

    @property
    def name(self):
        raise NotImplementedError(self._not_implemented_string("name"))

    @property
    def TimeClass(self):
        raise NotImplementedError(self._not_implemented_string("TimeClass"))

    @property
    def DateTimeClass(self):
        raise NotImplementedError(self._not_implemented_string("DateTimeClass"))

    @property
    def DeltaClass(self):
        raise NotImplementedError(self._not_implemented_string("DeltaClass"))

    @property
    def months_in_year(self):
        """Default implementation."""
        return self.DateTimeClass.months_in_year()

    def __eq__(self, other):
        return isinstance(other, self.__class__)

    def __ne__(self, other):
        return not (self == other)

    def time_string(self, time):
        """Default implementation"""
        return "%d-%02d-%02d %02d:%02d:%02d" % self.DateTimeClass.from_time(time).to_tuple()

    def parse_time(self, time_string):
        """Default implementation."""
        match = re.search(r"^(-?\d+)-(\d+)-(\d+) (\d+):(\d+):(\d+)$", time_string)
        if match:
            year = int(match.group(1))
            month = int(match.group(2))
            day = int(match.group(3))
            hour = int(match.group(4))
            minute = int(match.group(5))
            second = int(match.group(6))
            try:
                return self.DateTimeClass(year, month, day, hour, minute, second).to_time()
            except ValueError:
                raise ValueError("Invalid time, time string = '%s'" % time_string)
        else:
            raise ValueError("Time not on correct format = '%s'" % time_string)

    def days_in_week(self, time):
        """Default implementation."""
        return self.DateTimeClass.from_time(time).days_in_week

    def get_navigation_functions(self):
        raise NotImplementedError(self._not_implemented_string("get_navigation_functions"))

    def format_period(self, time_period):
        """
        Default implementation.
        Returns a unicode string describing the time period.
        """
        def label_with_time(time):
            return "%s %s" % (label_without_time(time), time_label(time))

        def label_without_time(time):
            datetime = self.DateTimeClass.from_time(time)
            return "%s %s %s" % (datetime.day,
                                 datetime.abbreviated_month_name,
                                 datetime.year)

        def time_label(time):
            return "%02d:%02d" % time.get_time_of_day()[:-1]

        if time_period.is_period():
            if time_period.has_nonzero_time():
                label = "%s to %s" % (label_with_time(time_period.start_time),
                                      label_with_time(time_period.end_time))
            else:
                label = "%s to %s" % (label_without_time(time_period.start_time),
                                      label_without_time(time_period.end_time))
        else:
            if time_period.has_nonzero_time():
                label = "%s" % label_with_time(time_period.start_time)
            else:
                label = "%s" % label_without_time(time_period.start_time)
        return label

    def format_delta(self, delta):
        """Default implementation."""
        days = abs(delta.get_days())
        seconds = abs(delta.seconds) - days * SECONDS_IN_DAY
        delta_format = (YEARS, DAYS, HOURS, MINUTES, SECONDS)
        return DurationFormatter([days, seconds]).format(delta_format)

    def get_min_time(self):
        """Default implementation."""
        return self.TimeClass.min()

    def get_max_time(self):
        """Default implementation."""
        return self.TimeClass.max()

    def get_min_wx_time(self):
        """
        Default implementation.
        This function is only needed if the subclass uses the CalendarPopup class
        """
        return self._to_wx_time(self.get_min_time())

    def get_max_wx_time(self):
        """
        Default implementation.
        This function is only needed if the subclass uses the CalendarPopup class
        """
        return self._to_wx_time(self.get_max_time())

    def choose_strip(self, metrics, appearance):
        raise NotImplementedError(self._not_implemented_string("choose_strip"))

    def get_default_time_period(self):
        """Default implementation."""
        return time_period_center(self.now(), self.DeltaClass.from_days(30))

    def supports_saved_now(self):
        """Default implementation."""
        return False

    def set_saved_now(self, time):
        """Default implementation."""
        pass

    def now(self):
        """Default implementation."""
        return self.TimeClass(*gregorian_datetime_to_julian_day_and_seconds(datetime.now()))

    def get_min_zoom_delta(self):
        """Default implementation."""
        return self.DeltaClass.from_seconds(60), _("Can't zoom deeper than 1 minute")

    def get_duplicate_functions(self):
        """Default implementation."""
        return [
            (_("Day"), self.move_period_num_days),
            (_("Week"), self.move_period_num_weeks),
            (_("Month"), self.move_period_num_months),
            (_("Year"), self.move_period_num_years),
        ]

    def is_special_day(self, time):
        """Default implementation."""
        return False

    def create_period_picker(self, parent, *args, **kwargs):
        """Default implementation."""
        return PeriodPicker(parent, self, *args, **kwargs)

    def create_time_picker(self, parent, *args, **kwargs):
        raise NotImplementedError(self._not_implemented_string("create_time_picker"))

    def get_duration_types(self):
        """Default implementation."""
        return [
            self.DURATION_TYPE_HOURS,
            self.DURATION_TYPE_WORKDAYS,
            self.DURATION_TYPE_DAYS,
            self.DURATION_TYPE_MINUTES,
            self.DURATION_TYPE_SECONDS]

    def get_duration_divisor(self, duration_type, workday_length):
        """Default implementation."""
        return {
            self.DURATION_TYPE_SECONDS: 1,
            self.DURATION_TYPE_MINUTES: 60,
            self.DURATION_TYPE_HOURS: 3600,
            self.DURATION_TYPE_DAYS: 86400,
            self.DURATION_TYPE_WORKDAYS: 3600 * workday_length,
        }[duration_type]

    def _not_implemented_string(self, function_name):
        return f"The function {function_name} is not defined in class {self.__class__.__name__}."

    def _to_wx_time(self, time):
        MIN_WX_YEAR = -4712
        year, month, day = self.DateTimeClass.from_time(time).to_date_tuple()
        year = max(MIN_WX_YEAR, year)
        try:
            return wx.DateTime.FromDMY(day, month - 1, year, 0, 0, 0)
        except OverflowError:
            if year < 0:
                year, month, day = self.DateTimeClass.from_time(self.TimeClass(0, 0)).to_date_tuple()
                return wx.DateTime.FromDMY(day, month - 1, year, 0, 0, 0)

    #
    # Navigation Functions
    #

    def go_to_today_fn(self, main_frame, current_period, navigation_fn):
        """Default implementation."""
        navigation_fn(lambda tp: tp.center(self.now()))

    def go_to_date_fn(self, main_frame, current_period, navigation_fn):
        """Default implementation."""

        def navigate_to(time):
            navigation_fn(lambda tp: tp.center(time))

        open_time_editor_dialog(main_frame, self.__class__(), current_period.mean_time(), navigate_to, _("Go to Date"))

    def backward_fn(self, main_frame, current_period, navigation_fn):
        self._move_page_smart(current_period, navigation_fn, -1)

    def forward_fn(self, main_frame, current_period, navigation_fn):
        self._move_page_smart(current_period, navigation_fn, 1)

    def forward_one_week_fn(self, main_frame, current_period, navigation_fn):
        wk = self.DeltaClass.from_days(self.days_in_week(current_period.start_time))
        navigation_fn(lambda tp: tp.move_delta(wk))

    def backward_one_week_fn(self, main_frame, current_period, navigation_fn):
        wk = self.DeltaClass.from_days(self.days_in_week(current_period.start_time))
        navigation_fn(lambda tp: tp.move_delta(-1 * wk))

    def _move_page_smart(self, current_period, navigation_fn, direction):
        if self.whole_number_of_years(current_period):
            self.move_page_years(current_period, navigation_fn, direction)
        elif self.whole_number_of_months(current_period):
            self.move_page_months(current_period, navigation_fn, direction)
        else:
            navigation_fn(lambda tp: tp.move_delta(direction * current_period.delta()))

    def move_page_years(self, curret_period, navigation_fn, direction):
        """Default implementation."""

        def navigate(tp):
            year_delta = direction * self.calculate_year_diff(curret_period)
            start = self.DateTimeClass.from_time(curret_period.start_time)
            end = self.DateTimeClass.from_time(curret_period.end_time)
            new_start_year = start.year + year_delta
            new_end_year = end.year + year_delta
            try:
                new_start = start.replace(year=new_start_year).to_time()
                new_end = end.replace(year=new_end_year).to_time()
                if new_end > self.get_max_time():
                    raise ValueError()
                if new_start < self.get_min_time():
                    raise ValueError()
            except ValueError:
                if direction < 0:
                    raise TimeOutOfRangeLeftError()
                else:
                    raise TimeOutOfRangeRightError()
            return tp.update(new_start, new_end)

        navigation_fn(navigate)

    def move_page_months(self, curret_period, navigation_fn, direction):
        """Default implementation."""

        def navigate(tp):
            start = self.DateTimeClass.from_time(curret_period.start_time)
            end = self.DateTimeClass.from_time(curret_period.end_time)
            start_months = start.year * self.months_in_year + start.month
            end_months = end.year * self.months_in_year + end.month
            month_diff = end_months - start_months
            month_delta = month_diff * direction
            new_start_year, new_start_month = self.months_to_year_and_month(start_months + month_delta)
            new_end_year, new_end_month = self.months_to_year_and_month(end_months + month_delta)
            try:
                new_start = start.replace(year=new_start_year, month=new_start_month)
                new_end = end.replace(year=new_end_year, month=new_end_month)
                start = new_start.to_time()
                end = new_end.to_time()
                if end > self.get_max_time():
                    raise ValueError()
                if start < self.get_min_time():
                    raise ValueError()
            except ValueError:
                if direction < 0:
                    raise TimeOutOfRangeLeftError()
                else:
                    raise TimeOutOfRangeRightError()
            return tp.update(start, end)

        navigation_fn(navigate)

    def calculate_year_diff(self, period):
        """Default implementation."""
        return (self.DateTimeClass.from_time(period.end_time).year -
                self.DateTimeClass.from_time(period.start_time).year)

    def months_to_year_and_month(self, months):
        years = int(months // self.months_in_year)
        month = months - years * self.months_in_year
        if month == 0:
            month = self.months_in_year
            years -= 1
        return years, month

    def whole_number_of_years(self, period):
        return (self.DateTimeClass.from_time(period.start_time).is_first_day_in_year() and
                self.DateTimeClass.from_time(period.end_time).is_first_day_in_year() and
                self.calculate_year_diff(period) > 0)

    def whole_number_of_months(self, period):
        start, end = self.DateTimeClass.from_time(period.start_time), self.DateTimeClass.from_time(period.end_time)
        start_months = start.year * self.months_in_year + start.month
        end_months = end.year * self.months_in_year + end.month
        month_diff = end_months - start_months
        return (start.is_first_of_month() and
                end.is_first_of_month() and
                month_diff > 0)

    def forward_one_month_fn(self, main_frame, current_period, navigation_fn):
        self.navigate_month_step(current_period, navigation_fn, 1)

    def backward_one_month_fn(self, main_frame, current_period, navigation_fn):
        self.navigate_month_step(current_period, navigation_fn, -1)

    def navigate_month_step(self, current_period, navigation_fn, direction):
        tm = current_period.start_time
        datetime = self.DateTimeClass.from_time(tm)
        if direction == -1:
            if datetime.month == 1:
                datetime = datetime.replace(year=datetime.year - 1, month=self.months_in_year)
            else:
                datetime = datetime.replace(month=datetime.month - 1)
        mv = self.DeltaClass.from_days(datetime.days_in_month())
        navigation_fn(lambda tp: tp.move_delta(direction * mv))

    def forward_one_year_fn(self, main_frame, current_period, navigation_fn):
        dt = self.DateTimeClass.from_time(current_period.start_time)
        yr = self.DeltaClass.from_days(dt.days_in_year)
        navigation_fn(lambda tp: tp.move_delta(yr))

    def backward_one_year_fn(self, main_frame, current_period, navigation_fn):
        dt = self.DateTimeClass.from_time(current_period.start_time)
        dt.replace(year=dt.year - 1)
        yr = self.DeltaClass.from_days(dt.days_in_year)
        navigation_fn(lambda tp: tp.move_delta(-1 * yr))

    def create_strip_fitter(self, strip_cls):
        def fit(main_frame, current_period, navigation_fn):
            def navigate(time_period):
                strip = strip_cls()
                start = strip.start(current_period.mean_time())
                end = strip.increment(start)
                return time_period.update(start, end)
            navigation_fn(navigate)
        return fit

    def fit_week_fn(self, main_frame, current_period, navigation_fn):
        mean = self.DateTimeClass.from_time(current_period.mean_time())
        start = self.DateTimeClass.from_ymd(mean.year, mean.month, mean.day).to_time()
        weekday = start.day_of_week
        start = start - self.DeltaClass.from_days(weekday)
        if not main_frame.config.week_starts_on_monday():
            start = start - self.DeltaClass.from_days(1)
        end = start + self.DeltaClass.from_days(7)
        navigation_fn(lambda tp: tp.update(start, end))

    def get_millenium_max_year(self):
        """Default implementation."""
        return self.DateTimeClass.from_time(self.get_max_time()).year - 1000

    def get_min_year_containing_jan_1(self):
        """Default implementation."""
        return self.DateTimeClass.from_time(self.get_min_time()).year + 1

    def fit_millennium_fn(self, main_frame, current_period, navigation_fn):
        mean = self.DateTimeClass.from_time(current_period.mean_time())
        if mean.year > self.get_millenium_max_year():
            year = self.get_millenium_max_year()
        else:
            year = max(self.get_min_year_containing_jan_1(), int(mean.year // 1000) * 1000)
        start = self.DateTimeClass.from_ymd(year, 1, 1).to_time()
        end = self.DateTimeClass.from_ymd(year + 1000, 1, 1).to_time()
        navigation_fn(lambda tp: tp.update(start, end))

    def move_period_num_days(self, period, num):
        delta = self.DeltaClass.from_days(1) * num
        start_time = period.start_time + delta
        end_time = period.end_time + delta
        return TimePeriod(start_time, end_time)

    def move_period_num_weeks(self, period, num):
        delta = self.DeltaClass.from_days(7) * num
        start_time = period.start_time + delta
        end_time = period.end_time + delta
        return TimePeriod(start_time, end_time)

    def move_period_num_months(self, period, num):

        def move_time(time):
            datetime = self.DateTimeClass.from_time(time)
            new_month = datetime.month + num
            new_year = datetime.year
            while new_month < 1:
                new_month += self.months_in_year
                new_year -= 1
            while new_month > self.months_in_year:
                new_month -= self.months_in_year
                new_year += 1
            return datetime.replace(year=new_year, month=new_month).to_time()

        try:
            return TimePeriod(
                move_time(period.start_time),
                move_time(period.end_time)
            )
        except ValueError:
            return None

    def move_period_num_years(self, period, num):
        try:
            delta = num
            start_year = self.DateTimeClass.from_time(period.start_time).year
            end_year = self.DateTimeClass.from_time(period.end_time).year
            start_time = self.DateTimeClass.from_time(period.start_time).replace(year=start_year + delta)
            end_time = self.DateTimeClass.from_time(period.end_time).replace(year=end_year + delta)
            return TimePeriod(start_time.to_time(), end_time.to_time())
        except ValueError:
            return None
