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

/* exported gInTab, gMainWindow, gTabmail, intializeTabOrWindowVariables,
 *          dispose, setDialogId, loadReminders, saveReminder,
 *          commonUpdateReminder, updateLink,
 *          adaptScheduleAgent, sendMailToOrganizer,
 *          openAttachmentFromItemSummary,
 */

/* import-globals-from ../item-editing/calendar-item-iframe.js */
/* import-globals-from ../calendar-ui-utils.js */

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

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

// Variables related to whether we are in a tab or a window dialog.
var gInTab = false;
var gMainWindow = null;
var gTabmail = null;

/**
 * Initialize variables for tab vs window.
 */
function intializeTabOrWindowVariables() {
  let args = window.arguments[0];
  gInTab = args.inTab;
  if (gInTab) {
    gTabmail = parent.document.getElementById("tabmail");
    gMainWindow = parent;
  } else {
    gMainWindow = parent.opener;
  }
}

/**
 * Dispose of controlling operations of this event dialog. Uses
 * window.arguments[0].job.dispose()
 */
function dispose() {
  let args = window.arguments[0];
  if (args.job && args.job.dispose) {
    args.job.dispose();
  }
}

/**
 * Sets the id of a Dialog to another value to allow different CSS styles
 * to be used.
 *
 * @param aDialog               The Dialog to be changed.
 * @param aNewId                The new ID as String.
 */
function setDialogId(aDialog, aNewId) {
  aDialog.setAttribute("id", aNewId);
  applyPersistedProperties(aDialog);
}

/**
 * Apply the persisted properties from xulstore.json on a dialog based on the current dialog id.
 * This needs to be invoked after changing a dialog id while loading to apply the values for the
 * new dialog id.
 *
 * @param aDialog               The Dialog to apply the property values for
 */
function applyPersistedProperties(aDialog) {
  let xulStore = Services.xulStore;
  // first we need to detect which properties are persisted
  let persistedProps = aDialog.getAttribute("persist") || "";
  if (persistedProps == "") {
    return;
  }
  let propNames = persistedProps.split(" ");
  let { outerWidth: width, outerHeight: height } = aDialog;
  let doResize = false;
  // now let's apply persisted values if applicable
  for (let propName of propNames) {
    if (xulStore.hasValue(aDialog.baseURI, aDialog.id, propName)) {
      let propValue = xulStore.getValue(aDialog.baseURI, aDialog.id, propName);
      if (propName == "width") {
        width = propValue;
        doResize = true;
      } else if (propName == "height") {
        height = propValue;
        doResize = true;
      } else {
        aDialog.setAttribute(propName, propValue);
      }
    }
  }

  if (doResize) {
    aDialog.ownerGlobal.resizeTo(width, height);
  }
}

/**
 * Create a calIAlarm from the given menuitem. The menuitem must have the
 * following attributes: unit, length, origin, relation.
 *
 * @param {Element} aMenuitem - The menuitem to create the alarm from.
 * @param {calICalendar} aCalendar - The calendar for getting the default alarm type.
 * @returns The calIAlarm with information from the menuitem.
 */
function createReminderFromMenuitem(aMenuitem, aCalendar) {
  let reminder = aMenuitem.reminder || new CalAlarm();
  // clone immutable reminders if necessary to set default values
  let isImmutable = !reminder.isMutable;
  if (isImmutable) {
    reminder = reminder.clone();
  }
  let offset = cal.createDuration();
  offset[aMenuitem.getAttribute("unit")] = aMenuitem.getAttribute("length");
  offset.normalize();
  offset.isNegative = aMenuitem.getAttribute("origin") == "before";
  reminder.related =
    aMenuitem.getAttribute("relation") == "START"
      ? Ci.calIAlarm.ALARM_RELATED_START
      : Ci.calIAlarm.ALARM_RELATED_END;
  reminder.offset = offset;
  reminder.action = getDefaultAlarmType(aCalendar);
  // make reminder immutable in case it was before
  if (isImmutable) {
    reminder.makeImmutable();
  }
  return reminder;
}

/**
 * This function opens the needed dialogs to edit the reminder. Note however
 * that calling this function from an extension is not recommended. To allow an
 * extension to open the reminder dialog, set the menulist "item-alarm" to the
 * custom menuitem and call updateReminder().
 *
 * @param {Element} reminderList - The reminder menu element.
 * @param {calIEvent | calIToDo} calendarItem - The calendar item.
 * @param {number} lastAlarmSelection - Index of previously selected item in the menu.
 * @param {calICalendar} calendar - The calendar to use.
 * @param {calITimezone} [timezone] - Timezone to use.
 */
function editReminder(
  reminderList,
  calendarItem,
  lastAlarmSelection,
  calendar,
  timezone = cal.dtz.defaultTimezone
) {
  let customItem = reminderList.querySelector(".reminder-custom-menuitem");

  let args = {
    reminders: customItem.reminders,
    item: calendarItem,
    timezone,
    calendar,
    // While these are "just" callbacks, the dialog is opened modally, so aside
    // from what's needed to set up the reminders, nothing else needs to be done.
    onOk(reminders) {
      customItem.reminders = reminders;
    },
    onCancel() {
      reminderList.selectedIndex = lastAlarmSelection;
    },
  };

  window.setCursor("wait");

  // open the dialog modally
  openDialog(
    "chrome://calendar/content/calendar-event-dialog-reminder.xhtml",
    "_blank",
    "chrome,titlebar,modal,resizable,centerscreen",
    args
  );
}

/**
 * Update the reminder details from the selected alarm. This shows a string
 * describing the reminder set, or nothing in case a preselected reminder was
 * chosen.
 *
 * @param {Element} reminderDetails - The reminder details element.
 * @param {Element} reminderList - The reminder menu element.
 * @param {calICalendar} calendar - The calendar.
 */
function updateReminderDetails(reminderDetails, reminderList, calendar) {
  // find relevant elements in the document
  let reminderMultipleLabel = reminderDetails.querySelector(".reminder-multiple-alarms-label");
  let iconBox = reminderDetails.querySelector(".alarm-icons-box");
  let reminderSingleLabel = reminderDetails.querySelector(".reminder-single-alarms-label");

  let reminders = reminderList.querySelector(".reminder-custom-menuitem").reminders || [];

  let actionValues = calendar.getProperty("capabilities.alarms.actionValues") || ["DISPLAY"];
  let actionMap = {};
  for (let action of actionValues) {
    actionMap[action] = true;
  }

  // Filter out any unsupported action types.
  reminders = reminders.filter(x => x.action in actionMap);

  if (reminderList.value == "custom") {
    // Depending on how many alarms we have, show either the "Multiple Alarms"
    // label or the single reminder label.
    reminderMultipleLabel.hidden = reminders.length < 2;
    reminderSingleLabel.hidden = reminders.length > 1;

    cal.alarms.addReminderImages(iconBox, reminders);

    // If there is only one reminder, display the reminder string
    if (reminders.length == 1) {
      reminderSingleLabel.value = reminders[0].toString(window.calendarItem);
    }
  } else {
    reminderMultipleLabel.setAttribute("hidden", "true");
    reminderSingleLabel.setAttribute("hidden", "true");
    if (reminderList.value == "none") {
      // No reminder selected means show no icons.
      while (iconBox.lastChild) {
        iconBox.lastChild.remove();
      }
    } else {
      // This is one of the predefined dropdown items. We should show a
      // single icon in the icons box to tell the user what kind of alarm
      // this will be.
      let mockAlarm = new CalAlarm();
      mockAlarm.action = getDefaultAlarmType(calendar);
      cal.alarms.addReminderImages(iconBox, [mockAlarm]);
    }
  }
}

/**
 * Check whether a reminder matches one of the default menu items or not.
 *
 * @param {calIAlarm} reminder - The reminder to match to a menu item.
 * @param {Element} reminderList - The reminder menu element.
 * @param {calICalendar} calendar - The current calendar, to get the default alarm type.
 * @returns {boolean} True if the reminder matches a menu item, false if not.
 */
function matchCustomReminderToMenuitem(reminder, reminderList, calendar) {
  let defaultAlarmType = getDefaultAlarmType(calendar);
  let reminderPopup = reminderList.menupopup;
  if (
    reminder.related != Ci.calIAlarm.ALARM_RELATED_ABSOLUTE &&
    reminder.offset &&
    reminder.action == defaultAlarmType
  ) {
    // Exactly one reminder that's not absolute, we may be able to match up
    // popup items.
    let relation = reminder.related == Ci.calIAlarm.ALARM_RELATED_START ? "START" : "END";

    // If the time duration for offset is 0, means the reminder is '0 minutes before'
    let origin = reminder.offset.inSeconds == 0 || reminder.offset.isNegative ? "before" : "after";

    let unitMap = {
      days: 86400,
      hours: 3600,
      minutes: 60,
    };

    for (let menuitem of reminderPopup.children) {
      if (
        menuitem.localName == "menuitem" &&
        menuitem.hasAttribute("length") &&
        menuitem.getAttribute("origin") == origin &&
        menuitem.getAttribute("relation") == relation
      ) {
        let unitMult = unitMap[menuitem.getAttribute("unit")] || 1;
        let length = menuitem.getAttribute("length") * unitMult;

        if (Math.abs(reminder.offset.inSeconds) == length) {
          menuitem.reminder = reminder.clone();
          reminderList.selectedItem = menuitem;
          // We've selected an item, so we are done here.
          return true;
        }
      }
    }
  }

  return false;
}

/**
 * Load an item's reminders into the dialog.
 *
 * @param {calIAlarm[]} reminders - An array of alarms to load.
 * @param {Element} reminderList - The reminders menulist element.
 * @param {calICalendar} calendar - The calendar the item belongs to.
 * @returns {number} Index of the selected item in reminders menu.
 */
function loadReminders(reminders, reminderList, calendar) {
  // Select 'no reminder' by default.
  reminderList.selectedIndex = 0;

  if (!reminders || !reminders.length) {
    // No reminders selected, we are done
    return reminderList.selectedIndex;
  }

  if (
    reminders.length > 1 ||
    !matchCustomReminderToMenuitem(reminders[0], reminderList, calendar)
  ) {
    // If more than one alarm is selected, or we didn't find a matching item
    // above, then select the "custom" item and attach the item's reminders to
    // it.
    reminderList.value = "custom";
    reminderList.querySelector(".reminder-custom-menuitem").reminders = reminders;
  }

  // Return the selected index so it can be remembered.
  return reminderList.selectedIndex;
}

/**
 * Save the selected reminder into the passed item.
 *
 * @param {calIEvent | calITodo} item   The calendar item to save the reminder into.
 * @param {calICalendar} calendar - The current calendar.
 * @param {Element} reminderList - The reminder menu element.
 */
function saveReminder(item, calendar, reminderList) {
  // We want to compare the old alarms with the new ones. If these are not
  // the same, then clear the snooze/dismiss times
  let oldAlarmMap = {};
  for (let alarm of item.getAlarms()) {
    oldAlarmMap[alarm.icalString] = true;
  }

  // Clear the alarms so we can add our new ones.
  item.clearAlarms();

  if (reminderList.value != "none") {
    let menuitem = reminderList.selectedItem;
    let reminders;

    if (menuitem.reminders) {
      // Custom reminder entries carry their own reminder object with
      // them. Make sure to clone in case these are the original item's
      // reminders.

      // XXX do we need to clone here?
      reminders = menuitem.reminders.map(x => x.clone());
    } else {
      // Pre-defined entries specify the necessary information
      // as attributes attached to the menuitem elements.
      reminders = [createReminderFromMenuitem(menuitem, calendar)];
    }

    let alarmCaps = item.calendar.getProperty("capabilities.alarms.actionValues") || ["DISPLAY"];
    let alarmActions = {};
    for (let action of alarmCaps) {
      alarmActions[action] = true;
    }

    // Make sure only alarms are saved that work in the given calendar.
    reminders.filter(x => x.action in alarmActions).forEach(item.addAlarm, item);
  }

  // Compare alarms to see if something changed.
  for (let alarm of item.getAlarms()) {
    let ics = alarm.icalString;
    if (ics in oldAlarmMap) {
      // The new alarm is also in the old set, remember this
      delete oldAlarmMap[ics];
    } else {
      // The new alarm is not in the old set, this means the alarms
      // differ and we can break out.
      oldAlarmMap[ics] = true;
      break;
    }
  }

  // If the alarms differ, clear the snooze/dismiss properties
  if (Object.keys(oldAlarmMap).length > 0) {
    let cmp = "X-MOZ-SNOOZE-TIME";

    // Recurring item alarms potentially have more snooze props, remove them
    // all.
    let propsToDelete = [];
    for (let [name] of item.properties) {
      if (name.startsWith(cmp)) {
        propsToDelete.push(name);
      }
    }

    item.alarmLastAck = null;
    propsToDelete.forEach(item.deleteProperty, item);
  }
}

/**
 * Get the default alarm type for the currently selected calendar. If the
 * calendar supports DISPLAY alarms, this is the default. Otherwise it is the
 * first alarm action the calendar supports.
 *
 * @param {calICalendar} calendar - The calendar to use.
 * @returns {string} The default alarm type.
 */
function getDefaultAlarmType(calendar) {
  let alarmCaps = calendar.getProperty("capabilities.alarms.actionValues") || ["DISPLAY"];
  return alarmCaps.includes("DISPLAY") ? "DISPLAY" : alarmCaps[0];
}

/**
 * Common update functions for both event dialogs. Called when a reminder has
 * been selected from the menulist.
 *
 * @param {Element} reminderList - The reminders menu element.
 * @param {calIEvent | calITodo} calendarItem - The calendar item.
 * @param {number} lastAlarmSelection - Index of the previous selection in the reminders menu.
 * @param {Element} reminderDetails - The reminder details element.
 * @param {calITimezone} timezone - The relevant timezone.
 * @param {boolean} suppressDialogs - If true, controls are updated without prompting
 *                                    for changes with the dialog
 * @returns {number} Index of the item selected in the reminders menu.
 */
function commonUpdateReminder(
  reminderList,
  calendarItem,
  lastAlarmSelection,
  calendar,
  reminderDetails,
  timezone,
  suppressDialogs
) {
  // if a custom reminder has been selected, we show the appropriate
  // dialog in order to allow the user to specify the details.
  // the result will be placed in the 'reminder-custom-menuitem' tag.
  if (reminderList.value == "custom") {
    // Clear the reminder icons first, this will make sure that while the
    // dialog is open the default reminder image is not shown which may
    // confuse users.
    let iconBox = reminderDetails.querySelector(".alarm-icons-box");
    while (iconBox.lastChild) {
      iconBox.lastChild.remove();
    }

    // show the dialog. This call blocks until the dialog is closed. Don't
    // pop up the dialog if aSuppressDialogs was specified or if this
    // happens during initialization of the dialog
    if (!suppressDialogs && reminderList.hasAttribute("last-value")) {
      editReminder(reminderList, calendarItem, lastAlarmSelection, calendar, timezone);
    }

    if (reminderList.value == "custom") {
      // Only do this if the 'custom' item is still selected. If the edit
      // reminder dialog was canceled then the previously selected
      // menuitem is selected, which may not be the custom menuitem.

      // If one or no reminders were selected, we have a chance of mapping
      // them to the existing elements in the dropdown.
      let customItem = reminderList.selectedItem;
      if (customItem.reminders.length == 0) {
        // No reminder was selected
        reminderList.value = "none";
      } else if (customItem.reminders.length == 1) {
        // We might be able to match the custom reminder with one of the
        // default menu items.
        matchCustomReminderToMenuitem(customItem.reminders[0], reminderList, calendar);
      }
    }
  }

  reminderList.setAttribute("last-value", reminderList.value);

  // possibly the selected reminder conflicts with the item.
  // for example an end-relation combined with a task without duedate
  // is an invalid state we need to take care of. we take the same
  // approach as with recurring tasks. in case the reminder is related
  // to the entry date we check the entry date automatically and disable
  // the checkbox. the same goes for end related reminder and the due date.
  if (calendarItem.isTodo()) {
    // In general, (re-)enable the due/entry checkboxes. This will be
    // changed in case the alarms are related to START/END below.
    enableElementWithLock("todo-has-duedate", "reminder-lock");
    enableElementWithLock("todo-has-entrydate", "reminder-lock");

    let menuitem = reminderList.selectedItem;
    if (menuitem.value != "none") {
      // In case a reminder is selected, retrieve the array of alarms from
      // it, or create one from the currently selected menuitem.
      let reminders = menuitem.reminders || [createReminderFromMenuitem(menuitem, calendar)];

      // If a reminder is related to the entry date...
      if (reminders.some(x => x.related == Ci.calIAlarm.ALARM_RELATED_START)) {
        // ...automatically check 'has entrydate'.
        if (!document.getElementById("todo-has-entrydate").checked) {
          document.getElementById("todo-has-entrydate").checked = true;

          // Make sure gStartTime is properly initialized
          updateEntryDate();
        }

        // Disable the checkbox to indicate that we need the entry-date.
        disableElementWithLock("todo-has-entrydate", "reminder-lock");
      }

      // If a reminder is related to the due date...
      if (reminders.some(x => x.related == Ci.calIAlarm.ALARM_RELATED_END)) {
        // ...automatically check 'has duedate'.
        if (!document.getElementById("todo-has-duedate").checked) {
          document.getElementById("todo-has-duedate").checked = true;

          // Make sure gStartTime is properly initialized
          updateDueDate();
        }

        // Disable the checkbox to indicate that we need the entry-date.
        disableElementWithLock("todo-has-duedate", "reminder-lock");
      }
    }
  }
  updateReminderDetails(reminderDetails, reminderList, calendar);

  // Return the current reminder drop down selection index so it can be remembered.
  return reminderList.selectedIndex;
}

/**
 * Updates the related link on the dialog. Currently only used by the
 * read-only summary dialog.
 *
 * @param {string} itemUrlString - The calendar item URL as a string.
 * @param {Element} linkRow - The row containing the link.
 * @param {Element} urlLink - The link element itself.
 */
function updateLink(itemUrlString, linkRow, urlLink) {
  let linkCommand = document.getElementById("cmd_toggle_link");

  if (linkCommand) {
    // Disable if there is no url.
    linkCommand.disabled = !itemUrlString;
  }

  if ((linkCommand && linkCommand.getAttribute("checked") != "true") || !itemUrlString.length) {
    // Hide if there is no url, or the menuitem was chosen so that the url
    // should be hidden
    linkRow.hidden = true;
  } else {
    let handler, uri;
    try {
      uri = Services.io.newURI(itemUrlString);
      handler = Services.io.getProtocolHandler(uri.scheme);
    } catch (e) {
      // No protocol handler for the given protocol, or invalid uri
      linkRow.hidden = true;
      return;
    }

    // Only show if its either an internal protocol handler, or its external
    // and there is an external app for the scheme
    handler = cal.wrapInstance(handler, Ci.nsIExternalProtocolHandler);
    let show = !handler || handler.externalAppExistsForScheme(uri.scheme);
    linkRow.hidden = !show;

    setTimeout(() => {
      // HACK the url link doesn't crop when setting the value in onLoad
      urlLink.setAttribute("value", itemUrlString);
      urlLink.setAttribute("href", itemUrlString);
    }, 0);
  }
}

/**
 * Adapts the scheduling responsibility for caldav servers according to RfC 6638
 * based on forceEmailScheduling preference for the respective calendar
 *
 * @param {calIEvent|calIToDo} aItem - Item to apply the change on
 */
function adaptScheduleAgent(aItem) {
  if (
    aItem.calendar &&
    aItem.calendar.type == "caldav" &&
    aItem.calendar.getProperty("capabilities.autoschedule.supported")
  ) {
    let identity = aItem.calendar.getProperty("imip.identity");
    let orgEmail = identity && identity.QueryInterface(Ci.nsIMsgIdentity).email;
    let organizerAction = aItem.organizer && orgEmail && aItem.organizer.id == "mailto:" + orgEmail;
    if (aItem.calendar.getProperty("forceEmailScheduling")) {
      cal.LOG("Enforcing clientside email based scheduling.");
      // for attendees, we change schedule-agent only in case of an
      // organizer triggered action
      if (organizerAction) {
        aItem.getAttendees().forEach(aAttendee => {
          // overwriting must always happen consistently for all
          // attendees regarding SERVER or CLIENT but must not override
          // e.g. NONE, so we only overwrite if the param is set to
          // SERVER or doesn't exist
          if (
            aAttendee.getProperty("SCHEDULE-AGENT") == "SERVER" ||
            !aAttendee.getProperty("SCHEDULE-AGENT")
          ) {
            aAttendee.setProperty("SCHEDULE-AGENT", "CLIENT");
            aAttendee.deleteProperty("SCHEDULE-STATUS");
            aAttendee.deleteProperty("SCHEDULE-FORCE-SEND");
          }
        });
      } else if (
        aItem.organizer &&
        (aItem.organizer.getProperty("SCHEDULE-AGENT") == "SERVER" ||
          !aItem.organizer.getProperty("SCHEDULE-AGENT"))
      ) {
        // for organizer, we change the schedule-agent only in case of
        // an attendee triggered action
        aItem.organizer.setProperty("SCHEDULE-AGENT", "CLIENT");
        aItem.organizer.deleteProperty("SCHEDULE-STATUS");
        aItem.organizer.deleteProperty("SCHEDULE-FORCE-SEND");
      }
    } else if (organizerAction) {
      aItem.getAttendees().forEach(aAttendee => {
        if (aAttendee.getProperty("SCHEDULE-AGENT") == "CLIENT") {
          aAttendee.deleteProperty("SCHEDULE-AGENT");
        }
      });
    } else if (aItem.organizer && aItem.organizer.getProperty("SCHEDULE-AGENT") == "CLIENT") {
      aItem.organizer.deleteProperty("SCHEDULE-AGENT");
    }
  }
}

/**
 * Extracts the item's organizer and opens a compose window to send the
 * organizer an email.
 *
 * @param {calIEvent | calITodo} item - The calendar item.
 */
function sendMailToOrganizer(item) {
  let organizer = item.organizer;
  let email = cal.email.getAttendeeEmail(organizer, true);
  let emailSubject = cal.l10n.getString("calendar-event-dialog", "emailSubjectReply", [item.title]);
  let identity = item.calendar.getProperty("imip.identity");
  cal.email.sendTo(email, emailSubject, null, identity);
}

/**
 * Opens an attachment.
 *
 * @param {AUTF8String}  aAttachmentId   The hashId of the attachment to open.
 * @param {calIEvent | calITodo} item    The calendar item.
 */
function openAttachmentFromItemSummary(aAttachmentId, item) {
  if (!aAttachmentId) {
    return;
  }
  let attachments = item
    .getAttachments()
    .filter(aAttachment => aAttachment.hashId == aAttachmentId);

  if (attachments.length && attachments[0].uri && attachments[0].uri.spec != "about:blank") {
    Cc["@mozilla.org/uriloader/external-protocol-service;1"]
      .getService(Ci.nsIExternalProtocolService)
      .loadURI(attachments[0].uri);
  }
}
