#
# This file is part of SickGear.
#
# SickGear 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.
#
# SickGear 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 SickGear.  If not, see <http://www.gnu.org/licenses/>.

import datetime
import functools
import locale
import re
import sys

import sickgear
from dateutil import tz

from six import integer_types, PY2, string_types

# noinspection PyUnreachableCode
if False:
    from typing import Callable, Optional, Union

date_presets = ('%Y-%m-%d',
                '%a, %Y-%m-%d',
                '%A, %Y-%m-%d',
                '%y-%m-%d',
                '%a, %y-%m-%d',
                '%A, %y-%m-%d',
                '%m/%d/%Y',
                '%a, %m/%d/%Y',
                '%A, %m/%d/%Y',
                '%m/%d/%y',
                '%a, %m/%d/%y',
                '%A, %m/%d/%y',
                '%m-%d-%Y',
                '%a, %m-%d-%Y',
                '%A, %m-%d-%Y',
                '%m-%d-%y',
                '%a, %m-%d-%y',
                '%A, %m-%d-%y',
                '%m.%d.%Y',
                '%a, %m.%d.%Y',
                '%A, %m.%d.%Y',
                '%m.%d.%y',
                '%a, %m.%d.%y',
                '%A, %m.%d.%y',
                '%d-%m-%Y',
                '%a, %d-%m-%Y',
                '%A, %d-%m-%Y',
                '%d-%m-%y',
                '%a, %d-%m-%y',
                '%A, %d-%m-%y',
                '%d/%m/%Y',
                '%a, %d/%m/%Y',
                '%A, %d/%m/%Y',
                '%d/%m/%y',
                '%a, %d/%m/%y',
                '%A, %d/%m/%y',
                '%d.%m.%Y',
                '%a, %d.%m.%Y',
                '%A, %d.%m.%Y',
                '%d.%m.%y',
                '%a, %d.%m.%y',
                '%A, %d.%m.%y',
                '%d. %b %Y',
                '%a, %d. %b %Y',
                '%A, %d. %b %Y',
                '%d. %b %y',
                '%a, %d. %b %y',
                '%A, %d. %b %y',
                '%d. %B %Y',
                '%a, %d. %B %Y',
                '%A, %d. %B %Y',
                '%d. %B %y',
                '%a, %d. %B %y',
                '%A, %d. %B %y',
                '%b %d, %Y',
                '%a, %b %d, %Y',
                '%A, %b %d, %Y',
                '%B %d, %Y',
                '%a, %B %d, %Y',
                '%A, %B %d, %Y')

time_presets = ('%I:%M:%S %p',
                '%I:%M:%S %P',
                '%H:%M:%S')

is_win = 'win32' == sys.platform


# helper decorator class
# noinspection PyPep8Naming
class static_or_instance(object):
    def __init__(self, func):
        self.func = func

    def __get__(self, instance, owner):
        return functools.partial(self.func, instance)


# subclass datetime.datetime to add function to display custom date and time formats
class SGDatetime(datetime.datetime):
    has_locale = True

    @static_or_instance
    def is_locale_eng(self):
        today = SGDatetime.sbfdate(SGDatetime.now(), '%A').lower()
        return ('day' == today[-3::] and today[0:-3:] in ['sun', 'mon', 'tues', 'wednes', 'thurs', 'fri', 'satur']
                and SGDatetime.sbfdate(SGDatetime.now(), '%B').lower() in [
                    'january', 'february', 'march', 'april', 'may', 'june',
                    'july', 'august', 'september', 'october', 'november', 'december'])

    @static_or_instance
    def convert_to_setting(self, dt=None, force_local=False):
        # type: (Optional[datetime.datetime, SGDatetime], bool) -> Union[SGDatetime, datetime.datetime]
        obj = (dt, self)[self is not None]  # type: datetime.datetime
        try:
            if force_local or 'local' == sickgear.TIMEZONE_DISPLAY:
                from sickgear.network_timezones import SG_TIMEZONE
                return obj.astimezone(SG_TIMEZONE)
        except (BaseException, Exception):
            pass

        return obj

    @static_or_instance
    def setlocale(self, setlocale=True, use_has_locale=None, locale_str=''):
        if setlocale:
            try:
                if None is use_has_locale or use_has_locale:
                    locale.setlocale(locale.LC_TIME, locale_str)
            except locale.Error:
                if None is not use_has_locale:
                    SGDatetime.has_locale = False
                pass

    # display Time in SickGear Format
    @static_or_instance
    def sbftime(self, dt=None, show_seconds=False, t_preset=None, setlocale=True, markup=False):

        SGDatetime.setlocale(setlocale=setlocale, use_has_locale=SGDatetime.has_locale, locale_str='us_US')

        strt = ''

        obj = (dt, self)[self is not None]  # type: datetime.datetime
        if None is not obj:
            tmpl = (((sickgear.TIME_PRESET, sickgear.TIME_PRESET_W_SECONDS)[show_seconds]),
                    t_preset)[None is not t_preset]
            tmpl = (tmpl.replace(':%S', ''), tmpl)[show_seconds]

            strt = SGDatetime.sbstrftime(obj, tmpl.replace('%P', '%p'))

            if sickgear.TRIM_ZERO:
                strt = re.sub(r'^0(\d:\d\d)', r'\1', strt)

            if re.search(r'(?im)%p$', tmpl):
                if '%p' in tmpl:
                    strt = strt.upper()
                elif '%P' in tmpl:
                    strt = strt.lower()

                if sickgear.TRIM_ZERO:
                    strt = re.sub(r'(?im)^(\d+)(?::00)?(\s?[ap]m)', r'\1\2', strt)

            if markup:
                match = re.search(r'(?im)(\d{1,2})(?:(.)(\d\d)(?:(.)(\d\d))?)?(?:\s?([ap]m))?$', strt)
                if match:
                    strt = ('%s%s%s%s%s%s' % (
                        ('<span class="time-hr">%s</span>' % match.group(1), '')[None is match.group(1)],
                        ('<span class="time-hr-min">%s</span>' % match.group(2), '')[None is match.group(2)],
                        ('<span class="time-min">%s</span>' % match.group(3), '')[None is match.group(3)],
                        ('<span class="time-min-sec">%s</span>' % match.group(4), '')[None is match.group(4)],
                        ('<span class="time-sec">%s</span>' % match.group(5), '')[None is match.group(5)],
                        ('<span class="time-am-pm">%s</span>' % match.group(6), '')[None is match.group(6)]))

        SGDatetime.setlocale(setlocale=setlocale, use_has_locale=SGDatetime.has_locale)
        return strt

    # display Date in SickGear Format
    @static_or_instance
    def sbfdate(self, dt=None, d_preset=None, setlocale=True):

        SGDatetime.setlocale(setlocale=setlocale)

        strd = ''
        try:
            obj = (dt, self)[self is not None]  # type: datetime.datetime
            if None is not obj:
                strd = SGDatetime.sbstrftime(obj, (sickgear.DATE_PRESET, d_preset)[None is not d_preset])

        finally:
            SGDatetime.setlocale(setlocale=setlocale)
            return strd

    # display Datetime in SickGear Format
    @static_or_instance
    def sbfdatetime(self, dt=None, show_seconds=False, d_preset=None, t_preset=None, markup=False):

        SGDatetime.setlocale()

        strd = ''
        obj = (dt, self)[self is not None]  # type: datetime.datetime
        try:
            if None is not obj:
                strd = u'%s, %s' % (
                    SGDatetime.sbstrftime(obj, (sickgear.DATE_PRESET, d_preset)[None is not d_preset]),
                    SGDatetime.sbftime(dt, show_seconds, t_preset, False, markup))

        finally:
            SGDatetime.setlocale(use_has_locale=SGDatetime.has_locale)
            return strd

    @staticmethod
    def sbstrftime(obj, str_format):
        try:
            result = obj.strftime(str_format),
        except ValueError:
            result = obj.replace(tzinfo=None).strftime(str_format)
        return result if isinstance(result, string_types) else \
            isinstance(result, tuple) and 1 == len(result) and '%s' % result[0] or ''

    @static_or_instance
    def to_file_timestamp(self, dt=None):
        # type: (Optional[SGDatetime, datetime.datetime]) -> Union[float, integer_types]
        """
        convert datetime to filetime
        special handling for windows filetime issues
        for pre Windows 7 this can result in an exception for pre 1970 dates
        """
        obj = (dt, self)[self is not None]  # type: datetime.datetime
        if is_win:
            from .network_timezones import EPOCH_START_WIN
            return (obj.replace(tzinfo=tz.tzwinlocal()) - EPOCH_START_WIN).total_seconds()
        return SGDatetime.timestamp_far(obj)

    @staticmethod
    def from_timestamp(ts, local_time=True, tz_aware=False, tzinfo=None):
        # type: (Union[float, integer_types], bool, bool, datetime.tzinfo) -> datetime.datetime
        """
        convert timestamp to datetime.datetime obj
        :param ts: timestamp integer, float
        :param local_time: return as local timezone (SG_TIMEZONE)
        :param tz_aware: return tz aware datetime
        :param tzinfo: tzinfo to be used
        """
        from .network_timezones import EPOCH_START, SG_TIMEZONE
        result = EPOCH_START + datetime.timedelta(seconds=ts)
        if local_time and SG_TIMEZONE:
            result = result.astimezone(SG_TIMEZONE)
        if isinstance(tzinfo, datetime.tzinfo):
            result = result.astimezone(tzinfo)
        if not tz_aware:
            return result.replace(tzinfo=None)
        return result

    @static_or_instance
    def timestamp_far(self,
                      dt=None,  # type: Optional[SGDatetime, datetime.datetime]
                      default=None  # type: Optional[float, integer_types]
                      ):
        # type: (...) -> Union[float, integer_types, None]
        """
        Use `timestamp_far` for a timezone aware UTC timestamp in far future or far past
        """
        obj = (dt, self)[self is not None]  # type: datetime.datetime
        if isinstance(obj, datetime.datetime) and not isinstance(getattr(obj, 'tzinfo', None), datetime.tzinfo):
            from sickgear.network_timezones import SG_TIMEZONE
            obj = obj.replace(tzinfo=SG_TIMEZONE)
        from .network_timezones import EPOCH_START
        timestamp = default
        try:
            timestamp = (obj - EPOCH_START).total_seconds()
        finally:
            return (default, timestamp)[isinstance(timestamp, (float, integer_types))]


if PY2:
    """
    Use `timestamp_near` for a timezone aware UTC timestamp in the near future or recent past.

    Under py3, using the faster variable assigned cpython callable, so py2 is set up to mimic the signature types.
    Note: the py3 callable is limited to datetime.datetime and does not work with datetime.date.
    """
    def _py2timestamp(dt=None):
        # type: (datetime.datetime) -> float
        try:
            import time
            return int(time.mktime(dt.timetuple()))
        except (BaseException, Exception):
            return 0
    timestamp_near = _py2timestamp  # type: Callable[[datetime.datetime], float]
else:
    # py3 native timestamp uses milliseconds
    timestamp_near = datetime.datetime.timestamp  # type: Callable[[datetime.datetime], float]