/* 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/. */

var { splitRecurrenceRules } = ChromeUtils.import(
  "resource:///modules/calendar/calRecurrenceUtils.jsm"
);
var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");
var { XPCOMUtils } = ChromeUtils.importESModule("resource://gre/modules/XPCOMUtils.sys.mjs");

XPCOMUtils.defineLazyModuleGetters(this, {
  CalRecurrenceInfo: "resource:///modules/CalRecurrenceInfo.jsm",
});

var gIsReadOnly = false;
var gStartTime = null;
var gEndTime = null;
var gUntilDate = null;

window.addEventListener("load", onLoad);

/**
 * Object wrapping the methods and properties of recurrencePreview binding.
 */
const RecurrencePreview = {
  /**
   * Initializes some properties and adds event listener to the #recurrencePreview node.
   */
  init() {
    this.node = document.getElementById("recurrencePreview");
    this.mRecurrenceInfo = null;
    this.mResizeHandler = null;
    this.mDateTime = null;
    document.getElementById("recurrencePrevious").addEventListener("click", () => {
      this.showPreviousMonth();
    });
    document.getElementById("recurrenceNext").addEventListener("click", () => {
      this.showNextMonth();
    });
    document.getElementById("recurrenceToday").addEventListener("click", () => {
      this.jumpToToday();
    });
    this.togglePreviousMonthButton();
  },
  /**
   * Setter for mDateTime property.
   *
   * @param {Date} val - The date value that is to be set.
   */
  set dateTime(val) {
    this.mDateTime = val.clone();
  },
  /**
   * Getter for mDateTime property.
   */
  get dateTime() {
    if (this.mDateTime == null) {
      this.mDateTime = cal.dtz.now();
    }
    return this.mDateTime;
  },
  /**
   * Updates content of #recurrencePreview node.
   */
  updateContent() {
    let date = cal.dtz.dateTimeToJsDate(this.dateTime);
    for (let minimonth of this.node.children) {
      minimonth.showMonth(date);
      date.setMonth(date.getMonth() + 1);
    }
  },
  /**
   * Updates preview of #recurrencePreview node.
   */
  updatePreview(recurrenceInfo) {
    let minimonth = this.node.querySelector("calendar-minimonth");
    this.node.style.minHeight = minimonth.getBoundingClientRect().height + "px";

    this.mRecurrenceInfo = recurrenceInfo;
    let start = this.dateTime.clone();
    start.day = 1;
    start.hour = 0;
    start.minute = 0;
    start.second = 0;
    let end = start.clone();
    end.month++;

    for (let minimonth of this.node.children) {
      // we now have one of the minimonth controls while 'start'
      // and 'end' are set to the interval this minimonth shows.
      minimonth.showMonth(cal.dtz.dateTimeToJsDate(start));
      if (recurrenceInfo) {
        // retrieve an array of dates that represents all occurrences
        // that fall into this time interval [start,end[.
        // note: the following loop assumes that this array contains
        // dates that are strictly monotonically increasing.
        // should getOccurrenceDates() not enforce this assumption we
        // need to fall back to some different algorithm.
        let dates = recurrenceInfo.getOccurrenceDates(start, end, 0);

        // now run through all days of this month and set the
        // 'busy' attribute with respect to the occurrence array.
        let index = 0;
        let occurrence = null;
        if (index < dates.length) {
          occurrence = dates[index++].getInTimezone(start.timezone);
        }
        let current = start.clone();
        while (current.compare(end) < 0) {
          let box = minimonth.getBoxForDate(current);
          if (box) {
            if (
              occurrence &&
              occurrence.day == current.day &&
              occurrence.month == current.month &&
              occurrence.year == current.year
            ) {
              box.setAttribute("busy", 1);
              if (index < dates.length) {
                occurrence = dates[index++].getInTimezone(start.timezone);
                // take into account that the very next occurrence
                // can happen at the same day as the previous one.
                if (
                  occurrence.day == current.day &&
                  occurrence.month == current.month &&
                  occurrence.year == current.year
                ) {
                  continue;
                }
              } else {
                occurrence = null;
              }
            } else {
              box.removeAttribute("busy");
            }
          }
          current.day++;
        }
      }
      start.month++;
      end.month++;
    }
  },
  /**
   * Shows the previous month in the recurrence preview.
   */
  showPreviousMonth() {
    let prevMinimonth = this.node.querySelector(`calendar-minimonth[active-month="true"]`);

    let activeDate = this.previousMonthDate(
      prevMinimonth.getAttribute("year"),
      prevMinimonth.getAttribute("month")
    );

    if (activeDate) {
      this.resetDisplayOfMonths();
      this.displayCurrentMonths(activeDate);
      this.togglePreviousMonthButton();
    }
  },
  /**
   * Shows the next month in the recurrence preview.
   */
  showNextMonth() {
    let prevMinimonth = this.node.querySelector(`calendar-minimonth[active-month="true"]`);

    let activeDate = this.nextMonthDate(
      prevMinimonth.getAttribute("year"),
      prevMinimonth.getAttribute("month")
    );

    if (activeDate) {
      this.resetDisplayOfMonths();
      this.displayCurrentMonths(activeDate);
      this.togglePreviousMonthButton();
    }
  },
  /**
   * Shows the current day's month in the recurrence preview.
   */
  jumpToToday() {
    let activeDate = new Date();
    this.resetDisplayOfMonths();
    this.displayCurrentMonths(activeDate);
    this.togglePreviousMonthButton();
  },
  /**
   * Selects the minimonth element belonging to a year and month.
   */
  selectMinimonth(year, month) {
    let minimonthIdentifier = `calendar-minimonth[year="${year}"][month="${month}"]`;
    let selectedMinimonth = this.node.querySelector(minimonthIdentifier);

    if (selectedMinimonth) {
      return selectedMinimonth;
    }

    selectedMinimonth = document.createXULElement("calendar-minimonth");
    this.node.appendChild(selectedMinimonth);

    selectedMinimonth.setAttribute("readonly", "true");
    selectedMinimonth.setAttribute("month", month);
    selectedMinimonth.setAttribute("year", year);
    selectedMinimonth.hidden = true;

    if (this.mRecurrenceInfo) {
      this.updatePreview(this.mRecurrenceInfo);
    }

    return selectedMinimonth;
  },
  /**
   * Returns the next month's first day when given a year and month.
   */
  nextMonthDate(currentYear, currentMonth) {
    // If month is December, select first day of January
    if (currentMonth == 11) {
      return new Date(parseInt(currentYear) + 1, 0, 1);
    }
    return new Date(parseInt(currentYear), parseInt(currentMonth) + 1, 1);
  },
  /**
   * Returns the previous month's first day when given a year and month.
   */
  previousMonthDate(currentYear, currentMonth) {
    // If month is January, select first day of December.
    if (currentMonth == 0) {
      return new Date(parseInt(currentYear) - 1, 11, 1);
    }
    return new Date(parseInt(currentYear), parseInt(currentMonth) - 1, 1);
  },
  /**
   * Reset the recurrence preview months, making all hidden and none set to active.
   */
  resetDisplayOfMonths() {
    let calContainer = this.node;
    for (let minimonth of calContainer.children) {
      minimonth.hidden = true;
      minimonth.setAttribute("active-month", false);
    }
  },
  /**
   * Display the active month and the next two months in the recurrence preview.
   */
  displayCurrentMonths(activeDate) {
    let activeMonth = activeDate.getMonth();
    let activeYear = activeDate.getFullYear();

    let month1Date = this.nextMonthDate(activeYear, activeMonth);
    let month2Date = this.nextMonthDate(month1Date.getFullYear(), month1Date.getMonth());

    let activeMinimonth = this.selectMinimonth(activeYear, activeMonth);
    let minimonth1 = this.selectMinimonth(month1Date.getFullYear(), month1Date.getMonth());
    let minimonth2 = this.selectMinimonth(month2Date.getFullYear(), month2Date.getMonth());

    activeMinimonth.setAttribute("active-month", true);
    activeMinimonth.removeAttribute("hidden");
    minimonth1.removeAttribute("hidden");
    minimonth2.removeAttribute("hidden");
  },
  /**
   * Disable previous month button when the active month is the first month of the event.
   */
  togglePreviousMonthButton() {
    let activeMinimonth = this.node.querySelector(`calendar-minimonth[active-month="true"]`);

    if (activeMinimonth.getAttribute("initial-month") == "true") {
      document.getElementById("recurrencePrevious").setAttribute("disabled", "true");
    } else {
      document.getElementById("recurrencePrevious").removeAttribute("disabled");
    }
  },
};

/**
 * An object containing the daypicker-weekday binding functionalities.
 */
const DaypickerWeekday = {
  /**
   * Method intitializing DaypickerWeekday.
   */
  init() {
    this.weekStartOffset = Services.prefs.getIntPref("calendar.week.start", 0);

    let mainbox = document.getElementById("daypicker-weekday");
    let numChilds = mainbox.children.length;
    for (let i = 0; i < numChilds; i++) {
      let child = mainbox.children[i];
      let dow = i + this.weekStartOffset;
      if (dow >= 7) {
        dow -= 7;
      }
      let day = cal.l10n.getString("dateFormat", `day.${dow + 1}.Mmm`);
      child.label = day;
      child.calendar = mainbox;
    }
  },
  /**
   * Getter for days property.
   */
  get days() {
    let mainbox = document.getElementById("daypicker-weekday");
    let numChilds = mainbox.children.length;
    let days = [];
    for (let i = 0; i < numChilds; i++) {
      let child = mainbox.children[i];
      if (child.getAttribute("checked") == "true") {
        let index = i + this.weekStartOffset;
        if (index >= 7) {
          index -= 7;
        }
        days.push(index + 1);
      }
    }
    return days;
  },
  /**
   * The weekday-picker manages an array of selected days of the week and
   * the 'days' property is the interface to this array. the expected argument is
   * an array containing integer elements, where each element represents a selected
   * day of the week, starting with SUNDAY=1.
   */
  set days(val) {
    let mainbox = document.getElementById("daypicker-weekday");
    for (let child of mainbox.children) {
      child.removeAttribute("checked");
    }
    for (let i in val) {
      let index = val[i] - 1 - this.weekStartOffset;
      if (index < 0) {
        index += 7;
      }
      mainbox.children[index].setAttribute("checked", "true");
    }
  },
};

/**
 * An object containing the daypicker-monthday binding functionalities.
 */
const DaypickerMonthday = {
  /**
   * Method intitializing DaypickerMonthday.
   */
  init() {
    let mainbox = document.querySelector(".daypicker-monthday-mainbox");
    let child = null;
    for (let row of mainbox.children) {
      for (child of row.children) {
        child.calendar = mainbox;
      }
    }
    let labelLastDay = cal.l10n.getString(
      "calendar-event-dialog",
      "eventRecurrenceMonthlyLastDayLabel"
    );
    child.setAttribute("label", labelLastDay);
  },
  /**
   * Setter for days property.
   */
  set days(val) {
    let mainbox = document.querySelector(".daypicker-monthday-mainbox");
    let days = [];
    for (let row of mainbox.children) {
      for (let child of row.children) {
        child.removeAttribute("checked");
        days.push(child);
      }
    }
    for (let i in val) {
      let lastDayOffset = val[i] == -1 ? 0 : -1;
      let index = val[i] < 0 ? val[i] + days.length + lastDayOffset : val[i] - 1;
      days[index].setAttribute("checked", "true");
    }
  },
  /**
   * Getter for days property.
   */
  get days() {
    let mainbox = document.querySelector(".daypicker-monthday-mainbox");
    let days = [];
    for (let row of mainbox.children) {
      for (let child of row.children) {
        if (child.getAttribute("checked") == "true") {
          days.push(Number(child.label) ? Number(child.label) : -1);
        }
      }
    }
    return days;
  },
  /**
   * Disables daypicker elements.
   */
  disable() {
    let mainbox = document.querySelector(".daypicker-monthday-mainbox");
    for (let row of mainbox.children) {
      for (let child of row.children) {
        child.setAttribute("disabled", "true");
      }
    }
  },
  /**
   * Enables daypicker elements.
   */
  enable() {
    let mainbox = document.querySelector(".daypicker-monthday-mainbox");
    for (let row of mainbox.children) {
      for (let child of row.children) {
        child.removeAttribute("disabled");
      }
    }
  },
};

/**
 * Sets up the recurrence dialog from the window arguments. Takes care of filling
 * the dialog controls with the recurrence information for this window.
 */
function onLoad() {
  RecurrencePreview.init();
  DaypickerWeekday.init();
  DaypickerMonthday.init();
  changeWidgetsOrder();

  let args = window.arguments[0];
  let item = args.calendarEvent;
  let calendar = item.calendar;
  let recinfo = args.recurrenceInfo;

  gStartTime = args.startTime;
  gEndTime = args.endTime;
  RecurrencePreview.dateTime = gStartTime.getInTimezone(cal.dtz.defaultTimezone);

  onChangeCalendar(calendar);

  // Set starting value for 'repeat until' rule and highlight the start date.
  let repeatDate = cal.dtz.dateTimeToJsDate(gStartTime.getInTimezone(cal.dtz.floating));
  document.getElementById("repeat-until-date").value = repeatDate;
  document.getElementById("repeat-until-date").extraDate = repeatDate;

  if (item.parentItem != item) {
    item = item.parentItem;
  }
  let rule = null;
  if (recinfo) {
    // Split out rules and exceptions
    try {
      let rrules = splitRecurrenceRules(recinfo);
      let rules = rrules[0];
      // Deal with the rules
      if (rules.length > 0) {
        // We only handle 1 rule currently
        rule = cal.wrapInstance(rules[0], Ci.calIRecurrenceRule);
      }
    } catch (ex) {
      console.error(ex);
    }
  }
  if (!rule) {
    rule = cal.createRecurrenceRule();
    rule.type = "DAILY";
    rule.interval = 1;
    rule.count = -1;

    // We don't let the user set the week start day for a given rule, but we
    // want to default to the user's week start so rules behave as expected
    let weekStart = Services.prefs.getIntPref("calendar.week.start", 0);
    rule.weekStart = weekStart;
  }
  initializeControls(rule);

  // Update controls
  updateRecurrenceBox();

  opener.setCursor("auto");
  self.focus();
}

/**
 * Initialize the dialog controls according to the passed rule
 *
 * @param rule    The recurrence rule to parse.
 */
function initializeControls(rule) {
  function getOrdinalAndWeekdayOfRule(aByDayRuleComponent) {
    return {
      ordinal: (aByDayRuleComponent - (aByDayRuleComponent % 8)) / 8,
      weekday: Math.abs(aByDayRuleComponent % 8),
    };
  }

  function setControlsForByMonthDay_YearlyRule(aDate, aByMonthDay) {
    if (aByMonthDay == -1) {
      // The last day of the month.
      document.getElementById("yearly-group").selectedIndex = 1;
      document.getElementById("yearly-ordinal").value = -1;
      document.getElementById("yearly-weekday").value = -1;
    } else {
      if (aByMonthDay < -1) {
        // The UI doesn't manage negative days apart from -1 but we can
        // display in the controls the day from the start of the month.
        aByMonthDay += aDate.endOfMonth.day + 1;
      }
      document.getElementById("yearly-group").selectedIndex = 0;
      document.getElementById("yearly-days").value = aByMonthDay;
    }
  }

  function everyWeekDay(aByDay) {
    // Checks if aByDay contains only values from 1 to 7 with any order.
    let mask = aByDay.reduce((value, item) => value | (1 << item), 1);
    return aByDay.length == 7 && mask == Math.pow(2, 8) - 1;
  }

  document.getElementById("week-start").value = rule.weekStart;

  switch (rule.type) {
    case "DAILY":
      document.getElementById("period-list").selectedIndex = 0;
      document.getElementById("daily-days").value = rule.interval;
      break;
    case "WEEKLY":
      document.getElementById("weekly-weeks").value = rule.interval;
      document.getElementById("period-list").selectedIndex = 1;
      break;
    case "MONTHLY":
      document.getElementById("monthly-interval").value = rule.interval;
      document.getElementById("period-list").selectedIndex = 2;
      break;
    case "YEARLY":
      document.getElementById("yearly-interval").value = rule.interval;
      document.getElementById("period-list").selectedIndex = 3;
      break;
    default:
      document.getElementById("period-list").selectedIndex = 0;
      dump("unable to handle your rule type!\n");
      break;
  }

  let byDayRuleComponent = rule.getComponent("BYDAY");
  let byMonthDayRuleComponent = rule.getComponent("BYMONTHDAY");
  let byMonthRuleComponent = rule.getComponent("BYMONTH");
  let kDefaultTimezone = cal.dtz.defaultTimezone;
  let startDate = gStartTime.getInTimezone(kDefaultTimezone);

  // "DAILY" ruletype
  // byDayRuleComponents may have been set priorily by "MONTHLY"- ruletypes
  // where they have a different context-
  // that's why we also query the current rule-type
  if (byDayRuleComponent.length == 0 || rule.type != "DAILY") {
    document.getElementById("daily-group").selectedIndex = 0;
  } else {
    document.getElementById("daily-group").selectedIndex = 1;
  }

  // "WEEKLY" ruletype
  if (byDayRuleComponent.length == 0 || rule.type != "WEEKLY") {
    DaypickerWeekday.days = [startDate.weekday + 1];
  } else {
    DaypickerWeekday.days = byDayRuleComponent;
  }

  // "MONTHLY" ruletype
  let ruleComponentsEmpty = byDayRuleComponent.length == 0 && byMonthDayRuleComponent.length == 0;
  if (ruleComponentsEmpty || rule.type != "MONTHLY") {
    document.getElementById("monthly-group").selectedIndex = 1;
    DaypickerMonthday.days = [startDate.day];
    let day = Math.floor((startDate.day - 1) / 7) + 1;
    document.getElementById("monthly-ordinal").value = day;
    document.getElementById("monthly-weekday").value = startDate.weekday + 1;
  } else if (everyWeekDay(byDayRuleComponent)) {
    // Every day of the month.
    document.getElementById("monthly-group").selectedIndex = 0;
    document.getElementById("monthly-ordinal").value = 0;
    document.getElementById("monthly-weekday").value = -1;
  } else if (byDayRuleComponent.length > 0) {
    // One of the first five days or weekdays of the month.
    document.getElementById("monthly-group").selectedIndex = 0;
    let ruleInfo = getOrdinalAndWeekdayOfRule(byDayRuleComponent[0]);
    document.getElementById("monthly-ordinal").value = ruleInfo.ordinal;
    document.getElementById("monthly-weekday").value = ruleInfo.weekday;
  } else if (byMonthDayRuleComponent.length == 1 && byMonthDayRuleComponent[0] == -1) {
    // The last day of the month.
    document.getElementById("monthly-group").selectedIndex = 0;
    document.getElementById("monthly-ordinal").value = byMonthDayRuleComponent[0];
    document.getElementById("monthly-weekday").value = byMonthDayRuleComponent[0];
  } else if (byMonthDayRuleComponent.length > 0) {
    document.getElementById("monthly-group").selectedIndex = 1;
    DaypickerMonthday.days = byMonthDayRuleComponent;
  }

  // "YEARLY" ruletype
  if (byMonthRuleComponent.length == 0 || rule.type != "YEARLY") {
    document.getElementById("yearly-month-rule").value = startDate.month + 1;
    document.getElementById("yearly-month-ordinal").value = startDate.month + 1;
    if (byMonthDayRuleComponent.length > 0) {
      setControlsForByMonthDay_YearlyRule(startDate, byMonthDayRuleComponent[0]);
    } else {
      document.getElementById("yearly-days").value = startDate.day;
      let ordinalDay = Math.floor((startDate.day - 1) / 7) + 1;
      document.getElementById("yearly-ordinal").value = ordinalDay;
      document.getElementById("yearly-weekday").value = startDate.weekday + 1;
    }
  } else {
    document.getElementById("yearly-month-rule").value = byMonthRuleComponent[0];
    document.getElementById("yearly-month-ordinal").value = byMonthRuleComponent[0];
    if (byMonthDayRuleComponent.length > 0) {
      let date = startDate.clone();
      date.month = byMonthRuleComponent[0] - 1;
      setControlsForByMonthDay_YearlyRule(date, byMonthDayRuleComponent[0]);
    } else if (byDayRuleComponent.length > 0) {
      document.getElementById("yearly-group").selectedIndex = 1;
      if (everyWeekDay(byDayRuleComponent)) {
        // Every day of the month.
        document.getElementById("yearly-ordinal").value = 0;
        document.getElementById("yearly-weekday").value = -1;
      } else {
        let yearlyRuleInfo = getOrdinalAndWeekdayOfRule(byDayRuleComponent[0]);
        document.getElementById("yearly-ordinal").value = yearlyRuleInfo.ordinal;
        document.getElementById("yearly-weekday").value = yearlyRuleInfo.weekday;
      }
    } else if (byMonthRuleComponent.length > 0) {
      document.getElementById("yearly-group").selectedIndex = 0;
      document.getElementById("yearly-days").value = startDate.day;
    }
  }

  /* load up the duration of the event radiogroup */
  if (rule.isByCount) {
    if (rule.count == -1) {
      document.getElementById("recurrence-duration").value = "forever";
    } else {
      document.getElementById("recurrence-duration").value = "ntimes";
      document.getElementById("repeat-ntimes-count").value = rule.count;
    }
  } else {
    let untilDate = rule.untilDate;
    if (untilDate) {
      gUntilDate = untilDate.getInTimezone(gStartTime.timezone); // calIRecurrenceRule::untilDate is always UTC or floating
      // Change the until date to start date if the rule has a forbidden
      // value (earlier than the start date).
      if (gUntilDate.compare(gStartTime) < 0) {
        gUntilDate = gStartTime.clone();
      }
      let repeatDate = cal.dtz.dateTimeToJsDate(gUntilDate.getInTimezone(cal.dtz.floating));
      document.getElementById("recurrence-duration").value = "until";
      document.getElementById("repeat-until-date").value = repeatDate;
    } else {
      document.getElementById("recurrence-duration").value = "forever";
    }
  }
}

/**
 * Save the recurrence information selected in the dialog back to the given
 * item.
 *
 * @param item    The item to save back to.
 * @returns The saved recurrence info.
 */
function onSave(item) {
  // Always return 'null' if this item is an occurrence.
  if (!item || item.parentItem != item) {
    return null;
  }

  // This works, but if we ever support more complex recurrence,
  // e.g. recurrence for Martians, then we're going to want to
  // not clone and just recreate the recurrenceInfo each time.
  // The reason is that the order of items (rules/dates/datesets)
  // matters, so we can't always just append at the end.  This
  // code here always inserts a rule first, because all our
  // exceptions should come afterward.
  let periodNumber = Number(document.getElementById("period-list").value);

  let args = window.arguments[0];
  let recurrenceInfo = args.recurrenceInfo;
  if (recurrenceInfo) {
    recurrenceInfo = recurrenceInfo.clone();
    let rrules = splitRecurrenceRules(recurrenceInfo);
    if (rrules[0].length > 0) {
      recurrenceInfo.deleteRecurrenceItem(rrules[0][0]);
    }
    recurrenceInfo.item = item;
  } else {
    recurrenceInfo = new CalRecurrenceInfo(item);
  }

  let recRule = cal.createRecurrenceRule();

  // We don't let the user edit the start of the week for a given rule, but we
  // want to preserve the value set
  let weekStart = Number(document.getElementById("week-start").value);
  recRule.weekStart = weekStart;

  const ALL_WEEKDAYS = [2, 3, 4, 5, 6, 7, 1]; // The sequence MO,TU,WE,TH,FR,SA,SU.
  switch (periodNumber) {
    case 0: {
      recRule.type = "DAILY";
      let dailyGroup = document.getElementById("daily-group");
      if (dailyGroup.selectedIndex == 0) {
        let ndays = Math.max(1, Number(document.getElementById("daily-days").value));
        recRule.interval = ndays;
      } else {
        recRule.interval = 1;
        let onDays = [2, 3, 4, 5, 6];
        recRule.setComponent("BYDAY", onDays);
      }
      break;
    }
    case 1: {
      recRule.type = "WEEKLY";
      let ndays = Number(document.getElementById("weekly-weeks").value);
      recRule.interval = ndays;
      let onDays = DaypickerWeekday.days;
      if (onDays.length > 0) {
        recRule.setComponent("BYDAY", onDays);
      }
      break;
    }
    case 2: {
      recRule.type = "MONTHLY";
      let monthInterval = Number(document.getElementById("monthly-interval").value);
      recRule.interval = monthInterval;
      let monthlyGroup = document.getElementById("monthly-group");
      if (monthlyGroup.selectedIndex == 0) {
        let monthlyOrdinal = Number(document.getElementById("monthly-ordinal").value);
        let monthlyDOW = Number(document.getElementById("monthly-weekday").value);
        if (monthlyDOW < 0) {
          if (monthlyOrdinal == 0) {
            // Monthly rule "Every day of the month".
            recRule.setComponent("BYDAY", ALL_WEEKDAYS);
          } else {
            // One of the first five days or the last day of the month.
            recRule.setComponent("BYMONTHDAY", [monthlyOrdinal]);
          }
        } else {
          let sign = monthlyOrdinal < 0 ? -1 : 1;
          let onDays = [(Math.abs(monthlyOrdinal) * 8 + monthlyDOW) * sign];
          recRule.setComponent("BYDAY", onDays);
        }
      } else {
        let monthlyDays = DaypickerMonthday.days;
        if (monthlyDays.length > 0) {
          recRule.setComponent("BYMONTHDAY", monthlyDays);
        }
      }
      break;
    }
    case 3: {
      recRule.type = "YEARLY";
      let yearInterval = Number(document.getElementById("yearly-interval").value);
      recRule.interval = yearInterval;
      let yearlyGroup = document.getElementById("yearly-group");
      if (yearlyGroup.selectedIndex == 0) {
        let yearlyByMonth = [Number(document.getElementById("yearly-month-ordinal").value)];
        recRule.setComponent("BYMONTH", yearlyByMonth);
        let yearlyByDay = [Number(document.getElementById("yearly-days").value)];
        recRule.setComponent("BYMONTHDAY", yearlyByDay);
      } else {
        let yearlyByMonth = [Number(document.getElementById("yearly-month-rule").value)];
        recRule.setComponent("BYMONTH", yearlyByMonth);
        let yearlyOrdinal = Number(document.getElementById("yearly-ordinal").value);
        let yearlyDOW = Number(document.getElementById("yearly-weekday").value);
        if (yearlyDOW < 0) {
          if (yearlyOrdinal == 0) {
            // Yearly rule "Every day of a month".
            recRule.setComponent("BYDAY", ALL_WEEKDAYS);
          } else {
            // One of the first five days or the last of a month.
            recRule.setComponent("BYMONTHDAY", [yearlyOrdinal]);
          }
        } else {
          let sign = yearlyOrdinal < 0 ? -1 : 1;
          let onDays = [(Math.abs(yearlyOrdinal) * 8 + yearlyDOW) * sign];
          recRule.setComponent("BYDAY", onDays);
        }
      }
      break;
    }
  }

  // Figure out how long this event is supposed to last
  switch (document.getElementById("recurrence-duration").selectedItem.value) {
    case "forever": {
      recRule.count = -1;
      break;
    }
    case "ntimes": {
      recRule.count = Math.max(1, document.getElementById("repeat-ntimes-count").value);
      break;
    }
    case "until": {
      let untilDate = cal.dtz.jsDateToDateTime(
        document.getElementById("repeat-until-date").value,
        gStartTime.timezone
      );
      untilDate.isDate = gStartTime.isDate; // enforce same value type as DTSTART
      if (!gStartTime.isDate) {
        // correct UNTIL to exactly match start date's hour, minute, second:
        untilDate.hour = gStartTime.hour;
        untilDate.minute = gStartTime.minute;
        untilDate.second = gStartTime.second;
      }
      recRule.untilDate = untilDate;
      break;
    }
  }

  if (recRule.interval < 1) {
    return null;
  }

  recurrenceInfo.insertRecurrenceItemAt(recRule, 0);
  return recurrenceInfo;
}

/**
 * Handler function to be called when the accept button is pressed.
 */
document.addEventListener("dialogaccept", event => {
  let args = window.arguments[0];
  let item = args.calendarEvent;
  args.onOk(onSave(item));
  // Don't close the dialog if a warning must be showed.
  if (checkUntilDate.warning) {
    event.preventDefault();
  }
});

/**
 * Handler function to be called when the Cancel button is pressed.
 */
document.addEventListener("dialogcancel", () => {
  // Don't show any warning if the dialog must be closed.
  checkUntilDate.warning = false;
});

/**
 * Handler function called when the calendar is changed (also for initial
 * setup).
 *
 * XXX we don't change the calendar in this dialog, this function should be
 * consolidated or renamed.
 *
 * @param calendar    The calendar to use for setup.
 */
function onChangeCalendar(calendar) {
  let args = window.arguments[0];
  let item = args.calendarEvent;

  // Set 'gIsReadOnly' if the calendar is read-only
  gIsReadOnly = false;
  if (calendar && calendar.readOnly) {
    gIsReadOnly = true;
  }

  // Disable or enable controls based on a set or rules
  // - whether this item is a stand-alone item or an occurrence
  // - whether or not this item is read-only
  // - whether or not the state of the item allows recurrence rules
  //     - tasks without an entrydate are invalid
  disableOrEnable(item);

  updateRecurrenceControls();
}

/**
 * Disable or enable certain controls based on the given item:
 * Uses the following attribute:
 *
 * - disable-on-occurrence
 * - disable-on-readonly
 *
 * A task without a start time is also considered readonly.
 *
 * @param item        The item to check.
 */
function disableOrEnable(item) {
  if (item.parentItem != item) {
    disableRecurrenceFields("disable-on-occurrence");
  } else if (gIsReadOnly) {
    disableRecurrenceFields("disable-on-readonly");
  } else if (item.isTodo() && !gStartTime) {
    disableRecurrenceFields("disable-on-readonly");
  } else {
    enableRecurrenceFields("disable-on-readonly");
  }
}

/**
 * Disables all fields that have an attribute that matches the argument and is
 * set to "true".
 *
 * @param aAttributeName    The attribute to search for.
 */
function disableRecurrenceFields(aAttributeName) {
  let disableElements = document.getElementsByAttribute(aAttributeName, "true");
  for (let i = 0; i < disableElements.length; i++) {
    disableElements[i].setAttribute("disabled", "true");
  }
}

/**
 * Enables all fields that have an attribute that matches the argument and is
 * set to "true".
 *
 * @param aAttributeName    The attribute to search for.
 */
function enableRecurrenceFields(aAttributeName) {
  let enableElements = document.getElementsByAttribute(aAttributeName, "true");
  for (let i = 0; i < enableElements.length; i++) {
    enableElements[i].removeAttribute("disabled");
  }
}

/**
 * Handler function to update the period-box when an item from the period-list
 * is selected. Also updates the controls on that period-box.
 */
function updateRecurrenceBox() {
  let periodBox = document.getElementById("period-box");
  let periodNumber = Number(document.getElementById("period-list").value);
  for (let i = 0; i < periodBox.children.length; i++) {
    periodBox.children[i].hidden = i != periodNumber;
  }
  updateRecurrenceControls();
}

/**
 * Updates the controls regarding ranged controls (i.e repeat forever, repeat
 * until, repeat n times...)
 */
function updateRecurrenceRange() {
  let args = window.arguments[0];
  let item = args.calendarEvent;
  if (item.parentItem != item || gIsReadOnly) {
    return;
  }

  let radioRangeForever = document.getElementById("recurrence-range-forever");
  let radioRangeFor = document.getElementById("recurrence-range-for");
  let radioRangeUntil = document.getElementById("recurrence-range-until");
  let rangeTimesCount = document.getElementById("repeat-ntimes-count");
  let rangeUntilDate = document.getElementById("repeat-until-date");
  let rangeAppointmentsLabel = document.getElementById("repeat-appointments-label");

  radioRangeForever.removeAttribute("disabled");
  radioRangeFor.removeAttribute("disabled");
  radioRangeUntil.removeAttribute("disabled");
  rangeAppointmentsLabel.removeAttribute("disabled");

  let durationSelection = document.getElementById("recurrence-duration").selectedItem.value;

  if (durationSelection == "ntimes") {
    rangeTimesCount.removeAttribute("disabled");
  } else {
    rangeTimesCount.setAttribute("disabled", "true");
  }

  if (durationSelection == "until") {
    rangeUntilDate.removeAttribute("disabled");
  } else {
    rangeUntilDate.setAttribute("disabled", "true");
  }
}

/**
 * Updates the recurrence preview calendars using the window's item.
 */
function updatePreview() {
  let args = window.arguments[0];
  let item = args.calendarEvent;
  if (item.parentItem != item) {
    item = item.parentItem;
  }

  // TODO: We should better start the whole dialog with a newly cloned item
  // and always pump changes immediately into it. This would eliminate the
  // need to break the encapsulation, as we do it here. But we need the item
  // to contain the startdate in order to calculate the recurrence preview.
  item = item.clone();
  let kDefaultTimezone = cal.dtz.defaultTimezone;
  if (item.isEvent()) {
    let startDate = gStartTime.getInTimezone(kDefaultTimezone);
    let endDate = gEndTime.getInTimezone(kDefaultTimezone);
    if (startDate.isDate) {
      endDate.day--;
    }

    item.startDate = startDate;
    item.endDate = endDate;
  }
  if (item.isTodo()) {
    let entryDate = gStartTime;
    if (entryDate) {
      entryDate = entryDate.getInTimezone(kDefaultTimezone);
    } else {
      item.recurrenceInfo = null;
    }
    item.entryDate = entryDate;
    let dueDate = gEndTime;
    if (dueDate) {
      dueDate = dueDate.getInTimezone(kDefaultTimezone);
    }
    item.dueDate = dueDate;
  }

  let recInfo = onSave(item);
  RecurrencePreview.updatePreview(recInfo);
}

/**
 * Checks the until date just entered in the datepicker in order to avoid
 * setting a date earlier than the start date.
 * Restores the previous correct date, shows a warning and prevents to close the
 * dialog when the user enters a wrong until date.
 */
function checkUntilDate() {
  if (!gStartTime) {
    // This function shouldn't run before onLoad.
    return;
  }

  let untilDate = cal.dtz.jsDateToDateTime(
    document.getElementById("repeat-until-date").value,
    gStartTime.timezone
  );
  let startDate = gStartTime.clone();
  startDate.isDate = true;
  if (untilDate.compare(startDate) < 0) {
    let repeatDate = cal.dtz.dateTimeToJsDate(
      (gUntilDate || gStartTime).getInTimezone(cal.dtz.floating)
    );
    document.getElementById("repeat-until-date").value = repeatDate;
    checkUntilDate.warning = true;
    let callback = function () {
      // No warning when the dialog is being closed with the Cancel button.
      if (!checkUntilDate.warning) {
        return;
      }
      Services.prompt.alert(
        null,
        document.title,
        cal.l10n.getCalString("warningUntilDateBeforeStart")
      );
      checkUntilDate.warning = false;
    };
    setTimeout(callback, 1);
  } else {
    gUntilDate = untilDate;
    updateRecurrenceControls();
  }
}

/**
 * Checks the date entered for a yearly absolute rule (i.e. every 12 of January)
 * in order to avoid creating a rule on an invalid date.
 */
function checkYearlyAbsoluteDate() {
  if (!gStartTime) {
    // This function shouldn't run before onLoad.
    return;
  }

  const MONTH_LENGTHS = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
  let dayOfMonth = document.getElementById("yearly-days").value;
  let month = document.getElementById("yearly-month-ordinal").value;
  document.getElementById("yearly-days").max = MONTH_LENGTHS[month - 1];
  // Check if the day value is too high.
  if (dayOfMonth > MONTH_LENGTHS[month - 1]) {
    document.getElementById("yearly-days").value = MONTH_LENGTHS[month - 1];
  } else {
    updateRecurrenceControls();
  }
  // Check if the day value is too low.
  if (dayOfMonth < 1) {
    document.getElementById("yearly-days").value = 1;
  } else {
    updateRecurrenceControls();
  }
}

/**
 * Update all recurrence controls on the dialog.
 */
function updateRecurrenceControls() {
  updateRecurrencePattern();
  updateRecurrenceRange();
  updatePreview();
  window.sizeToContent();
}

/**
 * Disables/enables controls related to the recurrence pattern.
 * the status of the controls depends on which period entry is selected
 * and which form of pattern rule is selected.
 */
function updateRecurrencePattern() {
  let args = window.arguments[0];
  let item = args.calendarEvent;
  if (item.parentItem != item || gIsReadOnly) {
    return;
  }

  switch (Number(document.getElementById("period-list").value)) {
    // daily
    case 0: {
      let dailyGroup = document.getElementById("daily-group");
      let dailyDays = document.getElementById("daily-days");
      dailyDays.removeAttribute("disabled");
      if (dailyGroup.selectedIndex == 1) {
        dailyDays.setAttribute("disabled", "true");
      }
      break;
    }
    // weekly
    case 1: {
      break;
    }
    // monthly
    case 2: {
      let monthlyGroup = document.getElementById("monthly-group");
      let monthlyOrdinal = document.getElementById("monthly-ordinal");
      let monthlyWeekday = document.getElementById("monthly-weekday");
      let monthlyDays = DaypickerMonthday;
      monthlyOrdinal.removeAttribute("disabled");
      monthlyWeekday.removeAttribute("disabled");
      monthlyDays.enable();
      if (monthlyGroup.selectedIndex == 0) {
        monthlyDays.disable();
      } else {
        monthlyOrdinal.setAttribute("disabled", "true");
        monthlyWeekday.setAttribute("disabled", "true");
      }
      break;
    }
    // yearly
    case 3: {
      let yearlyGroup = document.getElementById("yearly-group");
      let yearlyDays = document.getElementById("yearly-days");
      let yearlyMonthOrdinal = document.getElementById("yearly-month-ordinal");
      let yearlyPeriodOfMonthLabel = document.getElementById("yearly-period-of-month-label");
      let yearlyOrdinal = document.getElementById("yearly-ordinal");
      let yearlyWeekday = document.getElementById("yearly-weekday");
      let yearlyMonthRule = document.getElementById("yearly-month-rule");
      let yearlyPeriodOfLabel = document.getElementById("yearly-period-of-label");
      yearlyDays.removeAttribute("disabled");
      yearlyMonthOrdinal.removeAttribute("disabled");
      yearlyOrdinal.removeAttribute("disabled");
      yearlyWeekday.removeAttribute("disabled");
      yearlyMonthRule.removeAttribute("disabled");
      yearlyPeriodOfLabel.removeAttribute("disabled");
      yearlyPeriodOfMonthLabel.removeAttribute("disabled");
      if (yearlyGroup.selectedIndex == 0) {
        yearlyOrdinal.setAttribute("disabled", "true");
        yearlyWeekday.setAttribute("disabled", "true");
        yearlyMonthRule.setAttribute("disabled", "true");
        yearlyPeriodOfLabel.setAttribute("disabled", "true");
      } else {
        yearlyDays.setAttribute("disabled", "true");
        yearlyMonthOrdinal.setAttribute("disabled", "true");
        yearlyPeriodOfMonthLabel.setAttribute("disabled", "true");
      }
      break;
    }
  }
}

/**
 * This function changes the order for certain elements using a locale string.
 * This is needed for some locales that expect a different wording order.
 *
 * @param aPropKey      The locale property key to get the order from
 * @param aPropParams   An array of ids to be passed to the locale property.
 *                        These should be the ids of the elements to change
 *                        the order for.
 */
function changeOrderForElements(aPropKey, aPropParams) {
  let localeOrder;
  let parents = {};

  for (let key in aPropParams) {
    // Save original parents so that the nodes to reorder get appended to
    // the correct parent nodes.
    parents[key] = document.getElementById(aPropParams[key]).parentNode;
  }

  try {
    localeOrder = cal.l10n.getString("calendar-event-dialog", aPropKey, aPropParams).split(" ");
  } catch (ex) {
    let msg =
      "The key " +
      aPropKey +
      " in calendar-event-dialog.prop" +
      "erties has incorrect number of params. Expected " +
      aPropParams.length +
      " params.";
    console.error(msg + " " + ex);
    return;
  }

  // Add elements in the right order, removing them from their old parent
  for (let i = 0; i < aPropParams.length; i++) {
    let newEl = document.getElementById(localeOrder[i]);
    if (newEl) {
      parents[i].appendChild(newEl);
    } else {
      cal.ERROR(
        "Localization error, could not find node '" +
          localeOrder[i] +
          "'. Please have your localizer check the string '" +
          aPropKey +
          "'"
      );
    }
  }
}

/**
 * Change locale-specific widget order for Edit Recurrence window
 */
function changeWidgetsOrder() {
  changeOrderForElements("monthlyOrder", ["monthly-ordinal", "monthly-weekday"]);
  changeOrderForElements("yearlyOrder", [
    "yearly-days",
    "yearly-period-of-month-label",
    "yearly-month-ordinal",
  ]);
  changeOrderForElements("yearlyOrder2", [
    "yearly-ordinal",
    "yearly-weekday",
    "yearly-period-of-label",
    "yearly-month-rule",
  ]);
}
