/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

Components.utils.import("resource://gre/modules/PluralForm.jsm");
Components.utils.import("resource://calendar/modules/calUtils.jsm");
this.EXPORTED_SYMBOLS = ["recurrenceRule2String", "splitRecurrenceRules", "checkRecurrenceRule"];

/**
 * This function takes the recurrence info passed as argument and creates a
 * literal string representing the repeat pattern in natural language.
 *
 * @param recurrenceInfo    An item's recurrence info to parse.
 * @param startDate         The start date to base rules on.
 * @param endDate           The end date to base rules on.
 * @param allDay            If true, the pattern should assume an allday item.
 * @return                  A human readable string describing the recurrence.
 */
function recurrenceRule2String(recurrenceInfo, startDate, endDate, allDay) {
    function getRString(name, args) {
        return cal.calGetString("calendar-event-dialog", name, args);
    }
    function day_of_week(day) {
        return Math.abs(day) % 8;
    }
    function day_position(day) {
        return (Math.abs(day) - day_of_week(day)) / 8 * (day < 0 ? -1 : 1);
    }
    function nounClass(aDayString, aRuleString) {
        // Select noun class (grammatical gender) for rule string
        let nounClass = getRString(aDayString + "Nounclass");
        return aRuleString + nounClass.substr(0, 1).toUpperCase() +
               nounClass.substr(1);
    }
    function pluralWeekday(aDayString) {
        let plural = getRString("pluralForWeekdays") == "true";
        return (plural ? aDayString + "Plural" : aDayString);
    }
    function everyWeekDay(aByDay) {
        // Checks if aByDay contains only values from 1 to 7 with any order.
        let mask = aByDay.reduce((v, c) => v | (1 << c), 1);
        return aByDay.length == 7 && mask == Math.pow(2, 8) - 1;
    }


    // Retrieve a valid recurrence rule from the currently
    // set recurrence info. Bail out if there's more
    // than a single rule or something other than a rule.
    recurrenceInfo = recurrenceInfo.clone();
    let rrules = splitRecurrenceRules(recurrenceInfo);
    if (rrules[0].length == 1) {
        let rule = cal.wrapInstance(rrules[0][0], Components.interfaces.calIRecurrenceRule);
        // Currently we allow only for BYDAY, BYMONTHDAY, BYMONTH rules.
        if (rule &&
            !checkRecurrenceRule(rule, ['BYSECOND',
                                        'BYMINUTE',
                                        //'BYDAY',
                                        'BYHOUR',
                                        //'BYMONTHDAY',
                                        'BYYEARDAY',
                                        'BYWEEKNO',
                                        //'BYMONTH',
                                        'BYSETPOS'])) {
            let dateFormatter = cal.getDateFormatter();
            let ruleString;
            if (rule.type == 'DAILY') {
                if (checkRecurrenceRule(rule, ['BYDAY'])) {
                    let days = rule.getComponent("BYDAY", {});
                    let weekdays = [2, 3, 4, 5, 6];
                    if (weekdays.length == days.length) {
                        let i;
                        for (i = 0; i < weekdays.length; i++) {
                            if (weekdays[i] != days[i]) {
                                break;
                            }
                        }
                        if (i == weekdays.length) {
                            ruleString = getRString("repeatDetailsRuleDaily4");
                        }
                    } else {
                        return null;
                    }
                } else {
                    let dailyString = getRString("dailyEveryNth");
                    ruleString = PluralForm.get(rule.interval, dailyString)
                                           .replace("#1", rule.interval);
                }
            } else if (rule.type == 'WEEKLY') {
                // weekly recurrence, currently we
                // support a single 'BYDAY'-rule only.
                if (checkRecurrenceRule(rule, ['BYDAY'])) {
                    // create a string like 'Monday, Tuesday and Wednesday'
                    let days = rule.getComponent("BYDAY", {});
                    let weekdays = "";
                    // select noun class (grammatical gender) according to the
                    // first day of the list
                    let weeklyString = nounClass("repeatDetailsDay" + days[0], "weeklyNthOn");
                    for (let i = 0; i < days.length; i++) {
                        if (rule.interval == 1) {
                            weekdays += getRString(pluralWeekday("repeatDetailsDay" + days[i]));
                        } else {
                            weekdays += getRString("repeatDetailsDay" + days[i]);
                        }
                        if (days.length > 1 && i == (days.length - 2)) {
                            weekdays += ' ' + getRString("repeatDetailsAnd") + ' ';
                        } else if (i < days.length - 1) {
                            weekdays += ', ';
                        }
                    }

                    weeklyString = getRString(weeklyString, [weekdays]);
                    ruleString= PluralForm.get(rule.interval, weeklyString)
                                          .replace("#2", rule.interval);

                } else {
                    let weeklyString = getRString("weeklyEveryNth");
                    ruleString = PluralForm.get(rule.interval, weeklyString)
                                           .replace("#1", rule.interval);
                }
            } else if (rule.type == 'MONTHLY') {
                if (checkRecurrenceRule(rule, ['BYDAY'])) {
                    let byday = rule.getComponent("BYDAY", {});
                    if (everyWeekDay(byday)) {
                        // Rule every day of the month.
                        ruleString = getRString("monthlyEveryDayOfNth");
                        ruleString = PluralForm.get(rule.interval, ruleString)
                                               .replace("#2", rule.interval);
                    } else {
                        // For rules with generic number of weekdays with and
                        // without "position" prefix we build two separate
                        // strings depending on the position and then join them.
                        // Notice: we build the description string but currently
                        // the UI can manage only rules with only one weekday.
                        let weekdaysString_every = "";
                        let weekdaysString_position = "";
                        let firstDay = byday[0];
                        for (let i = 0; i < byday.length; i++) {
                            if (day_position(byday[i]) == 0) {
                                if (!weekdaysString_every) {
                                    firstDay = byday[i];
                                }
                                weekdaysString_every += getRString(pluralWeekday("repeatDetailsDay" + byday[i])) + ", ";
                            } else {
                                if (day_position(byday[i]) < -1 || day_position(byday[i]) > 5) {
                                    // We support only weekdays with -1 as negative
                                    // position ('THE LAST ...').
                                    return null;
                                }
                                if (byday.some(function(element) {
                                                   return (day_position(element) == 0 &&
                                                           day_of_week(byday[i]) == day_of_week(element));
                                               })) {
                                    // Prevent to build strings such as for example:
                                    // "every Monday and the second Monday...".
                                    continue;
                                }
                                let ordinalString = "repeatOrdinal" + day_position(byday[i]);
                                let dayString = "repeatDetailsDay" + day_of_week(byday[i]);
                                ordinalString = nounClass(dayString, ordinalString);
                                ordinalString = getRString(ordinalString);
                                dayString = getRString(dayString);
                                let stringOrdinalWeekday = getRString("ordinalWeekdayOrder",
                                                                      [ordinalString, dayString]);
                                weekdaysString_position += stringOrdinalWeekday + ", ";
                            }
                        }
                        let weekdaysString = weekdaysString_every + weekdaysString_position;
                        weekdaysString = weekdaysString.slice(0,-2)
                                                       .replace(/,(?= [^,]*$)/,
                                                                ' ' + getRString("repeatDetailsAnd"));

                        let monthlyString = weekdaysString_every ? "monthlyEveryOfEvery" : "monthlyRuleNthOfEvery";
                        monthlyString = nounClass("repeatDetailsDay" + day_of_week(firstDay), monthlyString);
                        monthlyString = getRString(monthlyString, [weekdaysString]);
                        ruleString = PluralForm.get(rule.interval, monthlyString)
                                               .replace("#2", rule.interval);
                    }
                } else if (checkRecurrenceRule(rule, ['BYMONTHDAY'])) {
                    let component = rule.getComponent("BYMONTHDAY", {});

                    // First, find out if the 'BYMONTHDAY' component contains
                    // any elements with a negative value lesser than -1 ("the
                    // last day"). If so we currently don't support any rule
                    if (component.some(function(element, index, array) {
                                           return element < -1;
                                       })) {
                        // we don't support any other combination for now...
                        return getRString("ruleTooComplex");
                    } else {
                        if (component.length == 1 && component[0] == -1) {
                            // i.e. one day, the last day of the month
                            let monthlyString = getRString("monthlyLastDayOfNth");
                            ruleString = PluralForm.get(rule.interval, monthlyString)
                                                   .replace("#1", rule.interval);
                        } else {
                            // i.e. one or more monthdays every N months.

                            // Build a string with a list of days separated with commas.
                            let day_string = "";
                            let lastDay = false;
                            for (let i = 0; i < component.length; i++) {
                                if (component[i] == -1) {
                                    lastDay = true;
                                    continue;
                                }
                                day_string += dateFormatter.formatDayWithOrdinal(component[i]) + ", ";
                            }
                            if (lastDay) {
                                day_string += getRString("monthlyLastDay") + ", ";
                            }
                            day_string = day_string.slice(0,-2)
                                                   .replace(/,(?= [^,]*$)/,
                                                            ' ' + getRString("repeatDetailsAnd"));

                            // Add the word "day" in plural form to the list of days then
                            // compose the final string with the interval of months
                            let monthlyDayString = getRString("monthlyDaysOfNth_day", [day_string]);
                            monthlyDayString = PluralForm.get(component.length, monthlyDayString);
                            let monthlyString = getRString("monthlyDaysOfNth", [monthlyDayString]);
                            ruleString = PluralForm.get(rule.interval, monthlyString)
                                                   .replace("#2", rule.interval);
                        }
                    }
                } else {
                    let monthlyString = getRString("monthlyDaysOfNth", [startDate.day]);
                    ruleString = PluralForm.get(rule.interval, monthlyString)
                                           .replace("#2", rule.interval);
                }
            } else if (rule.type == 'YEARLY') {
                let bymonthday = null;
                let bymonth = null;
                if (checkRecurrenceRule(rule, ['BYMONTHDAY'])) {
                    bymonthday = rule.getComponent("BYMONTHDAY", {});
                }
                if (checkRecurrenceRule(rule, ['BYMONTH'])) {
                    bymonth = rule.getComponent("BYMONTH", {});
                }
                if (bymonth && bymonth.length > 1 ||
                    bymonthday && (bymonthday.length > 1 || bymonthday[0] < -1)) {
                    // Don't build a string for a recurrence rule that the UI
                    // currently can't show completely (with more than one month
                    // or than one monthday, or bymonthdays lesser than -1).
                    return getRString("ruleTooComplex");
                }

                if (checkRecurrenceRule(rule, ['BYMONTHDAY']) &&
                    (checkRecurrenceRule(rule, ['BYMONTH']) || !checkRecurrenceRule(rule, ['BYDAY']))) {
                    // RRULE:FREQ=YEARLY;BYMONTH=x;BYMONTHDAY=y.
                    // RRULE:FREQ=YEARLY;BYMONTHDAY=x (takes the month from the start date).
                    let monthNumber = bymonth ? bymonth[0] : (startDate.month + 1);
                    let month = getRString("repeatDetailsMonth" + monthNumber);
                    let monthDay = bymonthday[0] == -1 ? getRString("monthlyLastDay")
                                                       : dateFormatter.formatDayWithOrdinal(bymonthday[0]);
                    let yearlyString = getRString("yearlyNthOn", [month, monthDay]);
                    ruleString = PluralForm.get(rule.interval, yearlyString)
                                           .replace("#3", rule.interval);
                } else if (checkRecurrenceRule(rule, ['BYMONTH']) &&
                           checkRecurrenceRule(rule, ['BYDAY'])) {
                    // RRULE:FREQ=YEARLY;BYMONTH=x;BYDAY=y1,y2,....
                    let byday = rule.getComponent("BYDAY", {});
                    let month = getRString("repeatDetailsMonth" + bymonth[0]);
                    if (everyWeekDay(byday)) {
                        // Every day of the month.
                        let yearlyString = "yearlyEveryDayOf";
                        yearlyString = getRString(yearlyString, [month]);
                        ruleString = PluralForm.get(rule.interval, yearlyString)
                                               .replace("#2", rule.interval);
                    } else if (byday.length == 1) {
                        let dayString = "repeatDetailsDay" + day_of_week(byday[0]);
                        if (day_position(byday[0]) == 0) {
                            // Every any weekday.
                            let yearlyString = "yearlyOnEveryNthOfNth";
                            yearlyString = nounClass(dayString, yearlyString);
                            let day = getRString(pluralWeekday(dayString));
                            yearlyString = getRString(yearlyString, [day, month]);
                            ruleString = PluralForm.get(rule.interval, yearlyString)
                                                   .replace("#3", rule.interval);
                        } else if (day_position(byday[0]) >= -1 ||
                                   day_position(byday[0]) <= 5) {
                            // The first|the second|...|the last  Monday, Tuesday, ..., day.
                            let yearlyString = "yearlyNthOnNthOf";
                            yearlyString = nounClass(dayString, yearlyString);
                            let ordinalString = "repeatOrdinal" + day_position(byday[0])
                            ordinalString = nounClass(dayString, ordinalString);
                            let ordinal = getRString(ordinalString);
                            let day = getRString(dayString);
                            yearlyString = getRString(yearlyString, [ordinal, day, month]);
                            ruleString = PluralForm.get(rule.interval, yearlyString)
                                                   .replace("#4", rule.interval);
                        } else {
                            return getRString("ruleTooComplex");
                        }
                    } else {
                        // Currently we don't support yearly rules with
                        // more than one BYDAY element or exactly 7 elements
                        // with all the weekdays (the "every day" case).
                        return getRString("ruleTooComplex");
                    }
                } else if (checkRecurrenceRule(rule, ['BYMONTH'])) {
                    // RRULE:FREQ=YEARLY;BYMONTH=x (takes the day from the start date).
                    let month = getRString("repeatDetailsMonth" + bymonth[0]);
                    let yearlyString = getRString("yearlyNthOn", [month, startDate.day]);
                    ruleString = PluralForm.get(rule.interval, yearlyString)
                                           .replace("#3", rule.interval);
                } else {
                    let month = getRString("repeatDetailsMonth" + (startDate.month + 1));
                    let yearlyString = getRString("yearlyNthOn", [month, startDate.day]);
                    ruleString = PluralForm.get(rule.interval, yearlyString)
                                           .replace("#3", rule.interval);
                }
            }

            let kDefaultTimezone = cal.calendarDefaultTimezone();

            let detailsString;
            if (!endDate || allDay) {
                if (rule.isFinite) {
                    if (rule.isByCount) {
                        let countString = getRString("repeatCountAllDay",
                            [ruleString,
                             dateFormatter.formatDateShort(startDate)]);
                        detailsString = PluralForm.get(rule.count, countString)
                                                  .replace("#3", rule.count);
                    } else {
                        let untilDate = rule.untilDate.getInTimezone(kDefaultTimezone);
                        detailsString = getRString("repeatDetailsUntilAllDay",
                            [ruleString,
                             dateFormatter.formatDateShort(startDate),
                             dateFormatter.formatDateShort(untilDate)]);
                    }
                } else {
                    detailsString = getRString("repeatDetailsInfiniteAllDay",
                                               [ruleString,
                                                dateFormatter.formatDateShort(startDate)]);
                }
            } else {
                if (rule.isFinite) {
                    if (rule.isByCount) {
                        let countString = getRString("repeatCount",
                            [ruleString,
                             dateFormatter.formatDateShort(startDate),
                             dateFormatter.formatTime(startDate),
                             dateFormatter.formatTime(endDate) ]);
                        detailsString = PluralForm.get(rule.count, countString)
                                                  .replace("#5", rule.count);
                    } else {
                        let untilDate = rule.untilDate.getInTimezone(kDefaultTimezone);
                        detailsString = getRString("repeatDetailsUntil",
                            [ruleString,
                             dateFormatter.formatDateShort(startDate),
                             dateFormatter.formatDateShort(untilDate),
                             dateFormatter.formatTime(startDate),
                             dateFormatter.formatTime(endDate)]);
                    }
                } else {
                    detailsString = getRString("repeatDetailsInfinite",
                        [ruleString,
                         dateFormatter.formatDateShort(startDate),
                         dateFormatter.formatTime(startDate),
                         dateFormatter.formatTime(endDate) ]);
                }
            }
            return detailsString;
        }
    }
    return null;
}

/**
 * Split rules into negative and positive rules.
 *
 * @param recurrenceInfo    An item's recurrence info to parse.
 * @return                  An array with two elements: an array of positive
 *                            rules and an array of negative rules.
 */
function splitRecurrenceRules(recurrenceInfo) {
    var ritems = recurrenceInfo.getRecurrenceItems({});
    var rules = [];
    var exceptions = [];
    for (var r of ritems) {
        if (r.isNegative) {
            exceptions.push(r);
        } else {
            rules.push(r);
        }
    }
    return [rules, exceptions];
}

/**
 * Check if a recurrence rule's component is valid.
 *
 * @see                     calIRecurrenceRule
 * @param aRule             The recurrence rule to check.
 * @param aArray            An array of component names to check.
 * @return                  Returns true if the rule is valid.
 */
function checkRecurrenceRule(aRule, aArray) {
    for (var comp of aArray) {
        var ruleComp = aRule.getComponent(comp, {});
        if (ruleComp && ruleComp.length > 0) {
            return true;
        }
    }
    return false;
}
