/* -*- Mode: javascript; tab-width: 20; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/* ***** 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 Oracle Corporation code.
 *
 * The Initial Developer of the Original Code is
 *  Oracle Corporation
 * Portions created by the Initial Developer are Copyright (C) 2004
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *   Vladimir Vukicevic <vladimir.vukicevic@oracle.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 ***** */

//
// calMemoryCalendar.js
//

const calCalendarManagerContractID = "@mozilla.org/calendar/manager;1";
const calICalendarManager = Components.interfaces.calICalendarManager;

const USECS_PER_SECOND = 1000000;

var activeCalendarManager = null;
function getCalendarManager()
{
    if (!activeCalendarManager) {
        activeCalendarManager = 
            Components.classes[calCalendarManagerContractID].getService(calICalendarManager);
    }
    return activeCalendarManager;
}

function calMemoryCalendar() {
    this.wrappedJSObject = this;
    this.calendarToReturn = this,
    this.initMemoryCalendar();
}

// END_OF_TIME needs to be the max value a PRTime can be
const START_OF_TIME = -0x7fffffffffffffff;
const END_OF_TIME = 0x7fffffffffffffff;

calMemoryCalendar.prototype = {
    // This will be returned from getItems as the calendar. The ics
    // calendar overwrites this.
    calendarToReturn: null,

    //
    // nsISupports interface
    // 
    QueryInterface: function (aIID) {
        if (!aIID.equals(Components.interfaces.nsISupports) &&
            !aIID.equals(Components.interfaces.calICalendar)) {
            throw Components.results.NS_ERROR_NO_INTERFACE;
        }

        return this;
    },

    initMemoryCalendar: function() {
        this.mObservers = Array();
        this.mItems = { };
    },

    //
    // nsICalendar interface
    //

    // attribute AUTF8String name;
    get name() {
        return getCalendarManager().getCalendarPref(this, "NAME");
    },
    set name(name) {
        getCalendarManager().setCalendarPref(this, "NAME", name);
    },

    // readonly attribute AUTF8String type;
    get type() { return "memory"; },

    mReadOnly: false,

    // Most of the time you can just let the ics calendar handle this
    get readOnly() { 
        return this.mReadOnly;
    },
    set readOnly(bool) {
        this.mReadOnly = bool;
    },

    // attribute nsIURI uri;
    mUri: null,
    get uri() { return this.mUri; },
    set uri(aURI) { this.mUri = aURI; },


    refresh: function() {
        // no-op
    },

    // attribute boolean suppressAlarms;
    get suppressAlarms() { return false; },
    set suppressAlarms(aSuppressAlarms) { throw Components.results.NS_ERROR_NOT_IMPLEMENTED; },

    // void addObserver( in calIObserver observer );
    addObserver: function (aObserver, aItemFilter) {
        if (this.mObservers.some(function(o) { return (o == aObserver); }))
            return;

        this.mObservers.push(aObserver);
    },

    // void removeObserver( in calIObserver observer );
    removeObserver: function (aObserver) {
        var newObservers = this.mObservers.filter(function(o) { return (o != aObserver); });
        this.mObservers = newObservers;
    },

    // void addItem( in calIItemBase aItem, in calIOperationListener aListener );
    addItem: function (aItem, aListener) {
        var newItem = aItem.clone();
        return this.adoptItem(newItem, aListener);
    },
    
    // void adoptItem( in calIItemBase aItem, in calIOperationListener aListener );
    adoptItem: function (aItem, aListener) {
        if (this.readOnly) 
            throw Components.interfaces.calIErrors.CAL_IS_READONLY;
        if (aItem.id == null && aItem.isMutable)
            aItem.id = "uuid" + (new Date()).getTime();

        if (aItem.id == null) {
            if (aListener)
                aListener.onOperationComplete (this.calendarToReturn,
                                               Components.results.NS_ERROR_FAILURE,
                                               aListener.ADD,
                                               aItem.id,
                                               "Can't set ID on non-mutable item to addItem");
            return;
        }

        if (this.mItems[aItem.id] != null) {
            // is this an error?
            if (aListener)
                aListener.onOperationComplete (this.calendarToReturn,
                                               Components.results.NS_ERROR_FAILURE,
                                               aListener.ADD,
                                               aItem.id,
                                               "ID already exists for addItem");
            return;
        }

        aItem.calendar = this.calendarToReturn;
        aItem.generation = 1;
        aItem.makeImmutable();
        this.mItems[aItem.id] = aItem;

        // notify the listener
        if (aListener)
            aListener.onOperationComplete (this.calendarToReturn,
                                           Components.results.NS_OK,
                                           aListener.ADD,
                                           aItem.id,
                                           aItem);
        // notify observers
        this.observeAddItem(aItem);
    },

    // void modifyItem( in calIItemBase aNewItem, in calIItemBase aOldItem, in calIOperationListener aListener );
    modifyItem: function (aNewItem, aOldItem, aListener) {
        if (this.readOnly) 
            throw Components.interfaces.calIErrors.CAL_IS_READONLY;
        if (!aNewItem) {
            throw Components.results.NS_ERROR_FAILURE;
        }
        if (aNewItem.id == null || this.mItems[aNewItem.id] == null) {
            // this is definitely an error
            if (aListener)
                aListener.onOperationComplete (this.calendarToReturn,
                                               Components.results.NS_ERROR_FAILURE,
                                               aListener.MODIFY,
                                               aNewItem.id,
                                               "ID for modifyItem doesn't exist, is null, or is from different calendar");
            return;
        }

        // do the old and new items match?
        if (aOldItem.id != aNewItem.id) {
            if (aListener)
                aListener.onOperationComplete (this.calendarToReturn,
                                               Components.results.NS_ERROR_FAILURE,
                                               aListener.MODIFY,
                                               aNewItem.id,
                                               "item ID mismatch between old and new items");
            return;
        }
        
        if (aNewItem.parentItem != aNewItem) {
            aNewItem.parentItem.recurrenceInfo.modifyException(aNewItem);
            aNewItem = aNewItem.parentItem;
        }
        aOldItem = aOldItem.parentItem;
        
        if (aOldItem != this.mItems[aOldItem.id]) {
            if (aListener)
                aListener.onOperationComplete (this.calendarToReturn,
                                               Components.results.NS_ERROR_FAILURE,
                                               aListener.MODIFY,
                                               aNewItem.id,
                                               "old item mismatch in modifyItem");
            return;
        }

        if (aOldItem.generation != aNewItem.generation) {
            if (aListener)
                aListener.onOperationComplete (this.calendarToReturn,
                                               Components.results.NS_ERROR_FAILURE,
                                               aListener.MODIFY,
                                               aNewItem.id,
                                               "generation mismatch in modifyItem");
            return;
        }

        var modifiedItem = aNewItem.clone();
        modifiedItem.generation += 1;
        modifiedItem.makeImmutable();
        this.mItems[aNewItem.id] = modifiedItem;

        if (aListener)
            aListener.onOperationComplete (this.calendarToReturn,
                                           Components.results.NS_OK,
                                           aListener.MODIFY,
                                           modifiedItem.id,
                                           modifiedItem);
        // notify observers
        this.observeModifyItem(modifiedItem, aOldItem);
    },

    // void deleteItem( in calIItemBase aItem, in calIOperationListener aListener );
    deleteItem: function (aItem, aListener) {
        if (this.readOnly) 
            throw Components.interfaces.calIErrors.CAL_IS_READONLY;
        if (aItem.id == null || this.mItems[aItem.id] == null) {
            if (aListener)
                aListener.onOperationComplete (this.calendarToReturn,
                                               Components.results.NS_ERROR_FAILURE,
                                               aListener.DELETE,
                                               aItem.id,
                                               "ID is null or is from different calendar in deleteItem");
            return;
        }

        var oldItem = this.mItems[aItem.id];
        if (oldItem.generation != aItem.generation) {
            if (aListener)
                aListener.onOperationComplete (this.calendarToReturn,
                                               Components.results.NS_ERROR_FAILURE,
                                               aListener.DELETE,
                                               aItem.id,
                                               "generation mismatch in deleteItem");
            return;
        }

        delete this.mItems[aItem.id];

        if (aListener)
            aListener.onOperationComplete (this.calendarToReturn,
                                           Components.results.NS_OK,
                                           aListener.DELETE,
                                           aItem.id,
                                           null);
        // notify observers
        this.observeDeleteItem(oldItem);
    },

    // void getItem( in string id, in calIOperationListener aListener );
    getItem: function (aId, aListener) {
        if (!aListener)
            return;

        if (aId == null ||
            this.mItems[aId] == null) {
            aListener.onOperationComplete(this.calendarToReturn,
                                          Components.results.NS_ERROR_FAILURE,
                                          aListener.GET,
                                          null,
                                          "IID doesn't exist for getItem");
            return;
        }

        var item = this.mItems[aId];
        var iid = null;

        if (item instanceof Components.interfaces.calIEvent) {
            iid = Components.interfaces.calIEvent;
        } else if (item instanceof Components.interfaces.calITodo) {
            iid = Components.interfaces.calITodo;
        } else {
            aListener.onOperationComplete (this.calendarToReturn,
                                           Components.results.NS_ERROR_FAILURE,
                                           aListener.GET,
                                           aId,
                                           "Can't deduce item type based on QI");
            return;
        }

        aListener.onGetResult (this.calendarToReturn,
                               Components.results.NS_OK,
                               iid,
                               null, 1, [item]);

        aListener.onOperationComplete (this.calendarToReturn,
                                       Components.results.NS_OK,
                                       aListener.GET,
                                       aId,
                                       null);

    },

    // void getItems( in unsigned long aItemFilter, in unsigned long aCount, 
    //                in calIDateTime aRangeStart, in calIDateTime aRangeEnd,
    //                in calIOperationListener aListener );
    getItems: function (aItemFilter, aCount,
                        aRangeStart, aRangeEnd, aListener)
    {
        if (!aListener)
            return;

        const calICalendar = Components.interfaces.calICalendar;
        const calIItemBase = Components.interfaces.calIItemBase;
        const calIEvent = Components.interfaces.calIEvent;
        const calITodo = Components.interfaces.calITodo;
        const calIRecurrenceInfo = Components.interfaces.calIRecurrenceInfo;

        var itemsFound = Array();
        var startTime = START_OF_TIME;
        var endTime = END_OF_TIME;
        if (aRangeStart)
            startTime = aRangeStart.nativeTime;
        if (aRangeEnd)
            endTime = aRangeEnd.nativeTime;

        //
        // filters
        //

        // item base type
        var wantEvents = ((aItemFilter & calICalendar.ITEM_FILTER_TYPE_EVENT) != 0);
        var wantTodos = ((aItemFilter & calICalendar.ITEM_FILTER_TYPE_TODO) != 0);
        if(!wantEvents && !wantTodos) {
            // bail.
            aListener.onOperationComplete (this.calendarToReturn,
                                           Components.results.NS_ERROR_FAILURE,
                                           aListener.GET,
                                           null,
                                           "Bad aItemFilter passed to getItems");
            return;
        }

        // completed?
        var itemCompletedFilter = ((aItemFilter & calICalendar.ITEM_FILTER_COMPLETED_YES) != 0);
        var itemNotCompletedFilter = ((aItemFilter & calICalendar.ITEM_FILTER_COMPLETED_NO) != 0);

        // return occurrences?
        var itemReturnOccurrences = ((aItemFilter & calICalendar.ITEM_FILTER_CLASS_OCCURRENCES) != 0);

        // figure out the return interface type
        var typeIID = null;
        if (itemReturnOccurrences) {
            typeIID = calIItemBase;
        } else {
            if (wantEvents && wantTodos) {
                typeIID = calIItemBase;
            } else if (wantEvents) {
                typeIID = calIEvent;
            } else if (wantTodos) {
                typeIID = calITodo;
            }
        }

        //  if aCount != 0, we don't attempt to sort anything, and
        //  instead return the first aCount items that match.

        for (var itemIndex in this.mItems) {
            var item = this.mItems[itemIndex];
            var itemtoadd = null;

            var itemStartTime = 0;
            var itemEndTime = 0;

            var tmpitem = item;
            if (wantEvents && (item instanceof calIEvent)) {
                tmpitem = item.QueryInterface(calIEvent);
                itemStartTime = (item.startDate
                                 ? item.startDate.nativeTime
                                 : START_OF_TIME);
                itemEndTime = (item.endDate
                               ? item.endDate.nativeTime
                               : END_OF_TIME);
            } else if (wantTodos && (item instanceof calITodo)) {
                // if it's a todo, also filter based on completeness
                if (item.isCompleted && !itemCompletedFilter)
                    continue;
                else if (item.isCompleted && !itemNotCompletedFilter)
                    continue;

                itemEndTime = itemStartTime = 
                    item.entryDate ? item.entryDate.nativeTime : 0;
            } else {
                // XXX unknown item type, wth do we do?
                continue;
            }

            // Correct for floating
            if (aRangeStart && item.startDate && item.startDate.timezone == 'floating')
                itemStartTime -= aRangeStart.timezoneOffset * USECS_PER_SECOND;
            if (aRangeEnd && item.endDate && item.endDate.timezone == 'floating')
                itemEndTime -= aRangeEnd.timezoneOffset * USECS_PER_SECOND;

            if (itemStartTime < endTime) {
                // figure out if there are recurrences here we care about
                if (itemReturnOccurrences && item.recurrenceInfo)
                {
                    // there might be some recurrences here that we need to handle
                    var recs = item.recurrenceInfo.getOccurrences (aRangeStart, aRangeEnd, 0, {});
                    itemsFound = itemsFound.concat(recs);
                } else if (itemEndTime >= startTime) {
                    itemsFound.push(item);
                }
            }

            if (aCount && itemsFound.length >= aCount)
                break;
        }

        aListener.onGetResult (this.calendarToReturn,
                               Components.results.NS_OK,
                               typeIID,
                               null,
                               itemsFound.length,
                               itemsFound);

        aListener.onOperationComplete (this.calendarToReturn,
                                       Components.results.NS_OK,
                                       aListener.GET,
                                       null,
                                       null);
    },

    startBatch: function ()
    {
        this.observeBatchChange(true);
    },
    endBatch: function ()
    {
        this.observeBatchChange(false);
    },

    //
    // Helper functions
    //
    observeBatchChange: function (aNewBatchMode) {
        for each (obs in this.mObservers) {
            if (aNewBatchMode)
                obs.onStartBatch ();
            else
                obs.onEndBatch ();
        }
    },

    observeAddItem: function (aItem) {
        for each (obs in this.mObservers)
            obs.onAddItem (aItem);
    },

    observeModifyItem: function (aOldItem, aNewItem) {
        for each (obs in this.mObservers)
            obs.onModifyItem (aOldItem, aNewItem);
    },

    observeDeleteItem: function (aDeletedItem) {
        for each (obs in this.mObservers)
            obs.onDeleteItem (aDeletedItem);
    },
}

/****
 **** module registration
 ****/

var calMemoryCalendarModule = {
    mCID: Components.ID("{bda0dd7f-0a2f-4fcf-ba08-5517e6fbf133}"),
    mContractID: "@mozilla.org/calendar/calendar;1?type=memory",
    
    registerSelf: function (compMgr, fileSpec, location, type) {
        compMgr = compMgr.QueryInterface(Components.interfaces.nsIComponentRegistrar);
        compMgr.registerFactoryLocation(this.mCID,
                                        "Calendar in-memory back-end",
                                        this.mContractID,
                                        fileSpec,
                                        location,
                                        type);
    },

    getClassObject: function (compMgr, cid, iid) {
        if (!cid.equals(this.mCID))
            throw Components.results.NS_ERROR_NO_INTERFACE;

        if (!iid.equals(Components.interfaces.nsIFactory))
            throw Components.results.NS_ERROR_NOT_IMPLEMENTED;

        return this.mFactory;
    },

    mFactory: {
        createInstance: function (outer, iid) {
            if (outer != null)
                throw Components.results.NS_ERROR_NO_AGGREGATION;
            return (new calMemoryCalendar()).QueryInterface(iid);
        }
    },

    canUnload: function(compMgr) {
        return true;
    }
};

function NSGetModule(compMgr, fileSpec) {
    return calMemoryCalendarModule;
}
