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

/*
 * Wrap everything about virtual folders.
 */

this.EXPORTED_SYMBOLS = ['VirtualFolderHelper'];

var Cc = Components.classes;
var Ci = Components.interfaces;
var Cr = Components.results;
var Cu = Components.utils;

Cu.import("resource:///modules/mailServices.js");
Cu.import("resource:///modules/iteratorUtils.jsm");

var VirtualFolderHelper = {
  /**
   * Create a new virtual folder (an actual nsIMsgFolder that did not previously
   *  exist), wrapping it in a VirtualFolderWrapper, and returning that wrapper.
   *
   * If the call to addSubfolder fails (and therefore throws), we will NOT catch
   *  it.
   *
   * @param aFolderName The name of the new folder to create.
   * @param aParentFolder The folder in which to create the search folder.
   * @param aSearchFolders A list of nsIMsgFolders that you want to use as the
   *     sources for the virtual folder OR a string that is the already '|'
   *     delimited list of folder URIs to use.
   * @param aSearchTerms The search terms to use for the virtual folder.  This
   *     should be a JS list/nsIMutableArray/nsISupportsArray of
   *     nsIMsgSearchTermbs.
   * @param aOnlineSearch Should the search attempt to use the server's search
   *     capabilities when possible and appropriate?
   *
   * @return The VirtualFolderWrapper wrapping the newly created folder.  You
   *     would probably only want this for its virtualFolder attribute which has
   *     the nsIMsgFolder we created.  Be careful about accessing any of the
   *     other attributes, as they will bring its message database back to life.
   */
  createNewVirtualFolder: function (aFolderName, aParentFolder, aSearchFolders,
                                    aSearchTerms, aOnlineSearch) {
    let msgFolder = aParentFolder.addSubfolder(aFolderName);
    msgFolder.prettyName = aFolderName;
    msgFolder.setFlag(Ci.nsMsgFolderFlags.Virtual);

    let wrappedVirt = new VirtualFolderWrapper(msgFolder);
    wrappedVirt.searchTerms = aSearchTerms;
    wrappedVirt.searchFolders = aSearchFolders;
    wrappedVirt.onlineSearch = aOnlineSearch;

    let msgDatabase = msgFolder.msgDatabase;
    msgDatabase.summaryValid = true;
    msgDatabase.Close(true);

    aParentFolder.NotifyItemAdded(msgFolder);
    MailServices.accounts.saveVirtualFolders();

    return wrappedVirt;
  },

  /**
   * Given an existing nsIMsgFolder that is a virtual folder, wrap it into a
   *  VirtualFolderWrapper.
   */
  wrapVirtualFolder: function (aMsgFolder) {
    return new VirtualFolderWrapper(aMsgFolder);
  },
};

/**
 * Abstracts dealing with the properties of a virtual folder that differentiate
 *  it from a non-virtual folder.  A virtual folder is an odd duck.  When
 *  holding an nsIMsgFolder that is a virtual folder, it is distinguished by
 *  the virtual flag and a number of properties that tell us the string
 *  representation of its search, the folders it searches over, and whether we
 *  use online searching or not.
 * Virtual folders and their defining attributes are loaded from
 *  virtualFolders.dat (in the profile directory) by the account manager at
 *  startup, (re-)creating them if need be.  It also saves them back to the
 *  file at shutdown.  The most important thing the account manager does is to
 *  create VirtualFolderChangeListener instances that are registered with the
 *  message database service.  This means that if one of the databases for the
 *  folders that the virtual folder includes is opened for some reason (for
 *  example, new messages are added to the folder because of a filter or they
 *  are delivered there), the virtual folder gets a chance to know about this
 *  and update the virtual folder's "cache" of information, such as the message
 *  counts or the presence of the message in the folder.
 * The odd part is that a lot of the virtual folder logic also happens as a
 *  result of the nsMsgDBView subclasses being told the search query and the
 *  underlying folders.  This makes for an odd collaboration of UI and backend
 *  logic.
 *
 * Justification for this class:  Virtual folders aren't all that complex, but
 *  they are complex enough that we don't want to have the same code duplicated
 *  all over the place.  We also don't want to have a loose assembly of global
 *  functions for working with them.  So here we are.
 *
 * Important! Accessing any of our attributes results in the message database
 *  being loaded so that we can access the dBFolderInfo associated with the
 *  database.  The message database is not automatically forgotten by the
 *  folder, which can lead to an (effective) memory leak.  Please make sure
 *  that you are playing your part in not leaking memory by only using the
 *  wrapper when you have a serious need to access the database, and by
 *  forcing the folder to forget about the database when you are done by
 *  setting the database to null (unless you know with confidence someone else
 *  definitely wants the database around and will clean it up.)
 */
function VirtualFolderWrapper(aVirtualFolder) {
  this.virtualFolder = aVirtualFolder;
};
VirtualFolderWrapper.prototype = {
  /**
   * @return the list of nsIMsgFolders that this virtual folder is a
   *     search over.
   */
  get searchFolders() {
    let rdfService = Cc['@mozilla.org/rdf/rdf-service;1']
                       .getService(Ci.nsIRDFService);
    let virtualFolderUris =
      this.dbFolderInfo.getCharProperty("searchFolderUri").split("|");
    let folders = [];
    for (let folderURI of virtualFolderUris) {
      if (folderURI)
        folders.push(rdfService.GetResource(folderURI)
                               .QueryInterface(Ci.nsIMsgFolder));
    }
    return folders;
  },
  /**
   * Set the search folders that back this virtual folder.
   *
   * @param aFolders Either a "|"-delimited string of folder URIs or a list of
   *     nsIMsgFolders that fixIterator can traverse (JS array/nsIMutableArray/
   *     nsISupportsArray).
   */
  set searchFolders(aFolders) {
    if (typeof(aFolders) == "string") {
      this.dbFolderInfo.setCharProperty("searchFolderUri", aFolders);
    }
    else {
      let uris = Array.from(fixIterator(aFolders, Ci.nsIMsgFolder))
                      .map(folder => folder.URI);
      this.dbFolderInfo.setCharProperty("searchFolderUri", uris.join("|"));
    }
  },

  /**
   * @return a "|"-delimited string containing the URIs of the folders that back
   *     this virtual folder.
   */
  get searchFolderURIs() {
    return this.dbFolderInfo.getCharProperty("searchFolderUri");
  },

  /**
   * @return the list of search terms that define this virtual folder.
   */
  get searchTerms() {
    return this.searchTermsSession.searchTerms;
  },
  /**
   * @return a newly created filter with the search terms loaded into it that
   *     define this virtual folder.  The filter is apparently useful as an
   *     nsIMsgSearchSession stand-in to some code.
   */
  get searchTermsSession() {
    // Temporary means it doesn't get exposed to the UI and doesn't get saved to
    //  disk.  Which is good, because this is just a trick to parse the string
    //  into search terms.
    let filterList = MailServices.filters.getTempFilterList(this.virtualFolder);
    let tempFilter = filterList.createFilter("temp");
    filterList.parseCondition(tempFilter, this.searchString);
    return tempFilter;
  },

  /**
   * Set the search string for this virtual folder to the stringified version of
   *  the provided list of nsIMsgSearchTerm search terms.  If you already have
   *  a strinigified version of the search constraint, just set |searchString|
   *  directly.
   *
   * @param aTerms Some collection that fixIterator can traverse.  A JS list or
   *     XPCOM array (nsIMutableArray or nsISupportsArray) should work.
   */
  set searchTerms(aTerms) {
    let condition = "";
    for (let term in fixIterator(aTerms, Ci.nsIMsgSearchTerm)) {
      if (condition.length)
        condition += " ";
      if (term.matchAll) {
        condition = "ALL";
        break;
      }
      condition += (term.booleanAnd) ? "AND (" : "OR (";
      condition += term.termAsString + ")";
    }
    this.searchString = condition;
  },

  /**
   * @return the set of search terms that define this virtual folder as a
   *     string.  You may prefer to use |searchTerms| which converts them
   *     into a list of nsIMsgSearchTerms instead.
   */
  get searchString() {
    return this.dbFolderInfo.getCharProperty("searchStr");
  },
  /**
   * Set the search that defines this virtual folder from a string.  If you have
   *  a list of nsIMsgSearchTerms, you should use |searchTerms| instead.
   */
  set searchString(aSearchString) {
    this.dbFolderInfo.setCharProperty("searchStr", aSearchString);
  },

  /**
   * @return whether the virtual folder is configured for online search.
   */
  get onlineSearch() {
    return this.dbFolderInfo.getBooleanProperty("searchOnline", false);
  },
  /**
   * Set whether the virtual folder is configured for online search.
   */
  set onlineSearch(aOnlineSearch) {
    this.dbFolderInfo.setBooleanProperty("searchOnline", aOnlineSearch);
  },

  /**
   * @return the dBFolderInfo associated with the virtual folder directly.  May
   *     be null.  Will cause the message database to be opened, which may have
   *     memory bloat/leak ramifications, so make sure the folder's database was
   *     already going to be opened anyways or that you call
   *     |cleanUpMessageDatabase|.
   */
  get dbFolderInfo() {
    let msgDatabase = this.virtualFolder.msgDatabase;
    return (msgDatabase && msgDatabase.dBFolderInfo);
  },

  /**
   * Avoid memory bloat by making the virtual folder forget about its database.
   *  If the database is actually in use (read: someone is keeping it alive by
   *  having references to it from places other than the nsIMsgFolder), the
   *  folder will be able to re-establish the reference for minimal cost.
   */
  cleanUpMessageDatabase:
      function VirtualFolderWrapper_cleanUpMessageDatabase() {
    this.virtualFolder.msgDatabase.Close(true);
    this.virtualFolder.msgDatabase = null;
  }
};
