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

Components.utils.import("resource:///modules/mailServices.js");
Components.utils.import("resource://calendar/modules/calUtils.jsm");
Components.utils.import("resource://calendar/modules/calAuthUtils.jsm");
Components.utils.import("resource://gre/modules/Services.jsm");
Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
Components.utils.import("resource://gre/modules/Preferences.jsm");

/*
 * Provider helper code
 */

EXPORTED_SYMBOLS = ["cal"]; // even though it's defined in calUtils.jsm, import needs this

/**
 * Prepare HTTP channel with standard request headers and upload
 * data/content-type if needed
 *
 * @param arUri                      Channel Uri, will only be used for a new
 *                                     channel.
 * @param aUploadData                Data to be uploaded, if any. This may be a
 *                                     nsIInputStream or string data. In the
 *                                     latter case the string will be converted
 *                                     to an input stream.
 * @param aContentType               Value for Content-Type header, if any
 * @param aNotificationCallbacks     Calendar using channel
 * @param aExisting                  An existing channel to modify (optional)
 */
cal.prepHttpChannel = function calPrepHttpChannel(aUri, aUploadData, aContentType, aNotificationCallbacks, aExisting) {
    let channel = aExisting || Services.io.newChannelFromURI(aUri);
    let httpchannel = channel.QueryInterface(Components.interfaces.nsIHttpChannel);

    httpchannel.setRequestHeader("Accept", "text/xml", false);
    httpchannel.setRequestHeader("Accept-Charset", "utf-8,*;q=0.1", false);
    httpchannel.loadFlags |= Components.interfaces.nsIRequest.LOAD_BYPASS_CACHE;
    httpchannel.notificationCallbacks = aNotificationCallbacks;

    if (aUploadData) {
        httpchannel = httpchannel.QueryInterface(Components.interfaces.nsIUploadChannel);
        let stream;
        if (aUploadData instanceof Components.interfaces.nsIInputStream) {
            // Make sure the stream is reset
            stream = aUploadData.QueryInterface(Components.interfaces.nsISeekableStream);
            stream.seek(Components.interfaces.nsISeekableStream.NS_SEEK_SET, 0);
        } else {
            // Otherwise its something that should be a string, convert it.
            let converter = Components.classes["@mozilla.org/intl/scriptableunicodeconverter"]
                                      .createInstance(Components.interfaces.nsIScriptableUnicodeConverter);
            converter.charset = "UTF-8";
            stream = converter.convertToInputStream(aUploadData.toString());
        }

        httpchannel.setUploadStream(stream, aContentType, -1);
    }

    return httpchannel;
};

/**
 * calSendHttpRequest; send prepared HTTP request
 *
 * @param aStreamLoader     streamLoader for request
 * @param aChannel          channel for request
 * @param aListener         listener for method completion
 */
cal.sendHttpRequest = function calSendHttpRequest(aStreamLoader, aChannel, aListener) {
    aStreamLoader.init(aListener);
    aChannel.asyncOpen(aStreamLoader, aChannel);
};

cal.createStreamLoader = function calCreateStreamLoader() {
    return Components.classes["@mozilla.org/network/stream-loader;1"]
                     .createInstance(Components.interfaces.nsIStreamLoader);
};

cal.convertByteArray = function calConvertByteArray(aResult, aResultLength, aCharset, aThrow) {
    try {
        let resultConverter = Components.classes["@mozilla.org/intl/scriptableunicodeconverter"]
                                        .createInstance(Components.interfaces.nsIScriptableUnicodeConverter);
        resultConverter.charset = aCharset || "UTF-8";
        return resultConverter.convertFromByteArray(aResult, aResultLength);
    } catch (e) {
        if (aThrow) {
            throw e;
        }
    }
    return null;
};

/**
 * getInterface method for providers. This should be called in the context of
 * the respective provider, i.e
 *
 * return cal.InterfaceRequestor_getInterface.apply(this, arguments);
 *
 * or
 * ...
 * getInterface: cal.InterfaceRequestor_getInterface,
 * ...
 *
 * NOTE: If the server only provides one realm for all calendars, be sure that
 * the |this| object implements calICalendar. In this case the calendar name
 * will be appended to the realm. If you need that feature disabled, see the
 * capabilities section of calICalendar.idl
 *
 * @param aIID      The interface ID to return
 */
cal.InterfaceRequestor_getInterface = function calInterfaceRequestor_getInterface(aIID) {
    try {
        // Try to query the this object for the requested interface but don't
        // throw if it fails since that borks the network code.
        return this.QueryInterface(aIID);
    } catch (e) {
        // Support Auth Prompt Interfaces
        if (aIID.equals(Components.interfaces.nsIAuthPrompt2)) {
            if (!this.calAuthPrompt) {
                this.calAuthPrompt = new cal.auth.Prompt(this);
            }
            return this.calAuthPrompt;
        } else if (aIID.equals(Components.interfaces.nsIAuthPromptProvider) ||
                   aIID.equals(Components.interfaces.nsIPrompt)) {
            return Services.ww.getNewPrompter(null);
        } else if (aIID.equals(Components.interfaces.nsIBadCertListener2)) {
            if (!this.badCertHandler) {
                this.badCertHandler = new cal.BadCertHandler(this);
            }
            return this.badCertHandler;
        } else if (aIID.equals(Components.interfaces.nsIWebNavigation)) {
            return new cal.LoadContext();
        } else {
            Components.returnCode = e;
        }
    }
    return null;
};

/**
 * Bad Certificate Handler for Network Requests. Shows the Network Exception
 * Dialog if a certificate Problem occurs.
 */
cal.BadCertHandler = function calBadCertHandler(thisProvider) {
    this.thisProvider = thisProvider;
};
cal.BadCertHandler.prototype = {
    QueryInterface: XPCOMUtils.generateQI([Components.interfaces.nsIBadCertListener2]),

    notifyCertProblem: function cBCL_notifyCertProblem(socketInfo, status, targetSite) {
        if (!status) {
            return true;
        }

        // Unfortunately we can't pass js objects using the window watcher, so
        // we'll just take the first available calendar window. We also need to
        // do this on a timer so that the modal window doesn't block the
        // network request.
        let calWindow = cal.getCalendarWindow();

        let timerCallback = {
            thisProvider: this.thisProvider,
            notify: function(timer) {
                let params = { exceptionAdded: false,
                               prefetchCert: true,
                               location: targetSite };
                calWindow.openDialog("chrome://pippki/content/exceptionDialog.xul",
                                     "",
                                     "chrome,centerscreen,modal",
                                     params);
                if (this.thisProvider.canRefresh &&
                    params.exceptionAdded) {
                    // Refresh the provider if the
                    // exception certificate was added
                    this.thisProvider.refresh();
                }
            }
        };
        let timer = Components.classes["@mozilla.org/timer;1"]
                    .createInstance(Components.interfaces.nsITimer);
        timer.initWithCallback(timerCallback,
                               0,
                               Components.interfaces.nsITimer.TYPE_ONE_SHOT);
        return true;
    }
};

/**
 * Implements an nsILoadContext that allows auth prompts to avoid using private
 * browsing without a parent DOM window
 */
cal.LoadContext = function calLoadContext() {
};
cal.LoadContext.prototype = {
    QueryInterface: XPCOMUtils.generateQI([Components.interfaces.nsISupports,
                                           Components.interfaces.nsILoadContext]),
    associatedWindow: null,
    topWindow: null,
    topFrameElement: null,
    isAppOfType: function() false,
    isContent: false,
    usePrivateBrowsing: false,
    isInBrowserElement: false,
    appId: null
};

/**
 * Freebusy interval implementation. All parameters are optional.
 *
 * @param aCalId         The calendar id to set up with.
 * @param aFreeBusyType  The type from calIFreeBusyInterval.
 * @param aStart         The start of the interval.
 * @param aEnd           The end of the interval.
 * @return               The fresh calIFreeBusyInterval.
 */
cal.FreeBusyInterval = function calFreeBusyInterval(aCalId, aFreeBusyType, aStart, aEnd) {
    this.calId = aCalId
    this.interval = Components.classes["@mozilla.org/calendar/period;1"]
                              .createInstance(Components.interfaces.calIPeriod);
    this.interval.start = aStart;
    this.interval.end = aEnd;

    this.freeBusyType = aFreeBusyType || Components.interfaces.calIFreeBusyInterval.UNKNOWN;
};
cal.FreeBusyInterval.prototype = {
    QueryInterface: XPCOMUtils.generateQI([Components.interfaces.calIFreeBusyInterval]),
    calId: null,
    interval: null,
    freeBusyType: Components.interfaces.calIFreeBusyInterval.UNKNOWN
};

/**
 * Gets the iTIP/iMIP transport if the passed calendar has configured email.
 */
cal.getImipTransport = function calGetImipTransport(aCalendar) {
    // assure an identity is configured for the calendar
    return (aCalendar.getProperty("imip.identity")
            ? Components.classes["@mozilla.org/calendar/itip-transport;1?type=email"]
                        .getService(Components.interfaces.calIItipTransport)
            : null);
};

/**
 * Gets the configured identity and account of a particular calendar instance, or null.
 *
 * @param aCalendar     Calendar instance
 * @param outAccount    Optional out value for account
 * @return              The configured identity
 */
cal.getEmailIdentityOfCalendar = function calGetEmailIdentityOfCalendar(aCalendar, outAccount) {
    cal.ASSERT(aCalendar, "no calendar!", Components.results.NS_ERROR_INVALID_ARG);
    let key = aCalendar.getProperty("imip.identity.key");
    if (key !== null) {
        if (key.length == 0) { // i.e. "None"
            return null;
        }
        let identity = null;
        cal.calIterateEmailIdentities(
            function(identity_, account) {
                if (identity_.key == key) {
                    identity = identity_;
                    if (outAccount) {
                        outAccount.value = account;
                    }
                }
                return (identity_.key != key);
            });
        if (!identity) {
            // dangling identity:
            cal.WARN("Calendar " + (aCalendar.uri ? aCalendar.uri.spec : aCalendar.id) +
                     " has a dangling E-Mail identity configured.");
        }
        return identity;
    } else { // take default account/identity:

        let accounts = MailServices.accounts.accounts;
        let account = null;
        let identity = null;
        try {
            account = MailServices.accounts.defaultAccount;
        } catch (exc) {}

        for (let i = 0; accounts && (i < accounts.length) && (!account || !identity); ++i) {
            if (!account) { // Pick an account only if none was set (i.e there is no default account)
                try {
                  account = accounts.queryElementAt(i, Components.interfaces.nsIMsgAccount);
                } catch (exc) {
                  account = null;
                }
            }

            if (account && account.identities.length) { // Pick an identity
                identity = account.defaultIdentity;
                if (!identity) { // there is no default identity, use the first
                    identity = account.identities.queryElementAt(0, Components.interfaces.nsIMsgIdentity);
                }
            } else { // If this account has no identities, continue to the next account.
                account = null;
            }
        }

        if (identity) {
            // If an identity was set above, set the account out parameter
            // and return the identity
            if (outAccount) {
                outAccount.value = account;
            }
            return identity;
        }
        return null;
    }
};


/**
 * fromRFC3339
 * Convert a RFC3339 compliant Date string to a calIDateTime.
 *
 * @param aStr          The RFC3339 compliant Date String
 * @param aTimezone     The timezone this date string is most likely in
 * @return              A calIDateTime object
 */
cal.fromRFC3339 = function fromRFC3339(aStr, aTimezone) {

    // XXX I have not covered leapseconds (matches[8]), this might need to
    // be done. The only reference to leap seconds I found is bug 227329.
    //

    // Create a DateTime instance (calUtils.js)
    let dateTime = cal.createDateTime();

    // Killer regex to parse RFC3339 dates
    var re = new RegExp("^([0-9]{4})-([0-9]{2})-([0-9]{2})" +
        "([Tt]([0-9]{2}):([0-9]{2}):([0-9]{2})(\\.[0-9]+)?)?" +
        "(([Zz]|([+-])([0-9]{2}):([0-9]{2})))?");

    var matches = re.exec(aStr);

    if (!matches) {
        return null;
    }

    // Set usual date components
    dateTime.isDate = (matches[4]==null);

    dateTime.year = matches[1];
    dateTime.month = matches[2] - 1; // Jan is 0
    dateTime.day = matches[3];

    if (!dateTime.isDate) {
        dateTime.hour = matches[5];
        dateTime.minute = matches[6];
        dateTime.second = matches[7];
    }

    // Timezone handling
    if (matches[9] == "Z" || matches[9] == "z") {
        // If the dates timezone is "Z" or "z", then this is UTC, no matter
        // what timezone was passed
        dateTime.timezone = cal.UTC();

    } else if (matches[9] == null) {
        // We have no timezone info, only a date. We have no way to
        // know what timezone we are in, so lets assume we are in the
        // timezone of our local calendar, or whatever was passed.

        dateTime.timezone = aTimezone;

    } else {
        let offset_in_s = (matches[11] == "-" ? -1 : 1) *
            ( (matches[12] * 3600) + (matches[13] * 60) );

        // try local timezone first
        dateTime.timezone = aTimezone;

        // If offset does not match, go through timezones. This will
        // give you the first tz in the alphabet and kill daylight
        // savings time, but we have no other choice
        if (dateTime.timezoneOffset != offset_in_s) {
            // TODO A patch to Bug 363191 should make this more efficient.

            let tzService = cal.getTimezoneService();
            // Enumerate timezones, set them, check their offset
            let enumerator = tzService.timezoneIds;
            while (enumerator.hasMore()) {
                let id = enumerator.getNext();
                dateTime.timezone = tzService.getTimezone(id);
                if (dateTime.timezoneOffset == offset_in_s) {
                    // This is our last step, so go ahead and return
                    return dateTime;
                }
            }
            // We are still here: no timezone was found
            dateTime.timezone = cal.UTC();
            if (!dateTime.isDate) {
                dateTime.hour += (matches[11] == "-" ? -1 : 1) * matches[12];
                dateTime.minute += (matches[11] == "-" ? -1 : 1) * matches[13];
             }
        }
    }
    return dateTime;
};

/**
 * toRFC3339
 * Convert a calIDateTime to a RFC3339 compliant Date string
 *
 * @param aDateTime     The calIDateTime object
 * @return              The RFC3339 compliant date string
 */
cal.toRFC3339 = function toRFC3339(aDateTime) {

    if (!aDateTime) {
        return "";
    }

    var full_tzoffset = aDateTime.timezoneOffset;
    var tzoffset_hr = Math.floor(Math.abs(full_tzoffset) / 3600);

    var tzoffset_mn = ((Math.abs(full_tzoffset) / 3600).toFixed(2) -
                       tzoffset_hr) * 60;

    var str = aDateTime.year + "-" +
        ("00" + (aDateTime.month + 1)).substr(-2) +  "-" +
        ("00" + aDateTime.day).substr(-2);

    // Time and Timezone extension
    if (!aDateTime.isDate) {
        str += "T" +
               ("00" + aDateTime.hour).substr(-2) + ":" +
               ("00" + aDateTime.minute).substr(-2) + ":" +
               ("00" + aDateTime.second).substr(-2);
        if (aDateTime.timezoneOffset != 0) {
            str += (full_tzoffset < 0 ? "-" : "+") +
                   ("00" + tzoffset_hr).substr(-2) + ":" +
                   ("00" + tzoffset_mn).substr(-2);
        } else if (aDateTime.timezone.isFloating) {
            // RFC3339 Section 4.3 Unknown Local Offset Convention
            str += "-00:00";
        } else {
            // ZULU Time, according to ISO8601's timezone-offset
            str += "Z";
        }
    }
    return str;
};

cal.promptOverwrite = function cal_promptOverwrite(aMode, aItem) {
    let window = cal.getCalendarWindow();
    let args = { item: aItem,
                 mode: aMode,
                 overwrite: false };

    window.openDialog("chrome://calendar/content/calendar-conflicts-dialog.xul",
                      "calendarConflictsDialog",
                      "chrome,titlebar,modal",
                      args);

    return args.overwrite;
};

/**
 * Observer bag implementation taking care to replay open batch notifications.
 */
cal.ObserverBag = function calObserverBag(iid) {
    this.init(iid);
};
cal.ObserverBag.prototype = {
    __proto__: cal.calListenerBag.prototype,

    mBatchCount: 0,
    notify: function calObserverBag_notify(func, args) {
        switch (func) {
            case "onStartBatch":
                 ++this.mBatchCount;
                 break;
            case "onEndBatch":
                 --this.mBatchCount;
                 break;
        }
        return this.__proto__.__proto__.notify.apply(this, arguments);
    },

    add: function calObserverBag_add(iface) {
        if (this.__proto__.__proto__.add.apply(this, arguments) && (this.mBatchCount > 0)) {
            // Replay batch notifications, because the onEndBatch notifications are yet to come.
            // We may think about doing the reverse on remove, though I currently see no need:
            for (var i = this.mBatchCount; i--;) {
                iface.onStartBatch();
            }
        }
    }
};

/**
 * Base prototype to be used implementing a provider.
 *
 * @see e.g. providers/gdata
 */
cal.ProviderBase = function calProviderBase() {
    cal.ASSERT("This prototype should only be inherited!");
};
cal.ProviderBase.mTransientProperties = {
    "cache.uncachedCalendar": true,
    "currentStatus": true,
    "itip.transport": true,
    "imip.identity": true,
    "imip.account": true,
    "imip.identity.disabled": true,
    "organizerId": true,
    "organizerCN": true
};
cal.ProviderBase.prototype = {
    QueryInterface: XPCOMUtils.generateQI([
        Components.interfaces.calICalendar,
        Components.interfaces.calISchedulingSupport
    ]),

    mID: null,
    mUri: null,
    mACLEntry: null,
    mObservers: null,
    mProperties: null,

    initProviderBase: function cPB_initProviderBase() {
        this.wrappedJSObject = this;
        this.mObservers = new cal.ObserverBag(Components.interfaces.calIObserver);
        this.mProperties = {};
        this.mProperties.currentStatus = Components.results.NS_OK;
    },

    get observers() {
        return this.mObservers;
    },

    // attribute AUTF8String id;
    get id() {
        return this.mID;
    },
    set id(aValue) {
        if (this.mID) {
            throw Components.results.NS_ERROR_ALREADY_INITIALIZED;
        }
        this.mID = aValue;

        let calMgr = cal.getCalendarManager();

        // make all properties persistent that have been set so far:
        for (let aName in this.mProperties) {
            if (!cal.ProviderBase.mTransientProperties[aName]) {
                let aValue = this.mProperties[aName];
                if (aValue !== null) {
                    calMgr.setCalendarPref_(this, aName, aValue);
                }
            }
        }

        let this_ = this;
        function takeOverIfNotPresent(oldPref, newPref, dontDeleteOldPref) {
            let val = calMgr.getCalendarPref_(this_, oldPref);
            if (val !== null) {
                if (!dontDeleteOldPref) {
                    calMgr.deleteCalendarPref_(this_, oldPref);
                }
                if (calMgr.getCalendarPref_(this_, newPref) === null) {
                    calMgr.setCalendarPref_(this_, newPref, val);
                }
            }
        }
        // takeover lightning calendar visibility from 0.5:
        takeOverIfNotPresent("lightning-main-in-composite", "calendar-main-in-composite");
        takeOverIfNotPresent("lightning-main-default", "calendar-main-default");

        return aValue;
    },

    // attribute AUTF8String name;
    get name() {
        return this.getProperty("name");
    },
    set name(aValue) {
        return this.setProperty("name", aValue);
    },

    // readonly attribute calICalendarACLManager aclManager;
    get aclManager() {
        const defaultACLProviderClass = "@mozilla.org/calendar/acl-manager;1?type=default";
        let providerClass = this.getProperty("aclManagerClass");
        if (!providerClass || !Components.classes[providerClass]) {
            providerClass = defaultACLProviderClass;
        }
        return Components.classes[providerClass].getService(Components.interfaces.calICalendarACLManager);
    },

    // readonly attribute calICalendarACLEntry aclEntry;
    get aclEntry() {
        return this.mACLEntry;
    },

    // attribute calICalendar superCalendar;
    get superCalendar() {
        // If we have a superCalendar, check this calendar for a superCalendar.
        // This will make sure the topmost calendar is returned
        return (this.mSuperCalendar ? this.mSuperCalendar.superCalendar : this);
    },
    set superCalendar(val) {
        return (this.mSuperCalendar = val);
    },

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

    // attribute boolean readOnly;
    get readOnly() {
        return this.getProperty("readOnly");
    },
    set readOnly(aValue) {
        return this.setProperty("readOnly", aValue);
    },

    // readonly attribute boolean canRefresh;
    get canRefresh() {
        return false;
    },

    // void startBatch();
    mBatchCount: 0,
    startBatch: function cPB_startBatch() {
        if (this.mBatchCount++ == 0) {
            this.mObservers.notify("onStartBatch");
        }
    },

    endBatch: function cPB_endBatch() {
        if (this.mBatchCount > 0) {
            if (--this.mBatchCount == 0) {
                this.mObservers.notify("onEndBatch");
            }
        } else {
            cal.ASSERT(this.mBatchCount > 0, "unexepcted endBatch!");
        }
    },

    notifyPureOperationComplete: function cPB_notifyPureOperationComplete(aListener,
                                                                          aStatus,
                                                                          aOperationType,
                                                                          aId,
                                                                          aDetail) {
        if (aListener) {
            try {
                aListener.onOperationComplete(this.superCalendar, aStatus, aOperationType, aId, aDetail);
            } catch (exc) {
                cal.ERROR(exc);
            }
        }

    },

    notifyOperationComplete: function cPB_notifyOperationComplete(aListener,
                                                                  aStatus,
                                                                  aOperationType,
                                                                  aId,
                                                                  aDetail,
                                                                  aExtraMessage) {
        this.notifyPureOperationComplete(aListener, aStatus, aOperationType, aId, aDetail);

        if (aStatus == Components.interfaces.calIErrors.OPERATION_CANCELLED) {
            return; // cancellation doesn't change current status, no notification
        }
        if (Components.isSuccessCode(aStatus)) {
            this.setProperty("currentStatus", aStatus);
        } else {
            if (aDetail instanceof Components.interfaces.nsIException) {
                this.notifyError(aDetail); // will set currentStatus
            } else {
                this.notifyError(aStatus, aDetail); // will set currentStatus
            }
            this.notifyError(aOperationType == Components.interfaces.calIOperationListener.GET
                             ? Components.interfaces.calIErrors.READ_FAILED
                             : Components.interfaces.calIErrors.MODIFICATION_FAILED,
                             aExtraMessage || "");
        }
    },

    // for convenience also callable with just an exception
    notifyError: function cPB_notifyError(aErrNo, aMessage) {
        if (aErrNo == Components.interfaces.calIErrors.OPERATION_CANCELLED) {
            return; // cancellation doesn't change current status, no notification
        }
        if (aErrNo instanceof Components.interfaces.nsIException) {
            if (!aMessage) {
                aMessage = aErrNo.message;
            }
            aErrNo = aErrNo.result;
        }
        this.setProperty("currentStatus", aErrNo);
        this.observers.notify("onError", [this.superCalendar, aErrNo, aMessage]);
    },

    mTransientPropertiesMode: false,
    get transientProperties() {
        return this.mTransientPropertiesMode;
    },
    set transientProperties(value) {
        return (this.mTransientPropertiesMode = value);
    },

    // nsIVariant getProperty(in AUTF8String aName);
    getProperty: function cPB_getProperty(aName) {
        switch (aName) {
            case "itip.transport": // iTIP/iMIP default:
                return cal.getImipTransport(this);
            case "itip.notify-replies": // iTIP/iMIP default:
                 return Preferences.get("calendar.itip.notify-replies", false);
            // temporary hack to get the uncached calendar instance:
            case "cache.uncachedCalendar":
                return this;
        }

        let ret = this.mProperties[aName];
        if (ret === undefined) {
            ret = null;
            switch (aName) {
                case "imip.identity": // we want to cache the identity object a little, because
                                      // it is heavily used by the invitation checks
                    ret = cal.getEmailIdentityOfCalendar(this);
                    break;
                case "imip.account": {
                    let outAccount = {};
                    if (cal.getEmailIdentityOfCalendar(this, outAccount)) {
                        ret = outAccount.value;
                    }
                    break;
                }
                case "organizerId": { // itip/imip default: derived out of imip.identity
                    let identity = this.getProperty("imip.identity");
                    ret = (identity
                           ? ("mailto:" + identity.QueryInterface(Components.interfaces.nsIMsgIdentity).email)
                           : null);
                    break;
                }
                case "organizerCN": { // itip/imip default: derived out of imip.identity
                    let identity = this.getProperty("imip.identity");
                    ret = (identity
                           ? identity.QueryInterface(Components.interfaces.nsIMsgIdentity).fullName
                           : null);
                    break;
                }
            }
            if ((ret === null) &&
                !cal.ProviderBase.mTransientProperties[aName] &&
                !this.transientProperties) {
                if (this.id) {
                    ret = cal.getCalendarManager().getCalendarPref_(this, aName);
                }
                switch (aName) {
                    case "suppressAlarms":
                        if (this.getProperty("capabilities.alarms.popup.supported") === false) {
                            // If popup alarms are not supported,
                            // automatically suppress alarms
                            ret = true;
                        }
                        break;
                }
            }
            this.mProperties[aName] = ret;
        }
//         cal.LOG("getProperty(\"" + aName + "\"): " + ret);
        return ret;
    },

    // void setProperty(in AUTF8String aName, in nsIVariant aValue);
    setProperty: function cPB_setProperty(aName, aValue) {
        let oldValue = this.getProperty(aName);
        if (oldValue != aValue) {
            this.mProperties[aName] = aValue;
            switch (aName) {
                case "imip.identity.key": // invalidate identity and account object if key is set:
                    delete this.mProperties["imip.identity"];
                    delete this.mProperties["imip.account"];
                    delete this.mProperties["organizerId"];
                    delete this.mProperties["organizerCN"];
                    break;
            }
            if (!this.transientProperties &&
                !cal.ProviderBase.mTransientProperties[aName] &&
                this.id) {
                cal.getCalendarManager().setCalendarPref_(this, aName, aValue);
            }
            this.mObservers.notify("onPropertyChanged",
                                   [this.superCalendar, aName, aValue, oldValue]);
        }
        return aValue;
    },

    // void deleteProperty(in AUTF8String aName);
    deleteProperty: function cPB_deleteProperty(aName) {
        this.mObservers.notify("onPropertyDeleting", [this.superCalendar, aName]);
        delete this.mProperties[aName];
        cal.getCalendarManager().deleteCalendarPref_(this, aName);
    },

    // calIOperation refresh
    refresh: function cPB_refresh() {
        return null;
    },

    // void addObserver( in calIObserver observer );
    addObserver: function cPB_addObserver(aObserver) {
        this.mObservers.add(aObserver);
    },

    // void removeObserver( in calIObserver observer );
    removeObserver: function cPB_removeObserver(aObserver) {
        this.mObservers.remove(aObserver);
    },

    // calISchedulingSupport: Implementation corresponding to our iTIP/iMIP support
    isInvitation: function cPB_isInvitation(aItem) {
        if (!this.mACLEntry || !this.mACLEntry.hasAccessControl) {
            // No ACL support - fallback to the old method
            let id = this.getProperty("organizerId");
            if (id) {
                let org = aItem.organizer;
                if (!org || !org.id || (org.id.toLowerCase() == id.toLowerCase())) {
                    return false;
                }
                return (aItem.getAttendeeById(id) != null);
            }
            return false;
        }

        let org = aItem.organizer;
        if (!org || !org.id) {
            // HACK
            // if we don't have an organizer, this is perhaps because it's an exception
            // to a recurring event. We check the parent item.
            if (aItem.parentItem) {
                org = aItem.parentItem.organizer;
                if (!org || !org.id) return false;
            } else {
                return false;
            }
        }

        // We check if :
        // - the organizer of the event is NOT within the owner's identities of this calendar
        // - if the one of the owner's identities of this calendar is in the attendees
        let ownerIdentities = this.mACLEntry.getOwnerIdentities({});
        for (let i = 0; i < ownerIdentities.length; i++) {
            let identity = "mailto:" + ownerIdentities[i].email.toLowerCase();
            if (org.id.toLowerCase() == identity)
                return false;

            if (aItem.getAttendeeById(identity) != null)
                return true;
        }

        return false;
    },

    getInvitedAttendee: function cPB_getInvitedAttendee(aItem) {
        let id = this.getProperty("organizerId");
        let attendee = (id ? aItem.getAttendeeById(id) : null);

        if (!attendee && this.mACLEntry && this.mACLEntry.hasAccessControl) {
            let ownerIdentities = this.mACLEntry.getOwnerIdentities({});
            if (ownerIdentities.length > 0) {
                let identity;
                for (let i = 0; !attendee && i < ownerIdentities.length; i++) {
                    identity = "mailto:" + ownerIdentities[i].email.toLowerCase();
                    attendee = aItem.getAttendeeById(identity);
                }
            }
        }

        return attendee;
    },

    canNotify: function cPB_canNotify(aMethod, aItem) {
        return false; // use outbound iTIP for all
    }
};
