/* ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is Mail utility functions for GMail Conversation View
 *
 * The Initial Developer of the Original Code is
 * Jonathan Protzenko
 * Portions created by the Initial Developer are Copyright (C) 2010
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either the GNU General Public License Version 2 or later (the "GPL"), or
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the MPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the MPL, the GPL or the LGPL.
 *
 * ***** END LICENSE BLOCK ***** */

/**
 * @fileoverview A whole bunch of utility functions that will abstract away
 *  various low-level nsIMsgDbHdr operations. The idea is to save time by not
 *  having to lookup how to do simple actions.
 * @author Jonathan Protzenko
 */

var EXPORTED_SYMBOLS = [
  // Low-level XPCOM boring stuff
  'msgHdrToMessageBody', 'msgHdrToNeckoURL', 'msgHdrGetTags', 'msgUriToMsgHdr',
  'msgHdrGetUri', 'msgHdrFromNeckoUrl', 'msgHdrSetTags',
  // Quickly identify a message
  'msgHdrIsDraft', 'msgHdrIsSent', 'msgHdrIsArchive', 'msgHdrIsInbox',
  'msgHdrIsRss', 'msgHdrIsNntp', 'msgHdrIsJunk',
  // Actions on a set of message headers
  'msgHdrsMarkAsRead', 'msgHdrsArchive', 'msgHdrsDelete',
  // Doesn't really belong here
  'getMail3Pane',
  // Higher-level functions
  'msgHdrGetHeaders',
]

  const {classes: Cc, interfaces: Ci, utils: Cu, results : Cr} = Components;

// from mailnews/base/public/nsMsgFolderFlags.idl
const nsMsgFolderFlags_SentMail = 0x00000200;
const nsMsgFolderFlags_Drafts   = 0x00000400;
const nsMsgFolderFlags_Archive  = 0x00004000;
const nsMsgFolderFlags_Inbox    = 0x00001000;

Cu.import("resource://gre/modules/XPCOMUtils.jsm"); // for defineLazyServiceGetter
Cu.import("resource:///modules/gloda/mimemsg.js");
Cu.import("resource:///modules/gloda/utils.js");
Cu.import("resource:///modules/iteratorUtils.jsm"); // for toXPCOMArray
Cu.import("resource:///modules/mailServices.js");

// Adding a messenger lazy getter to the MailServices even though it's not a service
XPCOMUtils.defineLazyGetter(MailServices, "messenger", function () {
  return Cc["@mozilla.org/messenger;1"].createInstance(Ci.nsIMessenger);
});

/**
 * Get a given message header's uri.
 * @param {nsIMsgDbHdr} aMsg The message
 * @return {String}
 */
function msgHdrGetUri (aMsg)
  aMsg.folder.getUriForMsg(aMsg)

/**
 * Get a msgHdr from a message URI (msgHdr.URI).
 * @param {String} aUri The URI of the message
 * @return {nsIMsgDbHdr}
 */
function msgUriToMsgHdr(aUri) {
  try {
    let messageService = MailServices.messenger.messageServiceFromURI(aUri);
    return messageService.messageURIToMsgHdr(aUri);
  } catch (e) {
    dump("Unable to get "+aUri+" — returning null instead");
    return null;
  }
}

/**
 * Tells if the message is in the account's inbox
 * @param {nsIMsgDbHdr} msgHdr The message header to examine
 * @return {bool}
 */
function msgHdrIsInbox(msgHdr)
  msgHdr.folder.getFlag(nsMsgFolderFlags_Inbox)

/**
 * Tells if the message is a draft message
 * @param {nsIMsgDbHdr} msgHdr The message header to examine
 * @return {bool}
 */
function msgHdrIsDraft(msgHdr)
  msgHdr.folder.getFlag(nsMsgFolderFlags_Drafts)

/**
 * Tells if the message is a sent message
 * @param {nsIMsgDbHdr} msgHdr The message header to examine
 * @return {bool}
 */
function msgHdrIsSent(msgHdr)
  msgHdr.folder.getFlag(nsMsgFolderFlags_SentMail)

/**
 * Tells if the message is an archived message
 * @param {nsIMsgDbHdr} msgHdr The message header to examine
 * @return {bool}
 */
function msgHdrIsArchive(msgHdr)
  msgHdr.folder.getFlag(nsMsgFolderFlags_Archive)

/**
 * Get a nsIMsgDbHdr from a Necko URL.
 * @param {String} The URL
 * @return {nsIMsgDbHdr} The message header.
 */
function msgHdrFromNeckoUrl(aUrl)
  aUrl.QueryInterface(Ci.nsIMsgMessageUrl).messageHeader

/**
 * Get a string containing the body of a messsage.
 * @param {nsIMsgDbHdr} aMessageHeader The message header
 * @param {bool} aStripHtml Keep html?
 * @return {string}
 */
function msgHdrToMessageBody(aMessageHeader, aStripHtml, aLength) {
  let messenger = Cc["@mozilla.org/messenger;1"].createInstance(Ci.nsIMessenger);  
  let listener = Cc["@mozilla.org/network/sync-stream-listener;1"].createInstance(Ci.nsISyncStreamListener);  
  let uri = aMessageHeader.folder.getUriForMsg(aMessageHeader);  
  messenger.messageServiceFromURI(uri).streamMessage(uri, listener, null, null, false, "");  
  let folder = aMessageHeader.folder;  
  /*
   * AUTF8String getMsgTextFromStream(in nsIInputStream aStream, in ACString aCharset,
                                      in unsigned long aBytesToRead, in unsigned long aMaxOutputLen, 
                                      in boolean aCompressQuotes, in boolean aStripHTMLTags,
                                      out ACString aContentType);
  */
  return folder.getMsgTextFromStream(
    listener.inputStream, aMessageHeader.Charset, 2*aLength, aLength, false, aStripHtml, { });  
}  

/**
 * Get a nsIURI from a nsIMsgDBHdr
 * @param {nsIMsgDbHdr} aMsgHdr The message header
 * @return {nsIURI}
 */
function msgHdrToNeckoURL(aMsgHdr) {
  let uri = aMsgHdr.folder.getUriForMsg(aMsgHdr);
  let neckoURL = {};
  let msgService = MailServices.messenger.messageServiceFromURI(uri);
  msgService.GetUrlForUri(uri, neckoURL, null);
  return neckoURL.value;
}

/**
 * Given a msgHdr, return a list of tag objects. This function
 * just does the messy work of understanding how tags are
 * stored in nsIMsgDBHdrs.
 *
 * @param {nsIMsgDbHdr} aMsgHdr the msgHdr whose tags we want
 * @return {nsIMsgTag array} a list of tag objects
 */
function msgHdrGetTags (aMsgHdr) {
  let keywords = aMsgHdr.getStringProperty("keywords");
  let keywordList = keywords.split(' ');
  let keywordMap = {};
  for (let [, keyword] in Iterator(keywordList)) {
    keywordMap[keyword] = true;
  }

  let tagArray = MailServices.tags.getAllTags({});
  let tags = [];
  for (let [iTag, tag] in Iterator(tagArray)) {
    let tag = tagArray[iTag];
    if (tag.key in keywordMap)
      tags.push(tag);
  }
  return tags;
}

/**
 * Set the tags for a given msgHdr.
 *
 * @param {nsIMsgDBHdr} aMsgHdr
 * @param {nsIMsgTag array} aTags
 */
function msgHdrSetTags (aMsgHdr, aTags) {
  let oldTagList = msgHdrGetTags(aMsgHdr);
  let oldTags = {}; // hashmap
  for each (let [, tag] in Iterator(oldTagList))
    oldTags[tag.key] = null;

  let newTags = {};
  let newTagList = aTags;
  for each (let [, tag] in Iterator(newTagList))
    newTags[tag.key] = null;

  let toAdd = [x.key for each ([, x] in Iterator(newTagList))
    if (!(x.key in oldTags))];
  let toRemove = [x.key for each ([, x] in Iterator(oldTagList))
    if (!(x.key in newTags))];

  let folder = aMsgHdr.folder;
  let msgHdr = toXPCOMArray([aMsgHdr], Ci.nsIMutableArray);
  folder.addKeywordsToMessages(msgHdr, toAdd.join(" "));
  folder.removeKeywordsFromMessages(msgHdr, toRemove.join(" "));
  aMsgHdr.folder.msgDatabase = null;
}

/**
 * Mark an array of msgHdrs read (or unread)
 * @param {nsIMsgDbHdr array} msgHdrs The message headers
 * @param {bool} read True to mark them read, false to mark them unread
 */
function msgHdrsMarkAsRead(msgHdrs, read) {
  let pending = {};
  for each (let msgHdr in msgHdrs) {
    if (msgHdr.isRead == read)
      continue;
    if (!pending[msgHdr.folder.URI]) {
      pending[msgHdr.folder.URI] = {
        folder: msgHdr.folder,
        msgs: Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray)
      };
    }
    pending[msgHdr.folder.URI].msgs.appendElement(msgHdr, false);
  }
  for each (let { folder, msgs } in pending) {
    folder.markMessagesRead(msgs, read);
    folder.msgDatabase = null; /* don't leak */
  }
}

/**
 * Delete a set of messages.
 * @param {nsIMsgDbHdr array} msgHdrs The message headers
 */
function msgHdrsDelete(msgHdrs) {
  let pending = {};
  for each (let msgHdr in msgHdrs) {
    if (!pending[msgHdr.folder.URI]) {
      pending[msgHdr.folder.URI] = {
        folder: msgHdr.folder,
        msgs: Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray)
      };
    }
    pending[msgHdr.folder.URI].msgs.appendElement(msgHdr, false);
  }
  for each (let { folder, msgs } in pending) {
    folder.deleteMessages(msgs, getMail3Pane().msgWindow, false, false, null, true);
    folder.msgDatabase = null; /* don't leak */
  }
}

/**
 * Get the main Thunderbird window. Used heavily to get a reference to globals
 *  that are defined in mail/base/content/.
 * @return The window object for the main window.
 */
function getMail3Pane() {
  return Cc["@mozilla.org/appshell/window-mediator;1"]
          .getService(Ci.nsIWindowMediator)
          .getMostRecentWindow("mail:3pane");
}

/**
 * Archive a set of messages
 * @param {nsIMsgDbHdr array} msgHdrs The message headers
 */
function msgHdrsArchive(msgHdrs) {
  /* See
   * http://mxr.mozilla.org/comm-central/source/suite/mailnews/mailWindowOverlay.js#1337
   *
   * The window is here because otherwise we don't have access to
   * BatchMessageMover.
   * */
  let mail3PaneWindow = getMail3Pane();
  let batchMover = new mail3PaneWindow.BatchMessageMover();
  batchMover.archiveMessages(msgHdrs.filter(
    function (x)
      !msgHdrIsArchive(x) && getMail3Pane().getIdentityForHeader(x).archiveEnabled
  ));
}

/**
 * Tell if a message is an RSS feed iteme
 * @param {nsIMsgDbHdr} msgHdr The message header
 * @return {Bool}
 */
function msgHdrIsRss(msgHdr)
  (msgHdr.folder.server instanceof Ci.nsIRssIncomingServer)

/**
 * Tell if a message is a NNTP message
 * @param {nsIMsgDbHdr} msgHdr The message header
 * @return {Bool}
 */
function msgHdrIsNntp(msgHdr)
  (msgHdr.folder.server instanceof Ci.nsINntpIncomingServer)

/**
 * Tell if a message has been marked as junk.
 * @param {nsIMsgDbHdr} msgHdr The message header
 * @return {Bool}
 */
function msgHdrIsJunk(aMsgHdr)
  aMsgHdr.getStringProperty("junkscore") == Ci.nsIJunkMailPlugin.IS_SPAM_SCORE

/**
 * Recycling the HeaderHandlerBase from mimemsg.js
 */
function HeaderHandler(aHeaders) {
  this.headers = aHeaders;
}

HeaderHandler.prototype = {
  __proto__: MimeMessage.prototype.__proto__, // == HeaderHandlerBase
};

/**
 * Creates a stream listener that will call k once done, passing it the string
 * that has been read.
 */
function createStreamListener(k) {
  return {
    _data: "",
    _stream : null,

    QueryInterface:
      XPCOMUtils.generateQI([Ci.nsIStreamListener, Ci.nsIRequestObserver]),

    // nsIRequestObserver
    onStartRequest: function(aRequest, aContext) {
    },
    onStopRequest: function(aRequest, aContext, aStatusCode) {
      k(this._data);
    },

    // nsIStreamListener
    onDataAvailable: function(aRequest, aContext, aInputStream, aOffset, aCount) {
      if (this._stream == null) {
        this._stream = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(Ci.nsIScriptableInputStream);
        this._stream.init(aInputStream);
      }
      this._data += this._stream.read(aCount);
    }
  };
}

/**
 * @param aMsgHdr The message header whose headers you want
 * @param k A function that takes a HeaderHandler object (see mimemsg.js).
 *  Such an object has a get function, a has function. It has a header property,
 *  whose keys are lowercased header names, and whose values are list of
 *  strings corresponding to the multiple entries found for that header.
 */
function msgHdrGetHeaders(aMsgHdr, k) {
  let uri = msgHdrGetUri(aMsgHdr);
  let messageService = MailServices.messenger.messageServiceFromURI(uri);

  let fallback = function ()
    MsgHdrToMimeMessage(aMsgHdr, null, function (aMsgHdr, aMimeMsg) {
      k(aMimeMsg);
    }, true, {
      partsOnDemand: true,
    });

  if ("streamHeaders" in messageService) {
    try {
      messageService.streamHeaders(uri, createStreamListener(function (aRawString) {
        let re = /\r?\n\s+/g;
        let str = aRawString.replace(re, " ");
        let lines = str.split(/\r?\n/);
        let obj = {};
        for each (let [, line] in Iterator(lines)) {
          let i = line.indexOf(":");
          if (i < 0)
            continue;
          let k = line.substring(0, i).toLowerCase();
          let v = line.substring(i+1).trim();
          if (!(k in obj))
            obj[k] = [];
          obj[k].push(v);
        }
        k(new HeaderHandler(obj));
      }), null, true);
    } catch (e) {
      fallback();
    }
  } else {
    fallback();
  }
}
