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

/* import-globals-from calendar-management.js */
/* import-globals-from calendar-views-utils.js */

var { cal } = ChromeUtils.import("resource://calendar/modules/calUtils.jsm");
var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");

/* exported modifyEventWithDialog, undo, redo, setContextPartstat */

/**
 * Takes a job and makes sure the dispose function on it is called. If there is
 * no dispose function or the job is null, ignore it.
 *
 * @param job       The job to dispose.
 */
function disposeJob(job) {
  if (job && job.dispose) {
    job.dispose();
  }
}

/**
 * Sets the default values for new items, taking values from either the passed
 * parameters or the preferences
 *
 * @param aItem         The item to set up
 * @param aCalendar     (optional) The calendar to apply.
 * @param aStartDate    (optional) The start date to set.
 * @param aEndDate      (optional) The end date/due date to set.
 * @param aInitialDate  (optional) The reference date for the date pickers
 * @param aForceAllday  (optional) Force the event/task to be an allday item.
 */
function setDefaultItemValues(
  aItem,
  aCalendar = null,
  aStartDate = null,
  aEndDate = null,
  aInitialDate = null,
  aForceAllday = false
) {
  function endOfDay(aDate) {
    let eod = aDate ? aDate.clone() : cal.dtz.now();
    eod.hour = Services.prefs.getIntPref("calendar.view.dayendhour", 19);
    eod.minute = 0;
    eod.second = 0;
    return eod;
  }
  function startOfDay(aDate) {
    let sod = aDate ? aDate.clone() : cal.dtz.now();
    sod.hour = Services.prefs.getIntPref("calendar.view.daystarthour", 8);
    sod.minute = 0;
    sod.second = 0;
    return sod;
  }

  let initialDate = aInitialDate ? aInitialDate.clone() : cal.dtz.now();
  initialDate.isDate = true;

  if (cal.item.isEvent(aItem)) {
    if (aStartDate) {
      aItem.startDate = aStartDate.clone();
      if (aStartDate.isDate && !aForceAllday) {
        // This is a special case where the date is specified, but the
        // time is not. To take care, we setup up the time to our
        // default event start time.
        aItem.startDate = cal.dtz.getDefaultStartDate(aItem.startDate);
      } else if (aForceAllday) {
        // If the event should be forced to be allday, then don't set up
        // any default hours and directly make it allday.
        aItem.startDate.isDate = true;
        aItem.startDate.timezone = cal.dtz.floating;
      }
    } else {
      // If no start date was passed, then default to the next full hour
      // of today, but with the date of the selected day
      aItem.startDate = cal.dtz.getDefaultStartDate(initialDate);
    }

    if (aEndDate) {
      aItem.endDate = aEndDate.clone();
      if (aForceAllday) {
        // XXX it is currently not specified, how callers that force all
        // day should pass the end date. Right now, they should make
        // sure that the end date is 00:00:00 of the day after.
        aItem.endDate.isDate = true;
        aItem.endDate.timezone = cal.dtz.floating;
      }
    } else {
      aItem.endDate = aItem.startDate.clone();
      if (aForceAllday) {
        // All day events need to go to the beginning of the next day.
        aItem.endDate.day++;
      } else {
        // If the event is not all day, then add the default event
        // length.
        aItem.endDate.minute += Services.prefs.getIntPref("calendar.event.defaultlength", 60);
      }
    }

    // Free/busy status is only valid for events, must not be set for tasks.
    aItem.setProperty("TRANSP", cal.item.getEventDefaultTransparency(aForceAllday));
  } else if (cal.item.isToDo(aItem)) {
    let now = cal.dtz.now();
    let initDate = initialDate ? initialDate.clone() : now;
    initDate.isDate = false;
    initDate.hour = now.hour;
    initDate.minute = now.minute;
    initDate.second = now.second;

    if (aStartDate) {
      aItem.entryDate = aStartDate.clone();
    } else {
      let defaultStart = Services.prefs.getStringPref("calendar.task.defaultstart", "none");
      if (
        Services.prefs.getIntPref("calendar.alarms.onfortodos", 0) == 1 &&
        defaultStart == "none"
      ) {
        // start date is required if we want to set an alarm
        defaultStart = "offsetcurrent";
      }

      let units = Services.prefs.getStringPref("calendar.task.defaultstartoffsetunits", "minutes");
      if (!["days", "hours", "minutes"].includes(units)) {
        units = "minutes";
      }
      let startOffset = cal.createDuration();
      startOffset[units] = Services.prefs.getIntPref("calendar.task.defaultstartoffset", 0);
      let start;

      switch (defaultStart) {
        case "none":
          break;
        case "startofday":
          start = startOfDay(initDate);
          break;
        case "tomorrow":
          start = startOfDay(initDate);
          start.day++;
          break;
        case "nextweek":
          start = startOfDay(initDate);
          start.day += 7;
          break;
        case "offsetcurrent":
          start = initDate.clone();
          start.addDuration(startOffset);
          break;
        case "offsetnexthour":
          start = initDate.clone();
          start.second = 0;
          start.minute = 0;
          start.hour++;
          start.addDuration(startOffset);
          break;
      }

      if (start) {
        aItem.entryDate = start;
      }
    }

    if (aEndDate) {
      aItem.dueDate = aEndDate.clone();
    } else {
      let defaultDue = Services.prefs.getStringPref("calendar.task.defaultdue", "none");

      let units = Services.prefs.getStringPref("calendar.task.defaultdueoffsetunits", "minutes");
      if (!["days", "hours", "minutes"].includes(units)) {
        units = "minutes";
      }
      let dueOffset = cal.createDuration();
      dueOffset[units] = Services.prefs.getIntPref("calendar.task.defaultdueoffset", 0);

      let start = aItem.entryDate ? aItem.entryDate.clone() : initDate.clone();
      let due;

      switch (defaultDue) {
        case "none":
          break;
        case "endofday":
          due = endOfDay(start);
          // go to tomorrow if we're past the end of today
          if (start.compare(due) > 0) {
            due.day++;
          }
          break;
        case "tomorrow":
          due = endOfDay(start);
          due.day++;
          break;
        case "nextweek":
          due = endOfDay(start);
          due.day += 7;
          break;
        case "offsetcurrent":
          due = start.clone();
          due.addDuration(dueOffset);
          break;
        case "offsetnexthour":
          due = start.clone();
          due.second = 0;
          due.minute = 0;
          due.hour++;
          due.addDuration(dueOffset);
          break;
      }

      if (aItem.entryDate && due && aItem.entryDate.compare(due) > 0) {
        // due can't be earlier than start date.
        due = aItem.entryDate;
      }

      if (due) {
        aItem.dueDate = due;
      }
    }
  }

  // Calendar
  aItem.calendar = aCalendar || getSelectedCalendar();

  // Alarms
  cal.alarms.setDefaultValues(aItem);
}

/**
 * Creates an event with the calendar event dialog.
 *
 * @param calendar      (optional) The calendar to create the event in
 * @param startDate     (optional) The event's start date.
 * @param endDate       (optional) The event's end date.
 * @param summary       (optional) The event's title.
 * @param event         (optional) A template event to show in the dialog
 * @param aForceAllDay  (optional) Make sure the event shown in the dialog is an
 *                                   allday event.
 */
function createEventWithDialog(calendar, startDate, endDate, summary, event, aForceAllday) {
  let onNewEvent = function(item, opcalendar, originalItem, listener, extresponse = null) {
    if (item.id) {
      // If the item already has an id, then this is the result of
      // saving the item without closing, and then saving again.
      doTransaction("modify", item, opcalendar, originalItem, listener, extresponse);
    } else {
      // Otherwise, this is an addition
      doTransaction("add", item, opcalendar, null, listener, extresponse);
    }
  };

  if (event) {
    if (!event.isMutable) {
      event = event.clone();
    }
    // If the event should be created from a template, then make sure to
    // remove the id so that the item obtains a new id when doing the
    // transaction
    event.id = null;

    if (aForceAllday) {
      event.startDate.isDate = true;
      event.endDate.isDate = true;
      if (event.startDate.compare(event.endDate) == 0) {
        // For a one day all day event, the end date must be 00:00:00 of
        // the next day.
        event.endDate.day++;
      }
    }

    if (!event.calendar) {
      event.calendar = calendar || getSelectedCalendar();
    }
  } else {
    event = cal.createEvent();

    let refDate = currentView().initialized && currentView().selectedDay.clone();
    setDefaultItemValues(event, calendar, startDate, endDate, refDate, aForceAllday);
    if (summary) {
      event.title = summary;
    }
  }
  openEventDialog(event, event.calendar, "new", onNewEvent);
}

/**
 * Creates a task with the calendar event dialog.
 *
 * @param calendar      (optional) The calendar to create the task in
 * @param dueDate       (optional) The task's due date.
 * @param summary       (optional) The task's title.
 * @param todo          (optional) A template task to show in the dialog.
 * @param initialDate   (optional) The initial date for new task datepickers
 */
function createTodoWithDialog(calendar, dueDate, summary, todo, initialDate) {
  let onNewItem = function(item, opcalendar, originalItem, listener, extresponse = null) {
    if (item.id) {
      // If the item already has an id, then this is the result of
      // saving the item without closing, and then saving again.
      doTransaction("modify", item, opcalendar, originalItem, listener, extresponse);
    } else {
      // Otherwise, this is an addition
      doTransaction("add", item, opcalendar, null, listener, extresponse);
    }
  };

  if (todo) {
    // If the todo should be created from a template, then make sure to
    // remove the id so that the item obtains a new id when doing the
    // transaction
    if (todo.id) {
      todo = todo.clone();
      todo.id = null;
    }

    if (!todo.calendar) {
      todo.calendar = calendar || getSelectedCalendar();
    }
  } else {
    todo = cal.createTodo();
    setDefaultItemValues(todo, calendar, null, dueDate, initialDate);

    if (summary) {
      todo.title = summary;
    }
  }

  openEventDialog(todo, calendar, "new", onNewItem, null, initialDate);
}

/**
 * Modifies the passed event in the event dialog.
 *
 * @param aItem                 The item to modify.
 * @param job                   (optional) The job object that controls this
 *                                           modification.
 * @param aPromptOccurrence     If the user should be prompted to select if the
 *                                parent item or occurrence should be modified.
 * @param initialDate           (optional) The initial date for new task datepickers
 * @param aCounterProposal      (optional) An object representing the counterproposal
 *        {
 *            {JsObject} result: {
 *                type: {String} "OK"|"OUTDATED"|"NOTLATESTUPDATE"|"ERROR"|"NODIFF"
 *                descr: {String} a technical description of the problem if type is ERROR or NODIFF,
 *                                otherwise an empty string
 *            },
 *            (empty if result.type = "ERROR"|"NODIFF"){Array} differences: [{
 *                property: {String} a property that is subject to the proposal
 *                proposed: {String} the proposed value
 *                original: {String} the original value
 *            }]
 *        }
 */
function modifyEventWithDialog(
  aItem,
  job = null,
  aPromptOccurrence,
  initialDate = null,
  aCounterProposal
) {
  let dlg = cal.item.findWindow(aItem);
  if (dlg) {
    dlg.focus();
    disposeJob(job);
    return;
  }

  let onModifyItem = function(item, calendar, originalItem, listener, extresponse = null) {
    doTransaction("modify", item, calendar, originalItem, listener, extresponse);
  };

  let item = aItem;
  let response;
  if (aPromptOccurrence !== false) {
    [item, , response] = promptOccurrenceModification(aItem, true, "edit");
  }

  if (item && (response || response === undefined)) {
    openEventDialog(
      item,
      item.calendar,
      "modify",
      onModifyItem,
      job,
      initialDate,
      aCounterProposal
    );
  } else {
    disposeJob(job);
  }
}

/**
 * Opens the event dialog with the given item (task OR event)
 *
 * @param calendarItem      The item to open the dialog with
 * @param calendar          The calendar to open the dialog with.
 * @param mode              The operation the dialog should do ("new", "modify")
 * @param callback          The callback to call when the dialog has completed.
 * @param job               (optional) The job object for the modification.
 * @param initialDate       (optional) The initial date for new task datepickers
 * @param counterProposal   (optional) An object representing the counterproposal - see
 *                                     description for modifyEventWithDialog()
 */
function openEventDialog(
  calendarItem,
  calendar,
  mode,
  callback,
  job = null,
  initialDate = null,
  counterProposal
) {
  let dlg = cal.item.findWindow(calendarItem);
  if (dlg) {
    dlg.focus();
    disposeJob(job);
    return;
  }

  // Set up some defaults
  mode = mode || "new";
  calendar = calendar || getSelectedCalendar();
  let calendars = cal.getCalendarManager().getCalendars({});
  calendars = calendars.filter(cal.acl.isCalendarWritable);

  let isItemSupported;
  if (cal.item.isToDo(calendarItem)) {
    isItemSupported = function(aCalendar) {
      return aCalendar.getProperty("capabilities.tasks.supported") !== false;
    };
  } else if (cal.item.isEvent(calendarItem)) {
    isItemSupported = function(aCalendar) {
      return aCalendar.getProperty("capabilities.events.supported") !== false;
    };
  }

  // Filter out calendars that don't support the given calendar item
  calendars = calendars.filter(isItemSupported);

  // Filter out calendar/items that we cannot write to/modify
  if (mode == "new") {
    calendars = calendars.filter(cal.acl.userCanAddItemsToCalendar);
  } else {
    /* modify */
    calendars = calendars.filter(aCalendar => {
      /* If the calendar is the item calendar, we check that the item
       * can be modified. If the calendar is NOT the item calendar, we
       * check that the user can remove items from that calendar and
       * add items to the current one.
       */
      let isSameCalendar = calendarItem.calendar == aCalendar;
      let canModify = cal.acl.userCanModifyItem(calendarItem);
      let canMoveItems =
        cal.acl.userCanDeleteItemsFromCalendar(calendarItem.calendar) &&
        cal.acl.userCanAddItemsToCalendar(aCalendar);

      return isSameCalendar ? canModify : canMoveItems;
    });
  }

  if (
    mode == "new" &&
    (!cal.acl.isCalendarWritable(calendar) ||
      !cal.acl.userCanAddItemsToCalendar(calendar) ||
      !isItemSupported(calendar))
  ) {
    if (calendars.length < 1) {
      // There are no writable calendars or no calendar supports the given
      // item. Don't show the dialog.
      disposeJob(job);
      return;
    } else {
      // Pick the first calendar that supports the item and is writable
      calendar = calendars[0];
      if (calendarItem) {
        // XXX The dialog currently uses the items calendar as a first
        // choice. Since we are shortly before a release to keep
        // regression risk low, explicitly set the item's calendar here.
        calendarItem.calendar = calendars[0];
      }
    }
  }

  // Setup the window arguments
  let args = {};
  args.calendarEvent = calendarItem;
  args.calendar = calendar;
  args.mode = mode;
  args.onOk = callback;
  args.job = job;
  args.initialStartDateValue = initialDate || cal.dtz.getDefaultStartDate();
  args.counterProposal = counterProposal;
  args.inTab = Services.prefs.getBoolPref("calendar.item.editInTab", false);
  args.useNewItemUI = Services.prefs.getBoolPref("calendar.item.useNewItemUI", false);

  // this will be called if file->new has been selected from within the dialog
  args.onNewEvent = function(opcalendar) {
    createEventWithDialog(opcalendar, null, null);
  };
  args.onNewTodo = function(opcalendar) {
    createTodoWithDialog(opcalendar);
  };

  // the dialog will reset this to auto when it is done loading.
  window.setCursor("wait");

  // ask the provide if this item is an invitation. if this is the case
  // we'll open the summary dialog since the user is not allowed to change
  // the details of the item.
  let wrappedCalendar = cal.wrapInstance(calendar, Ci.calISchedulingSupport);
  let isInvitation = wrappedCalendar && wrappedCalendar.isInvitation(calendarItem);

  // open the dialog modeless
  let url;
  let isEditable = mode == "modify" && !isInvitation && cal.acl.userCanModifyItem(calendarItem);
  if (cal.acl.isCalendarWritable(calendar) && (mode == "new" || isEditable)) {
    if (args.inTab) {
      url = args.useNewItemUI
        ? "chrome://lightning/content/html-item-editing/lightning-item-iframe.html"
        : "chrome://lightning/content/lightning-item-iframe.xul";
    } else {
      url = "chrome://calendar/content/calendar-event-dialog.xul";
    }
  } else {
    url = "chrome://calendar/content/calendar-summary-dialog.xul";
    args.inTab = false;
  }

  if (args.inTab) {
    // open in a tab, currently the read-only summary dialog is
    // never opened in a tab
    args.url = url;
    let tabmail = document.getElementById("tabmail");
    let tabtype = cal.item.isEvent(args.calendarEvent) ? "calendarEvent" : "calendarTask";
    tabmail.openTab(tabtype, args);
  } else {
    // open in a window

    // reminder: event dialog should not be modal (cf bug 122671)
    let features;
    // keyword "dependent" should not be used (cf bug 752206)
    if (Services.appinfo.OS == "WINNT") {
      features = "chrome,titlebar,resizable";
    } else if (Services.appinfo.OS == "Darwin") {
      features = "chrome,titlebar,resizable,minimizable=no";
    } else {
      // All other targets, mostly Linux flavors using gnome.
      features = "chrome,titlebar,resizable,minimizable=no,dialog=no";
    }
    openDialog(url, "_blank", features, args);
  }
}

/**
 * Prompts the user how the passed item should be modified. If the item is an
 * exception or already a parent item, the item is returned without prompting.
 * If "all occurrences" is specified, the parent item is returned. If "this
 * occurrence only" is specified, then aItem is returned. If "this and following
 * occurrences" is selected, aItem's parentItem is modified so that the
 * recurrence rules end (UNTIL) just before the given occurrence. If
 * aNeedsFuture is specified, a new item is made from the part that was stripped
 * off the passed item.
 *
 * EXDATEs and RDATEs that do not fit into the items recurrence are removed. If
 * the modified item or the future item only consist of a single occurrence,
 * they are changed to be single items.
 *
 * @param aItem                         The item or array of items to check.
 * @param aNeedsFuture                  If true, the future item is parsed.
 *                                        This parameter can for example be
 *                                        false if a deletion is being made.
 * @param aAction                       Either "edit" or "delete". Sets up
 *                                          the labels in the occurrence prompt
 * @return [modifiedItem, futureItem, promptResponse]
 *                                      modifiedItem is a single item or array
 *                                        of items depending on the past aItem
 *
 *                                        If "this and all following" was chosen,
 *                                        an array containing the item *until*
 *                                        the given occurrence (modifiedItem),
 *                                        and the item *after* the given
 *                                        occurrence (futureItem).
 *
 *                                        If any other option was chosen,
 *                                        futureItem is null  and the
 *                                        modifiedItem is either the parent item
 *                                        or the passed occurrence, or null if
 *                                        the dialog was canceled.
 *
 *                                        The promptResponse parameter gives the
 *                                        response of the dialog as a constant.
 */
function promptOccurrenceModification(aItem, aNeedsFuture, aAction) {
  const CANCEL = 0;
  const MODIFY_OCCURRENCE = 1;
  const MODIFY_FOLLOWING = 2;
  const MODIFY_PARENT = 3;

  let futureItems = false;
  let pastItems = [];
  let returnItem = null;
  let type = CANCEL;
  let items = Array.isArray(aItem) ? aItem : [aItem];

  // Check if this actually is an instance of a recurring event
  if (items.every(item => item == item.parentItem)) {
    type = MODIFY_PARENT;
  } else if (aItem && items.length) {
    // Prompt the user. Setting modal blocks the dialog until it is closed. We
    // use rv to pass our return value.
    let rv = { value: CANCEL, items: items, action: aAction };
    window.openDialog(
      "chrome://calendar/content/calendar-occurrence-prompt.xul",
      "PromptOccurrenceModification",
      "centerscreen,chrome,modal,titlebar",
      rv
    );
    type = rv.value;
  }

  switch (type) {
    case MODIFY_PARENT:
      pastItems = items.map(item => item.parentItem);
      break;
    case MODIFY_FOLLOWING:
      // TODO tbd in a different bug
      throw Cr.NS_ERROR_NOT_IMPLEMENTED;
    case MODIFY_OCCURRENCE:
      pastItems = items;
      break;
    case CANCEL:
      // Since we have not set past or futureItem, the return below will
      // take care.
      break;
  }
  if (aItem) {
    returnItem = Array.isArray(aItem) ? pastItems : pastItems[0];
  }
  return [returnItem, futureItems, type];
}

// Undo/Redo code

/**
 * Helper to return the transaction manager service.
 *
 * @return      The calITransactionManager service.
 */
function getTransactionMgr() {
  return Cc["@mozilla.org/calendar/transactionmanager;1"].getService(Ci.calITransactionManager);
}

/**
 * Create and commit a transaction with the given arguments to the transaction
 * manager. Also updates the undo/redo menu.
 *
 * @see                 calITransactionManager
 * @param aAction       The action to do.
 * @param aItem         The new item to add/modify/delete
 * @param aCalendar     The calendar to do the transaction on
 * @param aOldItem      (optional) some actions require an old item
 * @param aListener     (optional) the listener to call when complete.
 * @param aExtResponse  (optional) JS object with additional parameters for sending itip messages
 *                                 (see also description of checkAndSend in calItipUtils.jsm)
 */
function doTransaction(aAction, aItem, aCalendar, aOldItem, aListener, aExtResponse = null) {
  // This is usually a user-initiated transaction, so make sure the calendar
  // this transaction is happening on is visible.
  ensureCalendarVisible(aCalendar);

  // Now use the transaction manager to execute the action
  getTransactionMgr().createAndCommitTxn(
    aAction,
    aItem,
    aCalendar,
    aOldItem,
    aListener ? aListener : null,
    aExtResponse
  );
  updateUndoRedoMenu();
}

/**
 * Undo the last operation done through the transaction manager.
 */
function undo() {
  if (canUndo()) {
    getTransactionMgr().undo();
    updateUndoRedoMenu();
  }
}

/**
 * Redo the last undone operation in the transaction manager.
 */
function redo() {
  if (canRedo()) {
    getTransactionMgr().redo();
    updateUndoRedoMenu();
  }
}

/**
 * Start a batch transaction on the transaction manager. Can be called multiple
 * times, which nests transactions.
 */
function startBatchTransaction() {
  getTransactionMgr().beginBatch();
}

/**
 * End a previously started batch transaction. NOTE: be sure to call this in a
 * try-catch-finally-block in case you have code that could fail between
 * startBatchTransaction and this call.
 */
function endBatchTransaction() {
  getTransactionMgr().endBatch();
  updateUndoRedoMenu();
}

/**
 * Checks if the last operation can be undone (or if there is a last operation
 * at all).
 */
function canUndo() {
  return getTransactionMgr().canUndo();
}

/**
 * Checks if the last undone operation can be redone.
 */
function canRedo() {
  return getTransactionMgr().canRedo();
}

/**
 * Update the undo and redo commands.
 */
function updateUndoRedoMenu() {
  goUpdateCommand("cmd_undo");
  goUpdateCommand("cmd_redo");
}

/**
 * Updates the partstat of the calendar owner for specified items triggered by a
 * context menu operation
 *
 * For a documentation of the expected bahaviours for  different use cases of
 * dealing with context menu partstat actions, see also setupAttendanceMenu(...)
 * in calendar-ui-utils.js
 *
 * @param {EventTarget}  aTarget   the target of the triggering event
 * @param {Array}        aItems    an array of calEvent or calIToDo items
 */
function setContextPartstat(aTarget, aItems) {
  /**
   * Provides the participation representing the user for a provided item
   *
   * @param   {calEvent|calTodo}  aItem  The calendar item to inspect
   * @returns {?calIAttendee}            An calIAttendee object or null if no
   *                                       participant was detected
   */
  function getParticipant(aItem) {
    let party = null;
    if (cal.itip.isInvitation(aItem)) {
      party = cal.itip.getInvitedAttendee(aItem);
    } else if (aItem.organizer && aItem.getAttendees({}).length) {
      let calOrgId = aItem.calendar.getProperty("organizerId");
      if (calOrgId.toLowerCase() == aItem.organizer.id.toLowerCase()) {
        party = aItem.organizer;
      }
    }
    return party;
  }

  startBatchTransaction();
  try {
    // TODO: make sure we overwrite the partstat of all occurrences in
    // the selection, if the partstat of the respective master item is
    // changed - see matrix in the doc block of setupAttendanceMenu(...)
    // in calendar-ui-utils.js

    for (let oldItem of aItems) {
      // Skip this item if its calendar is read only.
      if (oldItem.calendar.readOnly) {
        continue;
      }
      if (aTarget.getAttribute("scope") == "all-occurrences") {
        oldItem = oldItem.parentItem;
      }
      let attendee = getParticipant(oldItem);
      if (attendee) {
        // skip this item if the partstat for the participant hasn't
        // changed. otherwise we would always perform update operations
        // for recurring events on both, the master and the occurrence
        // item
        let partStat = aTarget.getAttribute("respvalue");
        if (attendee.participationStatus == partStat) {
          continue;
        }

        let newItem = oldItem.clone();
        let newAttendee = attendee.clone();
        newAttendee.participationStatus = partStat;
        if (newAttendee.isOrganizer) {
          newItem.organizer = newAttendee;
        } else {
          newItem.removeAttendee(attendee);
          newItem.addAttendee(newAttendee);
        }

        let extResponse = null;
        if (aTarget.hasAttribute("respmode")) {
          let mode = aTarget.getAttribute("respmode");
          let itipMode = Ci.calIItipItem[mode];
          extResponse = { responseMode: itipMode };
        }

        doTransaction("modify", newItem, newItem.calendar, oldItem, null, extResponse);
      }
    }
  } catch (e) {
    cal.ERROR("Error setting partstat: " + e + "\r\n");
  } finally {
    endBatchTransaction();
  }
}
