/* 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");
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) cal.calGetString("calendar-event-dialog", name, args);

    // 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 don't allow for any BYxxx-rules.
        if (rule &&
            !checkRecurrenceRule(rule, ['BYSECOND',
                                        'BYMINUTE',
                                        //'BYDAY',
                                        'BYHOUR',
                                        //'BYMONTHDAY',
                                        'BYYEARDAY',
                                        'BYWEEKNO',
                                        //'BYMONTH',
                                        'BYSETPOS'])) {
            function day_of_week(day) {
                return Math.abs(day) % 8;
            }
            function day_position(day) {
                let dow = day_of_week(day);
                return (Math.abs(day) - dow) / 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);
            }

            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 weekdaysString_every = "";
                    let weekdaysString_position = "";
                    let byday = rule.getComponent("BYDAY", {});
                    let firstDay = byday[0];
                    // build two strings for weekdays with and without
                    // "position" prefix, then join these strings
                    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 if (component.length == 31 &&
                                    component.every(function (element, index, array) {
                                                        for (let i = 0; i < array.length; i++) {
                                                            if ((index + 1) == array[i])
                                                                return true;
                                                        }
                                                        return false;
                                                    })) {
                            // i.e. every day every N months
                            ruleString = getRString("monthlyEveryDayOfNth");
                            ruleString = PluralForm.get(rule.interval, ruleString)
                                                   .replace("#2", 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 ordinalSymbols = getRString("monthlyDaysOfNth_ordSymbol").split(",");
                            let lastDay = false;
                            for (let i = 0; i < component.length; i++) {
                                if (component[i] == -1) {
                                    lastDay = true;
                                    continue;
                                }
                                let dayOrdinalSymbol = ordinalSymbols[component[i] -1] || ordinalSymbols[0];
                                day_string += component[i] + dayOrdinalSymbol + ", ";
                            }
                            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 bymonth = rule.getComponent("BYMONTH", {});
                if (checkRecurrenceRule(rule, ['BYMONTH']) &&
                    checkRecurrenceRule(rule, ['BYMONTHDAY'])) {
                    let bymonthday = rule.getComponent("BYMONTHDAY", {});

                    if (bymonth.length == 1 && bymonthday.length == 1) {
                        let monthNameString = getRString("repeatDetailsMonth" + bymonth[0]);

                        let yearlyString = getRString("yearlyNthOn",
                                                      [monthNameString, bymonthday[0]]);
                        ruleString = PluralForm.get(rule.interval, yearlyString)
                                               .replace("#3", rule.interval);
                    }
                } else if (checkRecurrenceRule(rule, ['BYMONTH']) &&
                           checkRecurrenceRule(rule, ['BYDAY'])) {
                    let byday = rule.getComponent("BYDAY", {});

                    if (bymonth.length == 1 && byday.length == 1) {
                        let dayString = "repeatDetailsDay" + day_of_week(byday[0]);
                        let month = getRString("repeatDetailsMonth" + bymonth[0]);
                        if (day_position(byday[0]) == 0) {
                            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 {
                            let yearlyString = "yearlyNthOnNthOf";
                            let ordinalString = "repeatOrdinal" + day_position(byday[0])
                            yearlyString = nounClass(dayString, yearlyString);
                            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 null;
                    }
                } else {
                    let monthNameString = getRString("repeatDetailsMonth" + (startDate.month + 1));

                    let yearlyString = getRString("yearlyNthOn",
                                                  [monthNameString, startDate.day]);
                    ruleString = PluralForm.get(rule.interval, yearlyString)
                                           .replace("#3", rule.interval);
                }
            }

            let kDefaultTimezone = cal.calendarDefaultTimezone();
            let dateFormatter = cal.getDateFormatter();

            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 each (var r in 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 each (var comp in aArray) {
        var ruleComp = aRule.getComponent(comp, {});
        if (ruleComp && ruleComp.length > 0) {
            return true;
        }
    }
    return false;
}
