from dateutil import rrule
from plone.base.i18nl10n import _interp_regex
from plone.base.i18nl10n import datetime_formatvariables
from plone.base.i18nl10n import monthname_msgid
from plone.base.i18nl10n import monthname_msgid_abbr
from plone.base.i18nl10n import name_formatvariables
from plone.base.i18nl10n import weekdayname_msgid
from plone.base.i18nl10n import weekdayname_msgid_abbr
from Products.Five import BrowserView
from zope.i18n import interpolate
from zope.i18n import translate

import datetime
import json
import re


BATCH_DELTA = 3  # How many batches to show before + after current batch
BATCH_SIZE = 10  # How many items per batch

# Translations from dateinput formatting to Plone translation strings
# See http://flowplayer.org/tools/dateinput/index.html
DATEFORMAT_XLATE = [
    (re.compile(pattern), replacement)
    for (pattern, replacement) in (
        ("dddd", "${A}"),
        ("ddd", "${a}"),
        ("dd", "${d}"),
        ("!%d", "${e}"),  # Will include a leading space for 1-9
        ("mmmm", "${B}"),
        ("mmm", "${b}"),
        ("mm", "${m}"),
        ("!%m", "${m}"),  # Will include leading zero
        ("yyyy", "${Y}"),
        ("yy", "${y}"),
    )
]


def dateformat_xlate(dateformat):
    for regexp, replacement in DATEFORMAT_XLATE:
        dateformat = regexp.sub(replacement, dateformat)
    return dateformat


class RecurrenceView(BrowserView):
    def __call__(self):
        req = self.request
        req.response.setHeader("Content-type", "application/json")
        json_string = self.json_string
        return json.dumps(json_string)

    @property
    def json_string(self):
        occurrences = []
        data = self.request.form
        # Check for required parameters:
        for x in ("year", "month", "day", "rrule", "format"):
            if x not in data:
                self.request.response.setStatus(400)  # bad request
                return {}

        # Translate from the js dateformat style to the i18n style
        date_format = dateformat_xlate(data["format"])
        start_date = datetime.datetime(
            int(data["year"]), int(data["month"]), int(data["day"])
        )
        rule = rrule.rrulestr(data["rrule"], dtstart=start_date)
        iterator = iter(rule)

        if "batch_size" in data:
            batch_size = int(data["batch_size"])
        else:
            batch_size = BATCH_SIZE

        if "start" in data:
            start = int(data["start"])
        else:
            start = 0

        cur_batch = start // batch_size
        start = cur_batch * batch_size  # Avoid stupid start-values

        if hasattr(rule, "_exdate"):
            exdates = sorted(rule._exdate)
        else:
            exdates = []

        # Loop through the start first dates, to skip them:
        i = 0
        occurrences = []
        while True:
            try:
                # Get a date
                date = next(iterator)
            except StopIteration:
                # No more dates
                break
            while exdates and date > exdates[0]:
                # There are exdates that appear before this date:
                if i < start:
                    # Skip them
                    exdates.pop(0)
                    i += 1
                else:
                    # include them
                    exdate = exdates.pop(0)
                    occurrences.append(
                        {
                            "date": exdate.strftime("%Y%m%dT%H%M%S"),
                            "formattedDate": self.date_format(exdate, date_format),
                            "type": "exdate",
                        }
                    )
                    i += 1

            if i >= batch_size + start:
                break  # We are done!

            i += 1
            if i <= start:
                # We are still iterating up to the first event, so skip this:
                continue

            # Add it to the results
            if date in getattr(rule, "_rdate", []):
                occurrence_type = "rdate"
            elif date == start_date:
                occurrence_type = "start"
            else:
                occurrence_type = "rrule"
            occurrences.append(
                {
                    "date": date.strftime("%Y%m%dT%H%M%S"),
                    "formattedDate": self.date_format(date, date_format),
                    "type": occurrence_type,
                }
            )

        while exdates:
            # There are exdates that are after the end of the recurrence.
            # Excluding the last dates make no sense, as you can change the
            # range instead, but we need to support it anyway.
            exdate = exdates.pop(0)
            occurrences.append(
                {
                    "date": exdate.strftime("%Y%m%dT%H%M%S"),
                    "formattedDate": exdate.strftime(date_format),
                    "type": "exdate",
                }
            )

        # Calculate no of occurrences, but only to a max of three times
        # the batch size. This will support infinite recurrence in a
        # useable way, as there will always be more batches.
        first_batch = max(0, cur_batch - BATCH_DELTA)
        last_batch = max(BATCH_DELTA * 2, cur_batch + BATCH_DELTA)
        maxcount = (batch_size * last_batch) - start

        num_occurrences = 0
        while True:
            try:
                next(iterator)
                num_occurrences += 1
            except StopIteration:
                break
            if num_occurrences >= maxcount:
                break

        # Total number of occurrences:
        num_occurrences += batch_size + start

        max_batch = (num_occurrences - 1) // batch_size
        if last_batch > max_batch:
            last_batch = max_batch
            first_batch = max(0, max_batch - (BATCH_DELTA * 2))

        batches = [
            ((x * batch_size) + 1, (x + 1) * batch_size)
            for x in range(first_batch, last_batch + 1)
        ]
        # cur_batch must be set relative to first_batch as the batches array
        # is cut down, but the cur_batch variable is not. With this we prevent
        # a list index out of range error
        cur_batch = cur_batch - first_batch
        # correct number of occurrences in current batch
        (cur_batch_start, cur_batch_end) = batches[cur_batch]
        if cur_batch_end > num_occurrences:
            batches[cur_batch] = (cur_batch_start, num_occurrences)

        batch_data = {
            "start": start,
            "end": num_occurrences,
            "batch_size": batch_size,
            "batches": batches,
            "currentBatch": cur_batch,
        }

        result = {"occurrences": occurrences, "batch": batch_data}
        return result

    def date_format(self, time, formatstring):
        # This is a simplified version of
        # Products.CMFPlone.i18nl10n.ulocalized_time
        # that can take any format string.

        # ${a}        Locale's abbreviated weekday name.
        # ${A}        Locale's full weekday name.
        # ${b}        Locale's abbreviated month name.
        # ${B}        Locale's full month name.
        # ${d}        Day of the month as a decimal number [01,31].
        # ${H}        Hour (24-hour clock) as a decimal number [00,23].
        # ${I}        Hour (12-hour clock) as a decimal number [01,12].
        # ${m}        Month as a decimal number [01,12].
        # ${M}        Minute as a decimal number [00,59].
        # ${p}        Locale's equivalent of either AM or PM.
        # ${S}        Second as a decimal number [00,61].
        # ${y}        Year without century as a decimal number [00,99].
        # ${Y}        Year with century as a decimal number.
        # ${Z}        Time zone name (no characters if no time zone exists).

        # get the format elements used in the formatstring
        mapping = {}
        formatelements = _interp_regex.findall(formatstring)
        # reformat the ${foo} to foo
        formatelements = [el[2:-1] for el in formatelements]

        # add used elements to mapping
        elements = [e for e in formatelements if e in datetime_formatvariables]

        # add weekday name, abbr. weekday name, month name, abbr month name
        week_included = True
        month_included = True

        name_elements = [e for e in formatelements if e in name_formatvariables]
        if not ("a" in name_elements or "A" in name_elements):
            week_included = False
        if not ("b" in name_elements or "B" in name_elements):
            month_included = False

        for key in elements:
            mapping[key] = time.strftime("%" + key)

        if week_included:
            weekday = int(time.strftime("%w"))  # weekday, sunday = 0
            if "a" in name_elements:
                mapping["a"] = weekdayname_msgid_abbr(weekday)
            if "A" in name_elements:
                mapping["A"] = weekdayname_msgid(weekday)
        if month_included:
            monthday = int(time.strftime("%m"))  # month, january = 1
            if "b" in name_elements:
                mapping["b"] = monthname_msgid_abbr(monthday)
            if "B" in name_elements:
                mapping["B"] = monthname_msgid(monthday)

        # translate translateable elements
        for key in name_elements:
            mapping[key] = translate(
                mapping[key], "plonelocales", context=self.request, default=mapping[key]
            )

        # Apply the data to the format string:
        return interpolate(formatstring, mapping)
