/* ***** 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 Sun Microsystems code.
 *
 * The Initial Developer of the Original Code is
 *   Sun Microsystems, Inc.
 * Portions created by the Initial Developer are Copyright (C) 2008
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *   Daniel Boelzle <daniel.boelzle@sun.com>
 *   Berend Cornelius <berend.cornelius@sun.com>
 *
 * 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 ***** */

// New code must not load/import calUtils.js, but should use calUtils.jsm.

var gCalThreadingEnabled;

EXPORTED_SYMBOLS = ["cal"];
let cal = {
    // new code should land here,
    // and more code should be moved from calUtils.js into this object to avoid
    // clashes with other extensions

    getThreadManager: generateServiceAccessor("@mozilla.org/thread-manager;1",
                                              Components.interfaces.nsIThreadManager),
    getIOService: generateServiceAccessor("@mozilla.org/network/io-service;1",
                                          Components.interfaces.nsIIOService2),
    getObserverService: generateServiceAccessor("@mozilla.org/observer-service;1",
                                                Components.interfaces.nsIObserverService),
    getDragService: generateServiceAccessor("@mozilla.org/widget/dragservice;1",
                                                Components.interfaces.nsIDragService),

    /**
     * Loads an array of calendar scripts into the passed scope.
     *
     * @param scriptNames an array of calendar script names
     * @param scope       scope to load into
     * @param baseDir     base dir; defaults to calendar-js/
     */
    loadScripts: function cal_loadScripts(scriptNames, scope, baseDir) {
        let scriptLoader = Components.classes["@mozilla.org/moz/jssubscript-loader;1"]
                                     .createInstance(Components.interfaces.mozIJSSubScriptLoader);
        let ioService = cal.getIOService();

        if (!baseDir) {
            baseDir = __LOCATION__.parent.parent.clone();
            baseDir.append("calendar-js");
        }

        for each (let script in scriptNames) {
            if (!script) {
                // If the array element is null, then just skip this script.
                continue;
            }
            let scriptFile = baseDir.clone();
            scriptFile.append(script);
            let scriptUrlSpec = ioService.newFileURI(scriptFile).spec;
            try {
                scriptLoader.loadSubScript(scriptUrlSpec, scope);
            } catch (exc) {
                Components.utils.reportError(exc + " (" + scriptUrlSpec + ")");
            }
        }
    },

    /**
     * Schedules execution of the passed function to the current thread's queue.
     * Do this to work around re-entrancy problems w.r.t. processPendingEvent(), e.g. on chrome startup.
     */
    postPone: function cal_postPone(func) {
        if (this.threadingEnabled) {
            cal.getThreadManager().currentThread.dispatch({ run: func },
                                                          Components.interfaces.nsIEventTarget.DISPATCH_NORMAL);
        } else {
            func();
        }
    },

    /**
     * Call this function regularly to process a pending event, e.g. UI.
     */
    processPendingEvent: function cal_processPendingEvent() {
        let thread = cal.getThreadManager().currentThread;
        thread.processNextEvent(false /* don't wait */);
    },

    get threadingEnabled() {
        if (gCalThreadingEnabled === undefined) {
            gCalThreadingEnabled = !cal.getPrefSafe("calendar.threading.disabled", false);
        }
        return gCalThreadingEnabled;
    },

    getWorkerThread: function cal_getWorkerThread() {
        let threads = cal_getWorkerThread.mThreads;
        switch (threads) {
            case null:
                return null; // during shutdown
            case undefined: {
                cal_getWorkerThread.mThreads = threads = Components.classes["@mozilla.org/thread-pool;1"]
                                                                   .createInstance(Components.interfaces.nsIThreadPool);
                cal.addShutdownObserver(function() {
                        cal.LOG("cal.getWorkerThread() shutdown.");
                        let threads = cal_getWorkerThread.mThreads;
                        cal_getWorkerThread.mThreads = null;
                        threads.shutdown();
                    });
                break;
            }
        }
        return threads;
    },

    /**
     * Executes the passed action function on a worker thread.
     * Once the action function has been executed, respFunc is called
     * on the same thread that execAsync() has been called on.
     *
     * xxx todo: think of leaks, ref cycles!
     */
    execWorker: function cal_execWorker(actionFunc, respFunc) {
        // response executed on the calling thread:
        let callingThread = cal.getThreadManager().currentThread;

        let worker = { // nsIRunnable:
            run: function worker_run() {
                let res = null;
                try {
                    actionFunc(callingThread);
                } catch (exc) {
                    res = exc;
                }

                if (!respFunc) {
                    return;
                }

                let response = { // nsIRunnable:
                    run: function response_run() {
                        respFunc(res);
                    }
                };
                callingThread.dispatch(response, Components.interfaces.nsIEventTarget.DISPATCH_NORMAL);
            }
        };

        if (cal.threadingEnabled) {
            let thread = cal.getWorkerThread();
            if (thread) {
                thread.dispatch(worker, Components.interfaces.nsIEventTarget.DISPATCH_NORMAL);
                return;
            } // else shutdown ongoing
        }

        worker.run(); // exec on current thread
    },

    /**
     * Checks whether a timezone lacks a definition.
     */
    isPhantomTimezone: function cal_isPhantomTimezone(tz) {
        return (!tz.icalComponent && !tz.isUTC && !tz.isFloating);
    },

    /**
     * Shifts an item by the given timely offset.
     *
     * @param item an item
     * @param offset an offset (calIDuration)
     */
    shiftItem: function cal_shiftItem(item, offset) {
        // When modifying dates explicitly using the setters is important
        // since those may triggers e.g. calIRecurrenceInfo::onStartDateChange
        // or invalidate other properties. Moreover don't modify the date-time objects
        // without cloning, because changes cannot be calculated if doing so.
        if (cal.isEvent(item)) {
            let date = item.startDate.clone();
            date.addDuration(offset);
            item.startDate = date;
            date = item.endDate.clone();
            date.addDuration(offset);
            item.endDate = date;
        } else /* isToDo */ {
            if (item.entryDate) {
                let date = item.entryDate.clone();
                date.addDuration(offset);
                item.entryDate = date;
            }
            if (item.dueDate) {
                let date = item.dueDate.clone();
                date.addDuration(offset);
                item.dueDate = date;
            }
        }
    },

    /**
     * Shortcut function to serialize an item (including all overridden items).
     */
    getSerializedItem: function cal_getSerializedItem(aItem) {
        let serializer = Components.classes["@mozilla.org/calendar/ics-serializer;1"]
                                   .createInstance(Components.interfaces.calIIcsSerializer);
        serializer.addItems([aItem], 1);
        return serializer.serializeToString();
    },

    /**
     * Shortcut function to check whether an item is an invitation copy.
     */
    isInvitation: function cal_isInvitation(aItem) {
        let isInvitation = false;
        let calendar = aItem.calendar;
        if (cal.calInstanceOf(calendar, Components.interfaces.calISchedulingSupport)) {
            isInvitation = calendar.isInvitation(aItem);
        }
        return isInvitation;
    },

    /**
     * Shortcut function to check whether an item is an invitation copy and
     * has a participation status of either NEEDS-ACTION or TENTATIVE.
     *
     * @param aItem either calIAttendee or calIItemBase 
     */
    isOpenInvitation: function cal_isOpenInvitation(aItem) {
        if (!cal.calInstanceOf(aItem, Components.interfaces.calIAttendee)) {
            aItem = cal.getInvitedAttendee(aItem);
        }
        if (aItem) {
            switch (aItem.participationStatus) {
                case "NEEDS-ACTION":
                case "TENTATIVE":
                    return true;
            }
        }
        return false;
    },

    /**
     * Shortcut function to get the invited attendee of an item.
     */
    getInvitedAttendee: function cal_getInvitedAttendee(aItem, aCalendar) {
        if (!aCalendar) {
            aCalendar = aItem.calendar;
        }
        let invitedAttendee = null;
        if (cal.calInstanceOf(aCalendar, Components.interfaces.calISchedulingSupport)) {
            invitedAttendee = aCalendar.getInvitedAttendee(aItem);
        }
        return invitedAttendee;
    },

    // The below functions will move to some different place once the
    // unifinder tress are consolidated.

    compareNativeTime: function cal_compareNativeTime(a, b) {
      return (a < b ? -1 :
              a > b ?  1 : 0);
    },

    compareNumber: function cal_compareNumber(a, b) {
      a = Number(a);
      b = Number(b);
      return ((a < b) ? -1 :      // avoid underflow problems of subtraction
              (a > b) ?  1 : 0);
    },

    sortEntryComparer: function cal_sortEntryComparer(sortType, modifier) {
      switch (sortType) {
        case "number":
          function compareNumbers(sortEntryA, sortEntryB) {
            var nsA = cal.sortEntryKey(sortEntryA);
            var nsB = cal.sortEntryKey(sortEntryB);
            return cal.compareNumber(nsA, nsB) * modifier;
          }
          return compareNumbers;
        case "date":
          function compareTimes(sortEntryA, sortEntryB) {
            var nsA = cal.sortEntryKey(sortEntryA);
            var nsB = cal.sortEntryKey(sortEntryB);
            return cal.compareNativeTime(nsA, nsB) * modifier;
          }
          return compareTimes;
        case "string":
          var collator = cal.createLocaleCollator();
          function compareStrings(sortEntryA, sortEntryB) {
            var sA = cal.sortEntryKey(sortEntryA);
            var sB = cal.sortEntryKey(sortEntryB);
            if (sA.length == 0 || sB.length == 0) {
              // sort empty values to end (so when users first sort by a
              // column, they can see and find the desired values in that
              // column without scrolling past all the empty values).
              return -(sA.length - sB.length) * modifier;
            }
            var comparison = collator.compareString(0, sA, sB);
            return comparison * modifier;
          }
          return compareStrings;

        default:
          function compareOther(sortEntryA, sortEntryB) {
            return 0;
          }
          return compareOther;
      }
    },

    getItemSortKey: function cal_getItemSortKey(aItem, aKey, aStartTime) {
      switch(aKey) {
        case "priority":
          return aItem.priority || 5;

        case "title":
          return aItem.title || "";

        case "entryDate":
            return cal.nativeTimeOrNow(aItem.entryDate, aStartTime);
        case "startDate":
            return cal.nativeTimeOrNow(aItem.startDate, aStartTime);

        case "dueDate":
          return cal.nativeTimeOrNow(aItem.dueDate, aStartTime);

        case "endDate":
          return cal.nativeTimeOrNow(aItem.endDate, aStartTime);
        case "completedDate":
                  // XXX: is this right ??
          return cal.nativeTimeOrNow(aItem.completedDate, aStartTime);

        case "percentComplete":
          return aItem.percentComplete;

        case "categories":
          return aItem.getCategories({}).join(", ");

        case "location":
          return aItem.getProperty("LOCATION") || "";

        case "status":
          if (cal.isToDo(aItem)) {
            return ["NEEDS-ACTION", "IN-PROCESS", "COMPLETED", "CANCELLED" ].indexOf(aItem.status);
          } else {
            return ["TENTATIVE", "CONFIRMED", "CANCELLED"].indexOf(aItem.status);
          }
        case "calendar":
          return aItem.calendar.name || "";

        default:
          return null;
      }
    },

    getSortTypeForSortKey: function cal_getSortTypeForSortKey(aSortKey) {
      switch(aSortKey) {
        case "title":
        case "categories":
        case "location":
        case "calendar":
          return "string";

        case "completedDate":
        case "entryDate":
        case "dueDate":
        case "startDate":
        case "endDate":
          return "date";

        case "priority":
        case "percentComplete":
        case "status":
          return "number";
      }
    },

    nativeTimeOrNow: function cal_nativeTimeOrNow(calDateTime, sortStartedTime) {
        // Treat null/0 as 'now' when sort started, so incomplete tasks stay current.
        // Time is computed once per sort (just before sort) so sort is stable.
        if (calDateTime == null) {
            return sortStartedTime.nativeTime;
        }
        var ns = calDateTime.nativeTime;
        if (ns == -62168601600000000) { // ns value for (0000/00/00 00:00:00)
            return sortStartedTime;
        }
        return ns;
    },

    sortEntry: function cal_sortEntry(aItem) {
        var key = cal.getItemSortKey(aItem, this.mSortKey, this.mSortStartedDate);
        return {mSortKey : key, mItem: aItem};
    },

    sortEntryItem: function cal_sortEntryItem(sortEntry) {
        return sortEntry.mItem;
    },

    sortEntryKey: function cal_sortEntryKey(sortEntry) {
        return sortEntry.mSortKey;
    },

    createLocaleCollator: function cal_createLocaleCollator() {
        let localeService = Components.classes["@mozilla.org/intl/nslocaleservice;1"]
                                      .getService(Components.interfaces.nsILocaleService);
        return Components.classes["@mozilla.org/intl/collation-factory;1"]
                         .getService(Components.interfaces.nsICollationFactory)
                         .CreateCollation(localeService.getApplicationLocale());
     },

    /**
     * Sort an array of strings according to the current locale.
     * Modifies aStringArray, returning it sorted.
     */
    sortArrayByLocaleCollator: function cal_sortArrayByLocaleCollator(aStringArray) {
        var localeCollator = cal.createLocaleCollator();
        function compare(a, b) { return localeCollator.compareString(0, a, b); }
        aStringArray.sort(compare);
        return aStringArray;
    },

    /**
     * moves an item to another startDate
     *
     * @param aOldItem             The Item to be modified
     * @param aNewDate             The date at which the new item is going to start
     * @return                     The modified item
     */
    moveItem: function cal_moveItem(aOldItem, aNewDate) {
        let newItem = aOldItem.clone();
        let start = (aOldItem[calGetStartDateProp(aOldItem)] ||
                     aOldItem[calGetEndDateProp(aOldItem)]).clone();
        let isDate = start.isDate;
        start.resetTo(aNewDate.year, aNewDate.month, aNewDate.day,
                      start.hour, start.minute, start.second,
                      start.timezone);
        start.isDate = isDate;
        if (newItem[calGetStartDateProp(newItem)]) {
            newItem[calGetStartDateProp(newItem)] = start;
            let oldDuration = aOldItem.duration;
            if (oldDuration) {
                let oldEnd = aOldItem[calGetEndDateProp(aOldItem)];
                let newEnd = start.clone();
                newEnd.addDuration(oldDuration);
                newEnd = newEnd.getInTimezone(oldEnd.timezone);
                newItem[calGetEndDateProp(newItem)] = newEnd;
            }
        } else if (newItem[calGetEndDateProp(newItem)]) {
            newItem[calGetEndDateProp(newItem)] = start;
        }
        return newItem;
    },

    /**
     * sets the 'isDate' property of an item
     *
     * @param aItem         The Item to be modified
     * @param aIsDate       True or false indicating the new value of 'isDate'
     * @return              The modified item
     */
    setItemToAllDay: function cal_setItemToAllDay(aItem, aIsDate){
        let start = aItem[calGetStartDateProp(aItem)];
        let end = aItem[calGetEndDateProp(aItem)];
        if (start || end) {
            let item = aItem.clone();
            if (start && (start.isDate != aIsDate)) {
               start = start.clone();
               start.isDate = aIsDate;
               item[calGetStartDateProp(item)] = start;
            }
            if (end && (end.isDate != aIsDate)) {
               end = end.clone();
               end.isDate = aIsDate;
               item[calGetEndDateProp(item)] = end;
            }
            return item;
        } else {
            return aItem;
        }
    },

    /**
     * checks if the mousepointer of an event resides over a XULBox during an event
     *
     * @param aMouseEvent   The event eg. a 'mouseout' or 'mousedown' event
     * @param aXULBox       The xul element
     * @return              true or false depending on whether the mouse pointer
     *                      resides over the xulelement
     */
    isMouseOverBox: function cal_isMouseOverBox(aMouseEvent, aXULElement) {
        let boxObject = aXULElement.boxObject;
        let boxWidth = boxObject.width;
        let boxHeight = boxObject.height;
        let boxScreenX = boxObject.screenX;
        let boxScreenY = boxObject.screenY;
        let mouseX = aMouseEvent.screenX;
        let mouseY = aMouseEvent.screenY;
        let xIsWithin = (mouseX >= boxScreenX) &&
                        (mouseX <= (boxScreenX + boxWidth));
        let yIsWithin = (mouseY >= boxScreenY) &&
                        (mouseY <= (boxScreenY + boxHeight));
        return (xIsWithin && yIsWithin);
    },

    /**
     * removes those childnodes from a node that contain a specified attribute
     * and where the value of this attribute matches a passed value
     * @param aParentNode   The parent node that contains the child nodes in question
     * @param aAttribute    The name of the attribute
     * @param aAttribute    The value of the attribute
     */
    removeChildElementsByAttribute: function removeChildElementsByAttribute(aParentNode, aAttribute, aValue) {
        let childNode = aParentNode.lastChild;
        while (childNode) {
            let prevChildNode = childNode.previousSibling;
            if (!aAttribute || aAttribute === undefined) {
                aParentNode.removeChild(childNode);
             } else if (!aValue || aValue === undefined) {
                aParentNode.removeChild(childNode);
            } else if (childNode && childNode.hasAttribute(aAttribute)
                && childNode.getAttribute(aAttribute) == aValue) {
                aParentNode.removeChild(childNode);
            }
            childNode = prevChildNode;
        };
    },

    /**
     * Returns the most recent calendar window in an application independent way
     */
    getCalendarWindow: function cal_getCalendarWindow() {
        let wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
                           .getService(Components.interfaces.nsIWindowMediator);
        return wm.getMostRecentWindow("calendarMainWindow") ||
               wm.getMostRecentWindow("mail:3pane");
    },

    /**
     * Adds an observer listening for the topic.
     *
     * @param func function to execute on topic
     * @param topic topic to listen for
     * @param oneTime whether to listen only once
     */
    addObserver: function cal_addObserver(func, topic, oneTime) {
        let observer = { // nsIObserver:
            observe: function cal_addObserver_observe(subject, topic_, data) {
                if (topic == topic_) {
                    if (oneTime) {
                        Components.classes["@mozilla.org/observer-service;1"]
                                  .getService(Components.interfaces.nsIObserverService)
                                  .removeObserver(this, topic);
                    }
                    func(subject, topic, data);
                }
            }
        };
        Components.classes["@mozilla.org/observer-service;1"]
                  .getService(Components.interfaces.nsIObserverService)
                  .addObserver(observer, topic, false /* don't hold weakly */);
    },

    /**
     * Adds an xpcom shutdown observer.
     *
     * @param func function to execute
     */
    addShutdownObserver: function cal_addShutdownObserver(func) {
        cal.addObserver(func, "xpcom-shutdown", true /* one time */);
    },

    /**
     * Due to wrapped js objects, some objects may have cyclic references.
     * You can register properties of objects to be cleaned up on xpcom-shutdown.
     *
     * @param obj    object
     * @param prop   property to be deleted on shutdown
     *               (if null, |object| will be deleted)
     */
    registerForShutdownCleanup: shutdownCleanup
};

// local to this module;
// will be used to clean up global objects on shutdown
// some objects have cyclic references due to wrappers
function shutdownCleanup(obj, prop) {
    if (!shutdownCleanup.mEntries) {
        shutdownCleanup.mEntries = [];
        cal.addShutdownObserver(function() {
                for each (let entry in shutdownCleanup.mEntries) {
                    if (entry.mProp) {
                        delete entry.mObj[entry.mProp];
                    } else {
                        delete entry.mObj;
                    }
                }
                delete shutdownCleanup.mEntries;
            });
    }
    shutdownCleanup.mEntries.push({ mObj: obj, mProp: prop });
}

// local to this module;
// will be used to generate service accessor functions, getIOService()
function generateServiceAccessor(id, iface) {
    return function this_() {
        if (!("mService" in this_)) {
            this_.mService = Components.classes[id].getService(iface);
            shutdownCleanup(this_, "mService");
        }
        return this_.mService;
    };
}

// Interim import of all symbols into cal:
// This should serve as a clean start for new code, e.g. new code could use
// cal.createDatetime instead of plain createDatetime NOW.
cal.loadScripts(["calUtils.js"], cal);
// Some functions in calUtils.js refer to other in the same file, thus include
// the code in global scope (although only visible to this module file), too:
cal.loadScripts(["calUtils.js"], cal.__parent__);
