# -*- coding: utf-8 -*-
import calendar
import collections
from datetime import datetime, timedelta
from warnings import warn

import six
import regex as re
from dateutil.relativedelta import relativedelta

from dateparser.date_parser import date_parser
from dateparser.freshness_date_parser import freshness_date_parser
from dateparser.languages.loader import LanguageDataLoader
from dateparser.languages.detection import AutoDetectLanguage, ExactLanguages
from dateparser.conf import apply_settings
from dateparser.utils import normalize_unicode, apply_timezone_from_settings


APOSTROPHE_LOOK_ALIKE_CHARS = [
    u'\N{RIGHT SINGLE QUOTATION MARK}',     # u'\u2019'
    u'\N{MODIFIER LETTER APOSTROPHE}',      # u'\u02bc'
    u'\N{MODIFIER LETTER TURNED COMMA}',    # u'\u02bb'
    u'\N{ARMENIAN APOSTROPHE}',             # u'\u055a'
    u'\N{LATIN SMALL LETTER SALTILLO}',     # u'\ua78c'
    u'\N{PRIME}',                           # u'\u2032'
    u'\N{REVERSED PRIME}',                  # u'\u2035'
    u'\N{MODIFIER LETTER PRIME}',           # u'\u02b9'
    u'\N{FULLWIDTH APOSTROPHE}',            # u'\uff07'
]

RE_NBSP = re.compile(u'\xa0', flags=re.UNICODE)
RE_SPACES = re.compile(r'\s+')
RE_TRIM_SPACES = re.compile(r'^\s+(\S.*?)\s+$')

RE_SANITIZE_SKIP = re.compile(r'\t|\n|\r|\u00bb|,\s\u0432|\u200e|\xb7|\u200f|\u064e|\u064f', flags=re.M)
RE_SANITIZE_RUSSIAN = re.compile(r'([\W\d])\u0433\.', flags=re.I | re.U)
RE_SANITIZE_AMPM = re.compile(r'\b([ap])(\.)?m(\.)?\b', flags=re.DOTALL | re.I)
RE_SANITIZE_ON = re.compile(r'^.*?on:\s+(.*)')
RE_SANITIZE_APOSTROPHE = re.compile(u'|'.join(APOSTROPHE_LOOK_ALIKE_CHARS))

RE_SEARCH_TIMESTAMP = re.compile(r'^\d{10}(?![^\d.])')


def sanitize_spaces(html_string):
    html_string = RE_NBSP.sub(' ', html_string)
    html_string = RE_SPACES.sub(' ', html_string)
    html_string = RE_TRIM_SPACES.sub(r'\1', html_string)
    return html_string


def date_range(begin, end, **kwargs):
    dateutil_error_prone_args = ['year', 'month', 'week', 'day', 'hour',
                                 'minute', 'second']
    for arg in dateutil_error_prone_args:
        if arg in kwargs:
            raise ValueError("Invalid argument: %s" % arg)

    step = relativedelta(**kwargs) if kwargs else relativedelta(days=1)

    date = begin
    while date < end:
        yield date
        date += step

    # handles edge-case when iterating months and last interval is < 30 days
    if kwargs.get('months', 0) > 0 and (date.year, date.month) == (end.year, end.month):
        yield end


def get_intersecting_periods(low, high, period='day'):
    if period not in ['year', 'month', 'week', 'day', 'hour', 'minute', 'second', 'microsecond']:
        raise ValueError("Invalid period: {}".format(period))

    if high <= low:
        return

    step = relativedelta(**{period + 's': 1})

    current_period_start = low
    if isinstance(current_period_start, datetime):
        reset_arguments = {}
        for test_period in ['microsecond', 'second', 'minute', 'hour']:
            if test_period == period:
                break
            else:
                reset_arguments[test_period] = 0
        current_period_start = current_period_start.replace(**reset_arguments)

    if period == 'week':
        current_period_start \
            = current_period_start - timedelta(days=current_period_start.weekday())
    elif period == 'month':
        current_period_start = current_period_start.replace(day=1)
    elif period == 'year':
        current_period_start = current_period_start.replace(month=1, day=1)

    while current_period_start < high:
        yield current_period_start
        current_period_start += step


def sanitize_date(date_string):
    date_string = RE_SANITIZE_SKIP.sub(' ', date_string)
    date_string = RE_SANITIZE_RUSSIAN.sub(r'\1 ', date_string)  # remove u'г.' (Russian for year) but not in words
    date_string = sanitize_spaces(date_string)
    date_string = RE_SANITIZE_AMPM.sub(r'\1m', date_string)
    date_string = RE_SANITIZE_ON.sub(r'\1', date_string)

    date_string = RE_SANITIZE_APOSTROPHE.sub(u"'", date_string)

    return date_string


def get_date_from_timestamp(date_string, settings):
    if RE_SEARCH_TIMESTAMP.search(date_string):
        date_obj = datetime.fromtimestamp(int(date_string[:10]))
        date_obj = apply_timezone_from_settings(date_obj, settings)
        return date_obj


def get_last_day_of_month(year, month):
    return calendar.monthrange(year, month)[1]


def parse_with_formats(date_string, date_formats, settings):
    """ Parse with formats and return a dictionary with 'period' and 'obj_date'.

    :returns: :class:`datetime.datetime`, dict or None

    """
    period = 'day'
    for date_format in date_formats:
        try:
            date_obj = datetime.strptime(date_string, date_format)
        except ValueError:
            continue
        else:
            # If format does not include the day, use last day of the month
            # instead of first, because the first is usually out of range.
            if '%d' not in date_format:
                period = 'month'
                date_obj = date_obj.replace(
                    day=get_last_day_of_month(date_obj.year, date_obj.month))

            if not ('%y' in date_format or '%Y' in date_format):
                today = datetime.today()
                date_obj = date_obj.replace(year=today.year)

            date_obj = apply_timezone_from_settings(date_obj, settings)

            return {'date_obj': date_obj, 'period': period}
    else:
        return {'date_obj': None, 'period': period}


class _DateLanguageParser(object):
    DATE_FORMATS_ERROR_MESSAGE = "Date formats should be list, tuple or set of strings"

    def __init__(self, language, date_string, date_formats, settings=None):
        self._settings = settings
        if isinstance(date_formats, six.string_types):
            warn(self.DATE_FORMATS_ERROR_MESSAGE, FutureWarning)
            date_formats = [date_formats]
        elif not (date_formats is None or isinstance(date_formats, (list, tuple, collections.Set))):
            raise TypeError(self.DATE_FORMATS_ERROR_MESSAGE)

        self.language = language
        self.date_string = date_string
        self.date_formats = date_formats
        self._translated_date = None
        self._translated_date_with_formatting = None

    @classmethod
    def parse(cls, language, date_string, date_formats=None, settings=None):
        instance = cls(language, date_string, date_formats, settings)
        return instance._parse()

    def _parse(self):
        for parser in (
            self._try_timestamp,
            self._try_freshness_parser,
            self._try_given_formats,
            self._try_parser,
            self._try_hardcoded_formats,
        ):
            date_obj = parser()
            if self._is_valid_date_obj(date_obj):
                return date_obj
        else:
            return None

    def _try_timestamp(self):
        return {
            'date_obj': get_date_from_timestamp(self.date_string, self._settings),
            'period': 'day',
        }

    def _try_freshness_parser(self):
        return freshness_date_parser.get_date_data(self._get_translated_date(), self._settings)

    def _try_parser(self):
        _order = self._settings.DATE_ORDER
        try:
            if self._settings.PREFER_LANGUAGE_DATE_ORDER:
                self._settings.DATE_ORDER = self.language.info.get('dateorder', _order)
            date_obj, period = date_parser.parse(
                self._get_translated_date(), settings=self._settings)
            self._settings.DATE_ORDER = _order
            return {
                'date_obj': date_obj,
                'period': period,
            }
        except ValueError:
            self._settings.DATE_ORDER = _order
            return None

    def _try_given_formats(self):
        if not self.date_formats:
            return

        return parse_with_formats(
            self._get_translated_date_with_formatting(),
            self.date_formats, settings=self._settings
        )

    def _try_hardcoded_formats(self):
        hardcoded_date_formats = [
            '%B %d, %Y, %I:%M:%S %p',
            '%b %d, %Y at %I:%M %p',
            '%d %B %Y %H:%M:%S',
            '%A, %B %d, %Y',
            '%Y-%m-%dT%H:%M:%S.%fZ'
        ]
        try:
            return parse_with_formats(
                self._get_translated_date_with_formatting(),
                hardcoded_date_formats,
                settings=self._settings
            )
        except TypeError:
            return None

    def _get_translated_date(self):
        if self._translated_date is None:
            self._translated_date = self.language.translate(
                self.date_string, keep_formatting=False, settings=self._settings)
        return self._translated_date

    def _get_translated_date_with_formatting(self):
        if self._translated_date_with_formatting is None:
            self._translated_date_with_formatting = self.language.translate(
                self.date_string, keep_formatting=True, settings=self._settings)
        return self._translated_date_with_formatting

    def _is_valid_date_obj(self, date_obj):
        if not isinstance(date_obj, dict):
            return False
        if len(date_obj) != 2:
            return False
        if 'date_obj' not in date_obj or 'period' not in date_obj:
            return False
        if not date_obj['date_obj']:
            return False
        if date_obj['period'] not in ('day', 'week', 'month', 'year'):
            return False

        return True


class DateDataParser(object):
    """
    Class which handles language detection, translation and subsequent generic parsing of
    string representing date and/or time.

    :param languages:
            A list of two letters language codes, e.g. ['en', 'es'].
            If languages are given, it will not attempt to detect the language.
    :type languages: list

    :param allow_redetect_language:
            Enables/disables language re-detection.
    :type allow_redetect_language: bool

    :param settings:
           Configure customized behavior using settings defined in :mod:`dateparser.conf.Settings`.
    :type settings: dict

    :return: A parser instance

    :raises:
            ValueError - Unknown Language, TypeError - Languages argument must be a list
    """
    language_loader = None

    @apply_settings
    def __init__(self, languages=None, allow_redetect_language=False, settings=None):
        self._settings = settings
        available_language_map = self._get_language_loader().get_language_map()

        if isinstance(languages, (list, tuple, collections.Set)):

            if all([language in available_language_map for language in languages]):
                languages = [available_language_map[language] for language in languages]
            else:
                unsupported_languages = set(languages) - set(available_language_map.keys())
                raise ValueError(
                    "Unknown language(s): %s" % ', '.join(map(repr, unsupported_languages)))
        elif languages is not None:
            raise TypeError("languages argument must be a list (%r given)" % type(languages))

        if allow_redetect_language:
            self.language_detector = AutoDetectLanguage(
                languages if languages else list(available_language_map.values()),
                allow_redetection=True)
        elif languages:
            self.language_detector = ExactLanguages(languages=languages)
        else:
            self.language_detector = AutoDetectLanguage(
                list(available_language_map.values()), allow_redetection=False)

    def get_date_data(self, date_string, date_formats=None):
        """
        Parse string representing date and/or time in recognizable localized formats.
        Supports parsing multiple languages and timezones.

        :param date_string:
            A string representing date and/or time in a recognizably valid format.
        :type date_string: str|unicode
        :param date_formats:
            A list of format strings using directives as given
            `here <https://docs.python.org/2/library/datetime.html#strftime-and-strptime-behavior>`_.
            The parser applies formats one by one, taking into account the detected languages.
        :type date_formats: list

        :return: a dict mapping keys to :mod:`datetime.datetime` object and *period*. For example:
            {'date_obj': datetime.datetime(2015, 6, 1, 0, 0), 'period': u'day'}

        :raises: ValueError - Unknown Language

        .. note:: *Period* values can be a 'day' (default), 'week', 'month', 'year'.

        *Period* represents the granularity of date parsed from the given string.

        In the example below, since no day information is present, the day is assumed to be current
        day ``16`` from *current date* (which is June 16, 2015, at the moment of writing this).
        Hence, the level of precision is ``month``:

            >>> DateDataParser().get_date_data(u'March 2015')
            {'date_obj': datetime.datetime(2015, 3, 16, 0, 0), 'period': u'month'}

        Similarly, for date strings with no day and month information present, level of precision
        is ``year`` and day ``16`` and month ``6`` are from *current_date*.

            >>> DateDataParser().get_date_data(u'2014')
            {'date_obj': datetime.datetime(2014, 6, 16, 0, 0), 'period': u'year'}

        Dates with time zone indications or UTC offsets are returned in UTC time unless
        specified using `Settings`_.

            >>> DateDataParser().get_date_data(u'23 March 2000, 1:21 PM CET')
            {'date_obj': datetime.datetime(2000, 3, 23, 14, 21), 'period': 'day'}

        """
        if not(isinstance(date_string, six.text_type) or isinstance(date_string, six.string_types)):
            raise TypeError('Input type must be str or unicode')

        res = parse_with_formats(date_string, date_formats or [], self._settings)
        if res['date_obj']:
            return res

        if self._settings.NORMALIZE:
            date_string = normalize_unicode(date_string)

        date_string = sanitize_date(date_string)

        for language in self.language_detector.iterate_applicable_languages(
                date_string, modify=True, settings=self._settings):
            parsed_date = _DateLanguageParser.parse(
                language, date_string, date_formats, settings=self._settings)
            if parsed_date:
                parsed_date['language'] = language.shortname
                return parsed_date
        else:
            return {'date_obj': None, 'period': 'day', 'language': None}

    def get_date_tuple(self, *args, **kwargs):
        date_tuple = collections.namedtuple('DateData', 'date_obj period language')
        date_data = self.get_date_data(*args, **kwargs)
        return date_tuple(**date_data)

    @classmethod
    def _get_language_loader(cls):
        if not cls.language_loader:
            cls.language_loader = LanguageDataLoader()
        return cls.language_loader

[((31, 10, 31, 47), 'regex.compile', 're.compile', (), '', True, 'import regex as re\n'), ((32, 12, 32, 30), 'regex.compile', 're.compile', ({(32, 23, 32, 29): '"""\\\\s+"""'}, {}), "('\\\\s+')", True, 'import regex as re\n'), ((33, 17, 33, 47), 'regex.compile', 're.compile', ({(33, 28, 33, 46): '"""^\\\\s+(\\\\S.*?)\\\\s+$"""'}, {}), "('^\\\\s+(\\\\S.*?)\\\\s+$')", True, 'import regex as re\n'), ((35, 19, 35, 104), 'regex.compile', 're.compile', (), '', True, 'import regex as re\n'), ((36, 22, 36, 72), 'regex.compile', 're.compile', (), '', True, 'import regex as re\n'), ((37, 19, 37, 79), 'regex.compile', 're.compile', (), '', True, 'import regex as re\n'), ((38, 17, 38, 46), 'regex.compile', 're.compile', ({(38, 28, 38, 45): '"""^.*?on:\\\\s+(.*)"""'}, {}), "('^.*?on:\\\\s+(.*)')", True, 'import regex as re\n'), ((41, 22, 41, 54), 'regex.compile', 're.compile', ({(41, 33, 41, 53): '"""^\\\\d{10}(?![^\\\\d.])"""'}, {}), "('^\\\\d{10}(?![^\\\\d.])')", True, 'import regex as re\n'), ((77, 11, 77, 45), 'dateutil.relativedelta.relativedelta', 'relativedelta', ({}, {}), "(**{(period + 's'): 1})", False, 'from dateutil.relativedelta import relativedelta\n'), ((58, 11, 58, 34), 'dateutil.relativedelta.relativedelta', 'relativedelta', ({}, {}), '(**kwargs)', False, 'from dateutil.relativedelta import relativedelta\n'), ((58, 50, 58, 71), 'dateutil.relativedelta.relativedelta', 'relativedelta', (), '', False, 'from dateutil.relativedelta import relativedelta\n'), ((117, 19, 117, 67), 'dateparser.utils.apply_timezone_from_settings', 'apply_timezone_from_settings', ({(117, 48, 117, 56): 'date_obj', (117, 58, 117, 66): 'settings'}, {}), '(date_obj, settings)', False, 'from dateparser.utils import normalize_unicode, apply_timezone_from_settings\n'), ((122, 11, 122, 43), 'calendar.monthrange', 'calendar.monthrange', ({(122, 31, 122, 35): 'year', (122, 37, 122, 42): 'month'}, {}), '(year, month)', False, 'import calendar\n'), ((387, 21, 387, 83), 'collections.namedtuple', 'collections.namedtuple', ({(387, 44, 387, 54): '"""DateData"""', (387, 56, 387, 82): '"""date_obj period language"""'}, {}), "('DateData', 'date_obj period language')", False, 'import collections\n'), ((134, 23, 134, 66), 'datetime.datetime.strptime', 'datetime.strptime', ({(134, 41, 134, 52): 'date_string', (134, 54, 134, 65): 'date_format'}, {}), '(date_string, date_format)', False, 'from datetime import datetime, timedelta\n'), ((149, 23, 149, 71), 'dateparser.utils.apply_timezone_from_settings', 'apply_timezone_from_settings', ({(149, 52, 149, 60): 'date_obj', (149, 62, 149, 70): 'settings'}, {}), '(date_obj, settings)', False, 'from dateparser.utils import normalize_unicode, apply_timezone_from_settings\n'), ((162, 12, 162, 64), 'warnings.warn', 'warn', ({(162, 17, 162, 48): 'self.DATE_FORMATS_ERROR_MESSAGE', (162, 50, 162, 63): 'FutureWarning'}, {}), '(self.DATE_FORMATS_ERROR_MESSAGE, FutureWarning)', False, 'from warnings import warn\n'), ((372, 26, 372, 56), 'dateparser.utils.normalize_unicode', 'normalize_unicode', ({(372, 44, 372, 55): 'date_string'}, {}), '(date_string)', False, 'from dateparser.utils import normalize_unicode, apply_timezone_from_settings\n'), ((394, 34, 394, 54), 'dateparser.languages.loader.LanguageDataLoader', 'LanguageDataLoader', ({}, {}), '()', False, 'from dateparser.languages.loader import LanguageDataLoader\n'), ((146, 24, 146, 40), 'datetime.datetime.today', 'datetime.today', ({}, {}), '()', False, 'from datetime import datetime, timedelta\n'), ((316, 37, 316, 72), 'dateparser.languages.detection.ExactLanguages', 'ExactLanguages', (), '', False, 'from dateparser.languages.detection import AutoDetectLanguage, ExactLanguages\n')]