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

/* globals addMenuItem, getItemsFromIcsFile, putItemsIntoCal,
           sortCalendarArray */

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

const gModel = {
  /** @type {calICalendar[]} */
  calendars: [],

  /** @type {Map(number -> calIItemBase)} */
  itemsToImport: new Map(),

  /** @type {nsIFile | null} */
  file: null,

  /** @type {Map(number -> CalendarItemSummary)} */
  itemSummaries: new Map(),
};

/**
 * Window load event handler.
 */
async function onWindowLoad() {
  // Workaround to add padding to the dialog buttons area which is in shadow dom.
  // If the padding value changes here it should also change in the CSS.
  let dialog = document.getElementsByTagName("dialog")[0];
  dialog.shadowRoot.querySelector(".dialog-button-box").style = "padding-inline: 10px;";

  gModel.file = window.arguments[0];
  document.getElementById("calendar-ics-file-dialog-file-path").value = gModel.file.path;

  let calendars = cal.manager.getCalendars();
  gModel.calendars = getCalendarsThatCanImport(calendars);
  if (!gModel.calendars.length) {
    // No calendars to import into. Show error dialog and close the window.
    cal.showError(await document.l10n.formatValue("calendar-ics-file-dialog-no-calendars"), window);
    window.close();
    return;
  }

  let composite = cal.view.getCompositeCalendar(window);
  let defaultCalendarId = composite && composite.defaultCalendar?.id;
  setUpCalendarMenu(gModel.calendars, defaultCalendarId);
  cal.view.colorTracker.registerWindow(window);

  // Finish laying out and displaying the window, then come back to do the hard work.
  Services.tm.dispatchToMainThread(async () => {
    let startTime = Date.now();

    getItemsFromIcsFile(gModel.file).forEach((item, index) => {
      gModel.itemsToImport.set(index, item);
    });
    if (gModel.itemsToImport.size == 0) {
      // No items to import, close the window. An error dialog has already been
      // shown by `getItemsFromIcsFile`.
      window.close();
      return;
    }

    // We know that if `getItemsFromIcsFile` took a long time, then `setUpItemSummaries` will also
    // take a long time. Show a loading message so the user knows something is happening.
    let loadingMessage = document.getElementById("calendar-ics-file-dialog-items-loading-message");
    if (Date.now() - startTime > 150) {
      loadingMessage.removeAttribute("hidden");
      await new Promise(resolve => requestAnimationFrame(resolve));
    }

    // Not much point filtering or sorting if there's only one event.
    if (gModel.itemsToImport.size == 1) {
      document.getElementById("calendar-ics-file-dialog-filters").collapsed = true;
    }

    await setUpItemSummaries();

    // Remove the loading message from the DOM to avoid it causing problems later.
    loadingMessage.remove();

    document.addEventListener("dialogaccept", importRemainingItems);
  });
}
window.addEventListener("load", onWindowLoad);

/**
 * Takes an array of calendars and returns a sorted array of the calendars
 * that can import items.
 *
 * @param {calICalendar[]} calendars - An array of calendars.
 * @returns {calICalendar[]} Sorted array of calendars that can import items.
 */
function getCalendarsThatCanImport(calendars) {
  let calendarsThatCanImport = calendars.filter(
    calendar =>
      !calendar.getProperty("disabled") &&
      !calendar.readOnly &&
      cal.acl.userCanAddItemsToCalendar(calendar)
  );
  return sortCalendarArray(calendarsThatCanImport);
}

/**
 * Add calendars to the calendar drop down menu, and select one.
 *
 * @param {calICalendar[]} calendars - An array of calendars.
 * @param {string | null} defaultCalendarId - ID of the default (currently selected) calendar.
 */
function setUpCalendarMenu(calendars, defaultCalendarId) {
  let menulist = document.getElementById("calendar-ics-file-dialog-calendar-menu");
  for (let calendar of calendars) {
    let menuitem = addMenuItem(menulist, calendar.name, calendar.name);
    let cssSafeId = cal.view.formatStringForCSSRule(calendar.id);
    menuitem.style.setProperty("--item-color", `var(--calendar-${cssSafeId}-backcolor)`);
    menuitem.classList.add("menuitem-iconic");
  }

  let index = defaultCalendarId
    ? calendars.findIndex(calendar => calendar.id == defaultCalendarId)
    : 0;

  menulist.selectedIndex = index == -1 ? 0 : index;
  updateCalendarMenu();
}

/**
 * Update to reflect a change in the selected calendar.
 */
function updateCalendarMenu() {
  let menulist = document.getElementById("calendar-ics-file-dialog-calendar-menu");
  menulist.style.setProperty(
    "--item-color",
    menulist.selectedItem.style.getPropertyValue("--item-color")
  );
}

/**
 * Display summaries of each calendar item from the file being imported.
 */
async function setUpItemSummaries() {
  let items = [...gModel.itemsToImport];
  let itemsContainer = document.getElementById("calendar-ics-file-dialog-items-container");

  // Sort the items, chronologically first, tasks without a date to the end,
  // then alphabetically.
  let collator = new Intl.Collator(undefined, { numeric: true });
  items.sort(([, a], [, b]) => {
    let aStartDate =
      a.startDate?.nativeTime ||
      a.entryDate?.nativeTime ||
      a.dueDate?.nativeTime ||
      Number.MAX_SAFE_INTEGER;
    let bStartDate =
      b.startDate?.nativeTime ||
      b.entryDate?.nativeTime ||
      b.dueDate?.nativeTime ||
      Number.MAX_SAFE_INTEGER;
    return aStartDate - bStartDate || collator.compare(a.title, b.title);
  });

  let [eventButtonText, taskButtonText] = await document.l10n.formatValues([
    "calendar-ics-file-dialog-import-event-button-label",
    "calendar-ics-file-dialog-import-task-button-label",
  ]);

  items.forEach(([index, item]) => {
    let itemFrame = document.createXULElement("vbox");
    itemFrame.classList.add("calendar-ics-file-dialog-item-frame");

    let importButton = document.createXULElement("button");
    importButton.classList.add("calendar-ics-file-dialog-item-import-button");
    importButton.setAttribute("label", item.isEvent() ? eventButtonText : taskButtonText);
    importButton.addEventListener("command", importSingleItem.bind(null, item, index));

    let buttonBox = document.createXULElement("hbox");
    buttonBox.setAttribute("pack", "end");
    buttonBox.setAttribute("align", "end");

    let summary = document.createXULElement("calendar-item-summary");
    summary.setAttribute("id", "import-item-summary-" + index);

    itemFrame.appendChild(summary);
    buttonBox.appendChild(importButton);
    itemFrame.appendChild(buttonBox);

    itemsContainer.appendChild(itemFrame);
    summary.item = item;

    summary.updateItemDetails();
    gModel.itemSummaries.set(index, summary);
  });
}

/**
 * Filter item summaries by search string.
 *
 * @param {searchString} [searchString] - Terms to search for.
 */
function filterItemSummaries(searchString = "") {
  let itemsContainer = document.getElementById("calendar-ics-file-dialog-items-container");

  searchString = searchString.trim();
  // Nothing to search for. Display all item summaries.
  if (!searchString) {
    gModel.itemSummaries.forEach(s => {
      s.closest(".calendar-ics-file-dialog-item-frame").hidden = false;
    });

    itemsContainer.scrollTo(0, 0);
    return;
  }

  searchString = searchString.toLowerCase().normalize();

  // Split the search string into tokens. Quoted strings are preserved.
  let searchTokens = [];
  let startIndex;
  while ((startIndex = searchString.indexOf('"')) != -1) {
    let endIndex = searchString.indexOf('"', startIndex + 1);
    if (endIndex == -1) {
      endIndex = searchString.length;
    }

    searchTokens.push(searchString.substring(startIndex + 1, endIndex));
    let query = searchString.substring(0, startIndex);
    if (endIndex < searchString.length) {
      query += searchString.substr(endIndex + 1);
    }

    searchString = query.trim();
  }

  if (searchString.length != 0) {
    searchTokens = searchTokens.concat(searchString.split(/\s+/));
  }

  // Check the title and description of each item for matches.
  gModel.itemSummaries.forEach(s => {
    let title, description;
    let matches = searchTokens.every(term => {
      if (title === undefined) {
        title = s.item.title.toLowerCase().normalize();
      }
      if (title?.includes(term)) {
        return true;
      }

      if (description === undefined) {
        description = s.item.getProperty("description")?.toLowerCase().normalize();
      }
      return description?.includes(term);
    });
    s.closest(".calendar-ics-file-dialog-item-frame").hidden = !matches;
  });

  itemsContainer.scrollTo(0, 0);
}

/**
 * Sort item summaries.
 *
 * @param {Event} event - The oncommand event that triggered this sort.
 */
function sortItemSummaries(event) {
  let [key, direction] = event.target.value.split(" ");

  let comparer;
  if (key == "title") {
    let collator = new Intl.Collator(undefined, { numeric: true });
    if (direction == "ascending") {
      comparer = (a, b) => collator.compare(a.item.title, b.item.title);
    } else {
      comparer = (a, b) => collator.compare(b.item.title, a.item.title);
    }
  } else if (key == "start") {
    if (direction == "ascending") {
      comparer = (a, b) => a.item.startDate.nativeTime - b.item.startDate.nativeTime;
    } else {
      comparer = (a, b) => b.item.startDate.nativeTime - a.item.startDate.nativeTime;
    }
  } else {
    // How did we get here?
    throw new Error(`Unexpected sort key: ${key}`);
  }

  let items = [...gModel.itemSummaries.values()].sort(comparer);
  let itemsContainer = document.getElementById("calendar-ics-file-dialog-items-container");
  for (let item of items) {
    itemsContainer.appendChild(item.closest(".calendar-ics-file-dialog-item-frame"));
  }
  itemsContainer.scrollTo(0, 0);

  for (let menuitem of document.querySelectorAll(
    "#calendar-ics-file-dialog-sort-popup > menuitem"
  )) {
    menuitem.checked = menuitem == event.target;
  }
}

/**
 * Get the currently selected calendar.
 *
 * @returns {calICalendar} The currently selected calendar.
 */
function getCurrentlySelectedCalendar() {
  let menulist = document.getElementById("calendar-ics-file-dialog-calendar-menu");
  let calendar = gModel.calendars[menulist.selectedIndex];
  return calendar;
}

/**
 * Handler for buttons that import a single item. The arguments are bound for
 * each button instance, except for the event argument.
 *
 * @param {calIItemBase} item - Calendar item.
 * @param {number} itemIndex - Index of the calendar item in the item array.
 * @param {string} filePath - Path to the file being imported.
 * @param {Event} event - The button event.
 */
async function importSingleItem(item, itemIndex, event) {
  let dialog = document.getElementsByTagName("dialog")[0];
  let acceptButton = dialog.getButton("accept");
  let cancelButton = dialog.getButton("cancel");

  acceptButton.disabled = true;
  cancelButton.disabled = true;

  let calendar = getCurrentlySelectedCalendar();

  await putItemsIntoCal(calendar, [item], {
    onDuplicate(item, error) {
      // TODO: CalCalendarManager already shows a not-very-useful error pop-up.
      // Once that is fixed, use this callback to display a proper error message.
    },
    onError(item, error) {
      // TODO: CalCalendarManager already shows a not-very-useful error pop-up.
      // Once that is fixed, use this callback to display a proper error message.
    },
  });

  event.target.closest(".calendar-ics-file-dialog-item-frame").remove();
  gModel.itemsToImport.delete(itemIndex);
  gModel.itemSummaries.delete(itemIndex);

  acceptButton.disabled = false;
  if (gModel.itemsToImport.size > 0) {
    // Change the cancel button label to Close, as we've done some work that
    // won't be cancelled.
    cancelButton.label = await document.l10n.formatValue(
      "calendar-ics-file-cancel-button-close-label"
    );
    cancelButton.disabled = false;
  } else {
    // No more items to import, remove the "Import All" option.
    document.removeEventListener("dialogaccept", importRemainingItems);

    cancelButton.hidden = true;
    acceptButton.label = await document.l10n.formatValue(
      "calendar-ics-file-accept-button-ok-label"
    );
  }
}

/**
 * "Import All" button command handler.
 *
 * @param {Event} event - Button command event.
 */
async function importRemainingItems(event) {
  event.preventDefault();

  let dialog = document.getElementsByTagName("dialog")[0];
  let acceptButton = dialog.getButton("accept");
  let cancelButton = dialog.getButton("cancel");

  acceptButton.disabled = true;
  cancelButton.disabled = true;

  let calendar = getCurrentlySelectedCalendar();
  let filteredSummaries = [...gModel.itemSummaries.values()].filter(
    summary => !summary.closest(".calendar-ics-file-dialog-item-frame").hidden
  );
  let remainingItems = filteredSummaries.map(summary => summary.item);

  let progressElement = document.getElementById("calendar-ics-file-dialog-progress");
  let duplicatesElement = document.getElementById("calendar-ics-file-dialog-duplicates-message");
  let errorsElement = document.getElementById("calendar-ics-file-dialog-errors-message");

  let optionsPane = document.getElementById("calendar-ics-file-dialog-options-pane");
  let progressPane = document.getElementById("calendar-ics-file-dialog-progress-pane");
  let resultPane = document.getElementById("calendar-ics-file-dialog-result-pane");

  let importListener = {
    count: 0,
    duplicatesCount: 0,
    errorsCount: 0,
    progressInterval: null,

    onStart() {
      progressElement.max = remainingItems.length;
      optionsPane.hidden = true;
      progressPane.hidden = false;

      this.progressInterval = setInterval(() => {
        progressElement.value = this.count;
      }, 50);
    },
    onDuplicate(item, error) {
      this.duplicatesCount++;
    },
    onError(item, error) {
      this.errorsCount++;
    },
    onProgress(count, total) {
      this.count = count;
    },
    async onEnd() {
      progressElement.value = this.count;
      clearInterval(this.progressInterval);

      document.l10n.setAttributes(duplicatesElement, "calendar-ics-file-import-duplicates", {
        duplicatesCount: this.duplicatesCount,
      });
      duplicatesElement.hidden = this.duplicatesCount == 0;
      document.l10n.setAttributes(errorsElement, "calendar-ics-file-import-errors", {
        errorsCount: this.errorsCount,
      });
      errorsElement.hidden = this.errorsCount == 0;

      let [acceptButtonLabel, cancelButtonLabel] = await document.l10n.formatValues([
        { id: "calendar-ics-file-accept-button-ok-label" },
        { id: "calendar-ics-file-cancel-button-close-label" },
      ]);

      filteredSummaries.forEach(summary => {
        let itemIndex = parseInt(summary.id.substring("import-item-summary-".length), 10);
        gModel.itemsToImport.delete(itemIndex);
        gModel.itemSummaries.delete(itemIndex);
        summary.closest(".calendar-ics-file-dialog-item-frame").remove();
      });

      document.getElementById("calendar-ics-file-dialog-search-input").value = "";
      filterItemSummaries();
      let itemsRemain = !!document.querySelector(".calendar-ics-file-dialog-item-frame");

      // An artificial delay so the progress pane doesn't appear then immediately disappear.
      setTimeout(() => {
        if (itemsRemain) {
          acceptButton.disabled = false;
          cancelButton.label = cancelButtonLabel;
          cancelButton.disabled = false;
        } else {
          acceptButton.label = acceptButtonLabel;
          acceptButton.disabled = false;
          cancelButton.hidden = true;
          document.removeEventListener("dialogaccept", importRemainingItems);
        }

        optionsPane.hidden = !itemsRemain;
        progressPane.hidden = true;
        resultPane.hidden = itemsRemain;
      }, 500);
    },
  };

  putItemsIntoCal(calendar, remainingItems, importListener);
}

/**
 * These functions are called via `putItemsIntoCal` in import-export.js so
 * they need to be defined in global scope but they don't need to do anything
 * in this case.
 */
function startBatchTransaction() {}
function endBatchTransaction() {}
