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

ChromeUtils.defineModuleGetter(
  this,
  "MailServices",
  "resource:///modules/MailServices.jsm"
);
ChromeUtils.defineESModuleGetters(this, {
  DeferredTask: "resource://gre/modules/DeferredTask.sys.mjs",
});

/**
 * Tracks folder events.
 *
 * @implements {nsIMsgFolderListener}
 */
var folderTracker = new (class extends EventEmitter {
  constructor() {
    super();
    this.listenerCount = 0;
    this.pendingInfoNotifications = new ExtensionUtils.DefaultMap(
      () => new Map()
    );
    this.deferredInfoNotifications = new ExtensionUtils.DefaultMap(
      folder =>
        new DeferredTask(
          () => this.emitPendingInfoNotification(folder),
          NOTIFICATION_COLLAPSE_TIME
        )
    );
  }

  on(...args) {
    super.on(...args);
    this.incrementListeners();
  }

  off(...args) {
    super.off(...args);
    this.decrementListeners();
  }

  incrementListeners() {
    this.listenerCount++;
    if (this.listenerCount == 1) {
      // nsIMsgFolderListener
      const flags =
        MailServices.mfn.folderAdded |
        MailServices.mfn.folderDeleted |
        MailServices.mfn.folderMoveCopyCompleted |
        MailServices.mfn.folderRenamed;
      MailServices.mfn.addListener(this, flags);
      // nsIFolderListener
      MailServices.mailSession.AddFolderListener(
        this,
        Ci.nsIFolderListener.intPropertyChanged
      );
    }
  }
  decrementListeners() {
    this.listenerCount--;
    if (this.listenerCount == 0) {
      MailServices.mfn.removeListener(this);
      MailServices.mailSession.RemoveFolderListener(this);
    }
  }

  // nsIFolderListener

  onFolderIntPropertyChanged(item, property, oldValue, newValue) {
    if (!(item instanceof Ci.nsIMsgFolder)) {
      return;
    }

    switch (property) {
      case "FolderFlag":
        if (
          (oldValue & Ci.nsMsgFolderFlags.Favorite) !=
          (newValue & Ci.nsMsgFolderFlags.Favorite)
        ) {
          this.addPendingInfoNotification(
            item,
            "favorite",
            !!(newValue & Ci.nsMsgFolderFlags.Favorite)
          );
        }
        break;
      case "TotalMessages":
        this.addPendingInfoNotification(item, "totalMessageCount", newValue);
        break;
      case "TotalUnreadMessages":
        this.addPendingInfoNotification(item, "unreadMessageCount", newValue);
        break;
    }
  }

  addPendingInfoNotification(folder, key, value) {
    // If there is already a notification entry, decide if it must be emitted,
    // or if it can be collapsed: Message count changes can be collapsed.
    // This also collapses multiple different notifications types into a
    // single event.
    if (
      ["favorite"].includes(key) &&
      this.deferredInfoNotifications.has(folder) &&
      this.pendingInfoNotifications.get(folder).has(key)
    ) {
      this.deferredInfoNotifications.get(folder).disarm();
      this.emitPendingInfoNotification(folder);
    }

    this.pendingInfoNotifications.get(folder).set(key, value);
    this.deferredInfoNotifications.get(folder).disarm();
    this.deferredInfoNotifications.get(folder).arm();
  }

  emitPendingInfoNotification(folder) {
    let folderInfo = this.pendingInfoNotifications.get(folder);
    if (folderInfo.size > 0) {
      this.emit(
        "folder-info-changed",
        convertFolder(folder),
        Object.fromEntries(folderInfo)
      );
      this.pendingInfoNotifications.delete(folder);
    }
  }

  // nsIMsgFolderListener

  folderAdded(childFolder) {
    this.emit("folder-created", convertFolder(childFolder));
  }
  folderDeleted(oldFolder) {
    // Deleting an account, will trigger delete notifications for its folders,
    // but the account lookup fails, so skip them.
    let server = oldFolder.server;
    let account = MailServices.accounts.FindAccountForServer(server);
    if (account) {
      this.emit("folder-deleted", convertFolder(oldFolder, account.key));
    }
  }
  folderMoveCopyCompleted(move, srcFolder, targetFolder) {
    // targetFolder is not the copied/moved folder, but its parent. Find the
    // actual folder by its name (which is unique).
    let dstFolder = null;
    if (targetFolder && targetFolder.hasSubFolders) {
      dstFolder = targetFolder.subFolders.find(
        f => f.prettyName == srcFolder.prettyName
      );
    }

    if (move) {
      this.emit(
        "folder-moved",
        convertFolder(srcFolder),
        convertFolder(dstFolder)
      );
    } else {
      this.emit(
        "folder-copied",
        convertFolder(srcFolder),
        convertFolder(dstFolder)
      );
    }
  }
  folderRenamed(oldFolder, newFolder) {
    this.emit(
      "folder-renamed",
      convertFolder(oldFolder),
      convertFolder(newFolder)
    );
  }
})();

/**
 * Accepts a MailFolder or a MailAccount and returns the actual folder and its
 * accountId. Throws if the requested folder does not exist.
 */
function getFolder({ accountId, path, id }) {
  if (id && !path && !accountId) {
    accountId = id;
    path = "/";
  }

  let uri = folderPathToURI(accountId, path);
  let folder = MailServices.folderLookup.getFolderForURL(uri);
  if (!folder) {
    throw new ExtensionError(`Folder not found: ${path}`);
  }
  return { folder, accountId };
}

/**
 * Copy or Move a folder.
 */
async function doMoveCopyOperation(source, destination, isMove) {
  // The schema file allows destination to be either a MailFolder or a
  // MailAccount.
  let srcFolder = getFolder(source);
  let dstFolder = getFolder(destination);

  if (
    srcFolder.folder.server.type == "nntp" ||
    dstFolder.folder.server.type == "nntp"
  ) {
    throw new ExtensionError(
      `folders.${isMove ? "move" : "copy"}() is not supported in news accounts`
    );
  }

  if (
    dstFolder.folder.hasSubFolders &&
    dstFolder.folder.subFolders.find(
      f => f.prettyName == srcFolder.folder.prettyName
    )
  ) {
    throw new ExtensionError(
      `folders.${isMove ? "move" : "copy"}() failed, because ${
        srcFolder.folder.prettyName
      } already exists in ${folderURIToPath(
        dstFolder.accountId,
        dstFolder.folder.URI
      )}`
    );
  }

  let rv = await new Promise(resolve => {
    let _destination = null;
    const listener = {
      folderMoveCopyCompleted(_isMove, _srcFolder, _dstFolder) {
        if (
          _destination != null ||
          _isMove != isMove ||
          _srcFolder.URI != srcFolder.folder.URI ||
          _dstFolder.URI != dstFolder.folder.URI
        ) {
          return;
        }

        // The targetFolder is not the copied/moved folder, but its parent.
        // Find the actual folder by its name (which is unique).
        if (_dstFolder && _dstFolder.hasSubFolders) {
          _destination = _dstFolder.subFolders.find(
            f => f.prettyName == _srcFolder.prettyName
          );
        }
      },
    };
    MailServices.mfn.addListener(
      listener,
      MailServices.mfn.folderMoveCopyCompleted
    );
    MailServices.copy.copyFolder(
      srcFolder.folder,
      dstFolder.folder,
      isMove,
      {
        OnStartCopy() {},
        OnProgress() {},
        SetMessageKey() {},
        GetMessageId() {},
        OnStopCopy(status) {
          MailServices.mfn.removeListener(listener);
          resolve({
            status,
            folder: _destination,
          });
        },
      },
      null
    );
  });

  if (!Components.isSuccessCode(rv.status)) {
    throw new ExtensionError(
      `folders.${isMove ? "move" : "copy"}() failed for unknown reasons`
    );
  }

  return convertFolder(rv.folder, dstFolder.accountId);
}

/**
 * Wait for a folder operation.
 */
function waitForOperation(flags, uri) {
  return new Promise(resolve => {
    MailServices.mfn.addListener(
      {
        folderAdded(childFolder) {
          if (childFolder.parent.URI != uri) {
            return;
          }

          MailServices.mfn.removeListener(this);
          resolve(childFolder);
        },
        folderDeleted(oldFolder) {
          if (oldFolder.URI != uri) {
            return;
          }

          MailServices.mfn.removeListener(this);
          resolve();
        },
        folderMoveCopyCompleted(move, srcFolder, destFolder) {
          if (srcFolder.URI != uri) {
            return;
          }

          MailServices.mfn.removeListener(this);
          resolve(destFolder);
        },
        folderRenamed(oldFolder, newFolder) {
          if (oldFolder.URI != uri) {
            return;
          }

          MailServices.mfn.removeListener(this);
          resolve(newFolder);
        },
      },
      flags
    );
  });
}

this.folders = class extends ExtensionAPIPersistent {
  PERSISTENT_EVENTS = {
    // For primed persistent events (deactivated background), the context is only
    // available after fire.wakeup() has fulfilled (ensuring the convert() function
    // has been called).

    onCreated({ context, fire }) {
      async function listener(event, createdMailFolder) {
        if (fire.wakeup) {
          await fire.wakeup();
        }
        fire.async(createdMailFolder);
      }
      folderTracker.on("folder-created", listener);
      return {
        unregister: () => {
          folderTracker.off("folder-created", listener);
        },
        convert(newFire, extContext) {
          fire = newFire;
          context = extContext;
        },
      };
    },
    onRenamed({ context, fire }) {
      async function listener(event, originalMailFolder, renamedMailFolder) {
        if (fire.wakeup) {
          await fire.wakeup();
        }
        fire.async(originalMailFolder, renamedMailFolder);
      }
      folderTracker.on("folder-renamed", listener);
      return {
        unregister: () => {
          folderTracker.off("folder-renamed", listener);
        },
        convert(newFire, extContext) {
          fire = newFire;
          context = extContext;
        },
      };
    },
    onMoved({ context, fire }) {
      async function listener(event, srcMailFolder, dstMailFolder) {
        if (fire.wakeup) {
          await fire.wakeup();
        }
        fire.async(srcMailFolder, dstMailFolder);
      }
      folderTracker.on("folder-moved", listener);
      return {
        unregister: () => {
          folderTracker.off("folder-moved", listener);
        },
        convert(newFire, extContext) {
          fire = newFire;
          context = extContext;
        },
      };
    },
    onCopied({ context, fire }) {
      async function listener(event, srcMailFolder, dstMailFolder) {
        if (fire.wakeup) {
          await fire.wakeup();
        }
        fire.async(srcMailFolder, dstMailFolder);
      }
      folderTracker.on("folder-copied", listener);
      return {
        unregister: () => {
          folderTracker.off("folder-copied", listener);
        },
        convert(newFire, extContext) {
          fire = newFire;
          context = extContext;
        },
      };
    },
    onDeleted({ context, fire }) {
      async function listener(event, deletedMailFolder) {
        if (fire.wakeup) {
          await fire.wakeup();
        }
        fire.async(deletedMailFolder);
      }
      folderTracker.on("folder-deleted", listener);
      return {
        unregister: () => {
          folderTracker.off("folder-deleted", listener);
        },
        convert(newFire, extContext) {
          fire = newFire;
          context = extContext;
        },
      };
    },
    onFolderInfoChanged({ context, fire }) {
      async function listener(event, changedMailFolder, mailFolderInfo) {
        if (fire.wakeup) {
          await fire.wakeup();
        }
        fire.async(changedMailFolder, mailFolderInfo);
      }
      folderTracker.on("folder-info-changed", listener);
      return {
        unregister: () => {
          folderTracker.off("folder-info-changed", listener);
        },
        convert(newFire, extContext) {
          fire = newFire;
          context = extContext;
        },
      };
    },
  };

  getAPI(context) {
    return {
      folders: {
        onCreated: new EventManager({
          context,
          module: "folders",
          event: "onCreated",
          extensionApi: this,
        }).api(),
        onRenamed: new EventManager({
          context,
          module: "folders",
          event: "onRenamed",
          extensionApi: this,
        }).api(),
        onMoved: new EventManager({
          context,
          module: "folders",
          event: "onMoved",
          extensionApi: this,
        }).api(),
        onCopied: new EventManager({
          context,
          module: "folders",
          event: "onCopied",
          extensionApi: this,
        }).api(),
        onDeleted: new EventManager({
          context,
          module: "folders",
          event: "onDeleted",
          extensionApi: this,
        }).api(),
        onFolderInfoChanged: new EventManager({
          context,
          module: "folders",
          event: "onFolderInfoChanged",
          extensionApi: this,
        }).api(),
        async create(parent, childName) {
          // The schema file allows parent to be either a MailFolder or a
          // MailAccount.
          let { folder: parentFolder, accountId } = getFolder(parent);

          if (
            parentFolder.hasSubFolders &&
            parentFolder.subFolders.find(f => f.prettyName == childName)
          ) {
            throw new ExtensionError(
              `folders.create() failed, because ${childName} already exists in ${folderURIToPath(
                accountId,
                parentFolder.URI
              )}`
            );
          }

          let childFolderPromise = waitForOperation(
            MailServices.mfn.folderAdded,
            parentFolder.URI
          );
          parentFolder.createSubfolder(childName, null);

          let childFolder = await childFolderPromise;
          return convertFolder(childFolder, accountId);
        },
        async rename({ accountId, path }, newName) {
          let { folder } = getFolder({ accountId, path });

          if (!folder.parent) {
            throw new ExtensionError(
              `folders.rename() failed, because it cannot rename the root of the account`
            );
          }
          if (folder.server.type == "nntp") {
            throw new ExtensionError(
              `folders.rename() is not supported in news accounts`
            );
          }

          if (folder.parent.subFolders.find(f => f.prettyName == newName)) {
            throw new ExtensionError(
              `folders.rename() failed, because ${newName} already exists in ${folderURIToPath(
                accountId,
                folder.parent.URI
              )}`
            );
          }

          let newFolderPromise = waitForOperation(
            MailServices.mfn.folderRenamed,
            folder.URI
          );
          folder.rename(newName, null);

          let newFolder = await newFolderPromise;
          return convertFolder(newFolder, accountId);
        },
        async move(source, destination) {
          return doMoveCopyOperation(source, destination, true /* isMove */);
        },
        async copy(source, destination) {
          return doMoveCopyOperation(source, destination, false /* isMove */);
        },
        async delete({ accountId, path }) {
          if (
            !context.extension.hasPermission("accountsFolders") ||
            !context.extension.hasPermission("messagesDelete")
          ) {
            throw new ExtensionError(
              'Using folders.delete() requires the "accountsFolders" and the "messagesDelete" permission'
            );
          }

          let { folder } = getFolder({ accountId, path });
          if (folder.server.type == "nntp") {
            throw new ExtensionError(
              `folders.delete() is not supported in news accounts`
            );
          }

          if (folder.server.type == "imap") {
            let inTrash = false;
            let parent = folder.parent;
            while (!inTrash && parent) {
              inTrash = parent.flags & Ci.nsMsgFolderFlags.Trash;
              parent = parent.parent;
            }
            if (inTrash) {
              // FixMe: The UI is not updated, the folder is still shown, only after
              // a restart it is removed from trash.
              let deletedPromise = new Promise(resolve => {
                MailServices.imap.deleteFolder(
                  folder,
                  {
                    OnStartRunningUrl() {},
                    OnStopRunningUrl(url, status) {
                      resolve(status);
                    },
                  },
                  null
                );
              });
              let status = await deletedPromise;
              if (!Components.isSuccessCode(status)) {
                throw new ExtensionError(
                  `folders.delete() failed for unknown reasons`
                );
              }
            } else {
              // FixMe: Accounts could have their trash folder outside of their
              // own folder structure.
              let trash = folder.server.rootFolder.getFolderWithFlags(
                Ci.nsMsgFolderFlags.Trash
              );
              let deletedPromise = new Promise(resolve => {
                MailServices.imap.moveFolder(
                  folder,
                  trash,
                  {
                    OnStartRunningUrl() {},
                    OnStopRunningUrl(url, status) {
                      resolve(status);
                    },
                  },
                  null
                );
              });
              let status = await deletedPromise;
              if (!Components.isSuccessCode(status)) {
                throw new ExtensionError(
                  `folders.delete() failed for unknown reasons`
                );
              }
            }
          } else {
            let deletedPromise = waitForOperation(
              MailServices.mfn.folderDeleted |
                MailServices.mfn.folderMoveCopyCompleted,
              folder.URI
            );
            folder.deleteSelf(null);
            await deletedPromise;
          }
        },
        async getFolderInfo({ accountId, path }) {
          let { folder } = getFolder({ accountId, path });

          let mailFolderInfo = {
            favorite: folder.getFlag(Ci.nsMsgFolderFlags.Favorite),
            totalMessageCount: folder.getTotalMessages(false),
            unreadMessageCount: folder.getNumUnread(false),
          };

          return mailFolderInfo;
        },
        async getParentFolders({ accountId, path }, includeFolders) {
          let { folder } = getFolder({ accountId, path });
          let parentFolders = [];
          // We do not consider the absolute root ("/") as a root folder, but
          // the first real folders (all folders returned in MailAccount.folders
          // are considered root folders).
          while (folder.parent != null && folder.parent.parent != null) {
            folder = folder.parent;

            if (includeFolders) {
              parentFolders.push(traverseSubfolders(folder, accountId));
            } else {
              parentFolders.push(convertFolder(folder, accountId));
            }
          }
          return parentFolders;
        },
        async getSubFolders(accountOrFolder, includeFolders) {
          let { folder, accountId } = getFolder(accountOrFolder);
          let subFolders = [];
          if (folder.hasSubFolders) {
            for (let subFolder of folder.subFolders) {
              if (includeFolders) {
                subFolders.push(traverseSubfolders(subFolder, accountId));
              } else {
                subFolders.push(convertFolder(subFolder, accountId));
              }
            }
          }
          return subFolders;
        },
      },
    };
  }
};
