/* 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 promptDeleteCalendar, loadCalendarManager, unloadCalendarManager,
 *         updateSortOrderPref, calendarListTooltipShowing,
 *         calendarListSetupContextMenu, ensureCalendarVisible, toggleCalendarVisible,
 *         showAllCalendars, showOnlyCalendar, openCalendarSubscriptionsDialog,
 *         calendarOfflineManager
 */

/* import-globals-from dialogs/calendar-migration-dialog.js */
/* import-globals-from calendar-common-sets.js */
/* import-globals-from calendar-ui-utils.js */

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

/**
 * Get this window's currently selected calendar.
 *
 * @return      The currently selected calendar.
 */
function getSelectedCalendar() {
  return cal.view.getCompositeCalendar(window).defaultCalendar;
}

/**
 * Deletes the passed calendar, prompting the user if he really wants to do
 * this. If there is only one calendar left, no calendar is removed and the user
 * is not prompted.
 *
 * @param aCalendar     The calendar to delete.
 */
function promptDeleteCalendar(aCalendar) {
  const nIPS = Ci.nsIPromptService;
  const cICM = Ci.calICalendarManager;

  let calMgr = cal.getCalendarManager();
  let calendars = calMgr.getCalendars({});
  if (calendars.length <= 1) {
    // If this is the last calendar, don't delete it.
    return;
  }

  let modes = new Set(aCalendar.getProperty("capabilities.removeModes") || ["unsubscribe"]);
  let title = cal.l10n.getCalString("removeCalendarTitle");

  let textKey, b0text, b2text;
  let removeFlags = 0;
  let promptFlags =
    nIPS.BUTTON_POS_0 * nIPS.BUTTON_TITLE_IS_STRING + nIPS.BUTTON_POS_1 * nIPS.BUTTON_TITLE_CANCEL;

  if (modes.has("delete") && !modes.has("unsubscribe")) {
    textKey = "removeCalendarMessageDelete";
    promptFlags += nIPS.BUTTON_DELAY_ENABLE;
    b0text = cal.l10n.getCalString("removeCalendarButtonDelete");
  } else if (modes.has("delete")) {
    textKey = "removeCalendarMessageDeleteOrUnsubscribe";
    promptFlags += nIPS.BUTTON_POS_2 * nIPS.BUTTON_TITLE_IS_STRING;
    b0text = cal.l10n.getCalString("removeCalendarButtonUnsubscribe");
    b2text = cal.l10n.getCalString("removeCalendarButtonDelete");
  } else if (modes.has("unsubscribe")) {
    textKey = "removeCalendarMessageUnsubscribe";
    removeFlags |= cICM.REMOVE_NO_DELETE;
    b0text = cal.l10n.getCalString("removeCalendarButtonUnsubscribe");
  } else {
    return;
  }

  let text = cal.l10n.getCalString(textKey, [aCalendar.name]);
  let res = Services.prompt.confirmEx(
    window,
    title,
    text,
    promptFlags,
    b0text,
    null,
    b2text,
    null,
    {}
  );

  if (res != 1) {
    // Not canceled
    if (textKey == "removeCalendarMessageDeleteOrUnsubscribe" && res == 0) {
      // Both unsubscribing and deleting is possible, but unsubscribing was
      // requested. Make sure no delete is executed.
      removeFlags |= cICM.REMOVE_NO_DELETE;
    }

    calMgr.removeCalendar(aCalendar, removeFlags);
  }
}

/**
 * Called to initialize the calendar manager for a window.
 */
async function loadCalendarManager() {
  // Set up the composite calendar in the calendar list widget.
  let tree = document.getElementById("calendar-list-tree-widget");
  let compositeCalendar = cal.view.getCompositeCalendar(window);
  if (!tree.__lookupSetter__("compositeCalendar")) {
    await new Promise(resolve => tree.addEventListener("bindingattached", resolve, { once: true }));
  }
  tree.compositeCalendar = compositeCalendar;

  // Initialize our composite observer
  compositeCalendar.addObserver(compositeObserver);

  // Create the home calendar if no calendar exists.
  let calendars = cal.getCalendarManager().getCalendars({});
  if (calendars.length) {
    // migration code to make sure calendars, which do not support caching have cache enabled
    // required to further clean up on top of bug 1182264
    for (let calendar of calendars) {
      if (
        calendar.getProperty("cache.supported") === false &&
        calendar.getProperty("cache.enabled") === true
      ) {
        calendar.deleteProperty("cache.enabled");
      }
    }
  } else {
    initHomeCalendar();
  }
}

/**
 * Creates the initial "Home" calendar if no calendar exists.
 */
function initHomeCalendar() {
  let calMgr = cal.getCalendarManager();
  let composite = cal.view.getCompositeCalendar(window);
  let url = Services.io.newURI("moz-storage-calendar://");
  let homeCalendar = calMgr.createCalendar("storage", url);
  homeCalendar.name = cal.l10n.getCalString("homeCalendarName");
  calMgr.registerCalendar(homeCalendar);
  Services.prefs.setStringPref("calendar.list.sortOrder", homeCalendar.id);
  composite.addCalendar(homeCalendar);

  // Wrapping this in a try/catch block, as if any of the migration code
  // fails, the app may not load.
  if (Services.prefs.getBoolPref("calendar.migrator.enabled", true)) {
    try {
      gDataMigrator.checkAndMigrate();
    } catch (e) {
      Cu.reportError("Migrator error: " + e);
    }
  }

  return homeCalendar;
}

/**
 * Called to clean up the calendar manager for a window.
 */
function unloadCalendarManager() {
  let compositeCalendar = cal.view.getCompositeCalendar(window);
  compositeCalendar.setStatusObserver(null, null);
  compositeCalendar.removeObserver(compositeObserver);
}

/**
 * Updates the sort order preference based on the given event. The event is a
 * "SortOrderChanged" event, emitted from the calendar-list-tree binding. You
 * can also pass in an object like { sortOrder: "Space separated calendar ids" }
 *
 * @param event     The SortOrderChanged event described above.
 */
function updateSortOrderPref(event) {
  let sortOrderString = event.sortOrder.join(" ");
  Services.prefs.setStringPref("calendar.list.sortOrder", sortOrderString);
  try {
    Services.prefs.savePrefFile(null);
  } catch (e) {
    cal.ERROR(e);
  }
}

/**
 * Handler function to call when the tooltip is showing on the calendar list.
 *
 * @param event     The DOM event provoked by the tooltip showing.
 */
function calendarListTooltipShowing(event) {
  let tree = document.getElementById("calendar-list-tree-widget");
  let calendar = tree.getCalendarFromEvent(event);
  let tooltipText = false;
  if (calendar) {
    let currentStatus = calendar.getProperty("currentStatus");
    if (!Components.isSuccessCode(currentStatus)) {
      tooltipText = cal.l10n.getCalString("tooltipCalendarDisabled", [calendar.name]);
    } else if (calendar.readOnly) {
      tooltipText = cal.l10n.getCalString("tooltipCalendarReadOnly", [calendar.name]);
    }
  }
  setElementValue("calendar-list-tooltip", tooltipText, "label");
  return tooltipText !== false;
}

/**
 * A handler called to set up the context menu on the calendar list.
 *
 * @param event         The DOM event that caused the context menu to open.
 * @return              Returns true if the context menu should be shown.
 */
function calendarListSetupContextMenu(event) {
  let col = {};
  let row = {};
  let calendar;
  let calendars = cal.getCalendarManager().getCalendars({});
  let treeNode = document.getElementById("calendar-list-tree-widget");
  let composite = cal.view.getCompositeCalendar(window);

  if (document.popupNode.localName == "tree") {
    // Using VK_APPS to open the context menu will target the tree
    // itself. In that case we won't have a client point even for
    // opening the context menu. The "target" element should then be the
    // selected calendar.
    row.value = treeNode.tree.currentIndex;

    col.value = treeNode.tree.columns.getColumnFor(treeNode.querySelector(".calendarname-treecol"));

    calendar = treeNode.getCalendar(row.value);
  } else {
    // Using the mouse, the context menu will open on the treechildren
    // element. Here we can use client points.
    calendar = treeNode.getCalendarFromEvent(event, col, row);
  }

  if (col.value && col.value.element.getAttribute("anonid") == "checkbox-treecol") {
    // Don't show the context menu if the checkbox was clicked.
    return false;
  }

  document.getElementById("list-calendars-context-menu").contextCalendar = calendar;

  // Only enable calendar search if there's actually the chance of finding something:
  let hasProviders = cal.getCalendarSearchService().getProviders({}).length < 1 && "true";
  setElementValue("list-calendars-context-find", hasProviders, "collapsed");

  if (calendar) {
    enableElement("list-calendars-context-edit");
    enableElement("list-calendars-context-publish");

    enableElement("list-calendars-context-togglevisible");
    setElementValue("list-calendars-context-togglevisible", false, "collapsed");
    let stringName = composite.getCalendarById(calendar.id) ? "hideCalendar" : "showCalendar";
    setElementValue(
      "list-calendars-context-togglevisible",
      cal.l10n.getCalString(stringName, [calendar.name]),
      "label"
    );
    let accessKey = document
      .getElementById("list-calendars-context-togglevisible")
      .getAttribute(composite.getCalendarById(calendar.id) ? "accesskeyhide" : "accesskeyshow");
    setElementValue("list-calendars-context-togglevisible", accessKey, "accesskey");

    enableElement("list-calendars-context-showonly");
    setElementValue("list-calendars-context-showonly", false, "collapsed");
    setElementValue(
      "list-calendars-context-showonly",
      cal.l10n.getCalString("showOnlyCalendar", [calendar.name]),
      "label"
    );

    setupDeleteMenuitem("list-calendars-context-delete", calendar);
    // Only enable the delete calendars item if there is more than one
    // calendar. We don't want to have the last calendar deleted.
    setElementValue("list-calendars-context-delete", calendars.length < 2 && "true", "disabled");
  } else {
    disableElement("list-calendars-context-edit");
    disableElement("list-calendars-context-publish");
    disableElement("list-calendars-context-delete");
    disableElement("list-calendars-context-togglevisible");
    setElementValue("list-calendars-context-togglevisible", true, "collapsed");
    disableElement("list-calendars-context-showonly");
    setElementValue("list-calendars-context-showonly", true, "collapsed");
    setupDeleteMenuitem("list-calendars-context-delete", null);
  }
  return true;
}

/**
 * Changes the "delete calendar" menuitem to have the right label based on the
 * removeModes. The menuitem must have the attributes "labelremove",
 * "labeldelete" and "labelunsubscribe".
 *
 * @param aDeleteId     The id of the menuitem to delete the calendar
 */
function setupDeleteMenuitem(aDeleteId, aCalendar) {
  let calendar = aCalendar === undefined ? getSelectedCalendar() : aCalendar;
  let modes = new Set(
    calendar ? calendar.getProperty("capabilities.removeModes") || ["unsubscribe"] : []
  );

  let type = "remove";
  if (modes.has("delete") && !modes.has("unsubscribe")) {
    type = "delete";
  } else if (modes.has("unsubscribe") && !modes.has("delete")) {
    type = "unsubscribe";
  }

  let deleteItem = document.getElementById(aDeleteId);
  setElementValue(deleteItem, deleteItem.getAttribute("label" + type), "label");
  setElementValue(deleteItem, deleteItem.getAttribute("accesskey" + type), "accesskey");
  setElementValue(deleteItem, modes.size == 0 && "true", "disabled");
}

/**
 * Makes sure the passed calendar is visible to the user
 *
 * @param aCalendar   The calendar to make visible.
 */
function ensureCalendarVisible(aCalendar) {
  // We use the main window's calendar list to ensure that the calendar is visible.
  // If the main window has been closed this function may still be called,
  // like when an event/task window is still open and the user clicks 'save',
  // thus we have the extra checks.
  let list = document.getElementById("calendar-list-tree-widget");
  if (list && list.ensureCalendarVisible) {
    list.ensureCalendarVisible(aCalendar);
  }
}

/**
 * Hides the specified calendar if it is visible, or shows it if it is hidden.
 *
 * @param aCalendar   The calendar to show or hide
 */
function toggleCalendarVisible(aCalendar) {
  let composite = cal.view.getCompositeCalendar(window);
  if (composite.getCalendarById(aCalendar.id)) {
    composite.removeCalendar(aCalendar);
  } else {
    composite.addCalendar(aCalendar);
  }
}

/**
 * Shows all hidden calendars.
 */
function showAllCalendars() {
  let composite = cal.view.getCompositeCalendar(window);
  let cals = cal.getCalendarManager().getCalendars({});

  composite.startBatch();
  for (let calendar of cals) {
    if (!composite.getCalendarById(calendar.id)) {
      composite.addCalendar(calendar);
    }
  }
  composite.endBatch();
}

/**
 * Shows only the specified calendar, and hides all others.
 *
 * @param aCalendar   The calendar to show as the only visible calendar
 */
function showOnlyCalendar(aCalendar) {
  let composite = cal.view.getCompositeCalendar(window);
  let cals = composite.getCalendars({}) || [];

  composite.startBatch();
  for (let calendar of cals) {
    if (calendar.id != aCalendar.id) {
      composite.removeCalendar(calendar);
    }
  }
  composite.addCalendar(aCalendar);
  composite.endBatch();
}

var compositeObserver = {
  QueryInterface: cal.generateQI([Ci.calIObserver, Ci.calICompositeObserver]),

  onStartBatch: function() {},
  onEndBatch: function() {},
  onAddItem: function() {},
  onModifyItem: function() {},
  onDeleteItem: function() {},
  onError: function() {},
  onPropertyChanged: function() {},
  onPropertyDeleting: function() {},

  onLoad: function() {
    calendarUpdateNewItemsCommand();
    document.commandDispatcher.updateCommands("calendar_commands");
  },

  onCalendarAdded: function(aCalendar) {
    // Update the calendar commands for number of remote calendars and for
    // more than one calendar
    document.commandDispatcher.updateCommands("calendar_commands");
  },

  onCalendarRemoved: function(aCalendar) {
    // Update commands to disallow deleting the last calendar and only
    // allowing reload remote calendars when there are remote calendars.
    document.commandDispatcher.updateCommands("calendar_commands");
  },

  onDefaultCalendarChanged: function(aNewCalendar) {
    // A new default calendar may mean that the new calendar has different
    // ACLs. Make sure the commands are updated.
    calendarUpdateNewItemsCommand();
    document.commandDispatcher.updateCommands("calendar_commands");
  },
};

/**
 * Opens the subscriptions dialog modally.
 */
function openCalendarSubscriptionsDialog() {
  // the dialog will reset this to auto when it is done loading
  window.setCursor("wait");

  // open the dialog modally
  window.openDialog(
    "chrome://calendar/content/calendar-subscriptions-dialog.xul",
    "_blank",
    "chrome,titlebar,modal,resizable"
  );
}

/**
 * Calendar Offline Manager
 */
var calendarOfflineManager = {
  QueryInterface: ChromeUtils.generateQI([Ci.nsIObserver]),

  init: function() {
    if (this.initialized) {
      throw Cr.NS_ERROR_ALREADY_INITIALIZED;
    }
    Services.obs.addObserver(this, "network:offline-status-changed");

    this.updateOfflineUI(!this.isOnline());
    this.initialized = true;
  },

  uninit: function() {
    if (!this.initialized) {
      throw Cr.NS_ERROR_NOT_INITIALIZED;
    }
    Services.obs.removeObserver(this, "network:offline-status-changed");
    this.initialized = false;
  },

  isOnline: function() {
    return !Services.io.offline;
  },

  updateOfflineUI: function(aIsOffline) {
    // Refresh the current view
    currentView().goToDay(currentView().selectedDay);

    // Set up disabled locks for offline
    document.commandDispatcher.updateCommands("calendar_commands");
  },

  observe: function(aSubject, aTopic, aState) {
    if (aTopic == "network:offline-status-changed") {
      this.updateOfflineUI(aState == "offline");
    }
  },
};
