/// <summary>
        /// Returns the free busy times for the specified exchange users
        /// </summary>
        /// <param name="users">The user which free/busy blocks will be looked up for</param>
        /// <param name="window">The time period to look up FB info for</param>
        /// <returns></returns>
        public Dictionary<ExchangeUser, FreeBusy> LookupFreeBusyTimes(
            ExchangeUserDict users,
            DateTimeRange window )
        {
            /* Create an array of mailboxes to retrieve from exchange */
            Dictionary<ExchangeUser, FreeBusy> result = new Dictionary<ExchangeUser, FreeBusy>();

            try
            {
                using (BlockTimer bt = new BlockTimer("LookupFreeBusyTimes"))
                {
                    /* Perform the retrieval of free busy times through WebDav */
                    result = webDavQuery.LoadFreeBusy(exchangeServerUrl, users, window);
                }
            }
            catch (Exception ex)
            {
                throw new GCalExchangeException(
                    GCalExchangeErrorCode.ExchangeUnreachable,
                   "Error occured while retrieving free busy ranges",
                   ex);
            }

            return result;
        }
        /// <summary>
        /// Load a set of appointments from the server
        /// </summary>
        /// <param name="folderUrl">The mailbox URL</param>
        /// <param name="start">Start time of date range to fetch</param>
        /// <param name="end">End time of date range to fetch</param>
        /// <returns>A list of appointments from the mailbox</returns>
        public virtual List<Appointment> LoadAppointments(
            string folderUrl, DateTime start, DateTime end)
        {
            using (BlockTimer bt = new BlockTimer("LoadAppointments"))
            {
                string request = WebDavXmlResources.GetText(
                    WebDavXmlResources.FindAppointments,
                    folderUrl,
                    DateUtil.FormatDateForDASL(start),
                    DateUtil.FormatDateForDASL(end));

                string response = IssueRequestAndFetchReponse(folderUrl, Method.SEARCH, request);
                XmlDocument responseXML = ParseExchangeXml(response);
                return ParseAppointmentResultSetXml(responseXML);
            }
        }
        /// <summary>
        /// Perform sync of users from Google Calendar to Exchnage until there are no more
        /// users to sync. This can be called from a worker thread.
        /// </summary>
        public void SyncUsers()
        {
            ExchangeUser user;
            int userCount = 0;
            string login = string.Empty;

            errorThreshold = ConfigCache.ServiceErrorCountThreshold;
            errorCount = 0;

            while ( exchangeUsers.Count > 0 )
            {
                lock ( lockObject )
                {
                    if ( exchangeUsers.Count == 0 )
                    {
                        break;
                    }
                    else
                    {
                        user = exchangeUsers[0];
                        exchangeUsers.RemoveAt( 0 );
                    }
                }

                try
                {
                    userCount++;

                    login = user.Email.ToLower();

                    DateTime modifiedDate = modifiedDateUtil.GetModifiedDateForUser(login);
                    DateTime currentDate = DateUtil.NowUtc;

                    // Pick a window to synchronize for:
                    //
                    // [-N, +2N] days where N is settable in the config file
                    //
                    // Scanning back in time is necessary so that we pickup changes to meetings and events that were
                    // made invisible.
                    //
                    // TODO: The window we're syncing for should also be used when we modify events in Exchange
                    // so we only modify events in the window.

                    DateTime start = currentDate.AddDays(-ConfigCache.GCalSyncWindow);
                    DateTime end = currentDate.AddDays(2 * ConfigCache.GCalSyncWindow);
                    DateTimeRange syncWindow = new DateTimeRange(start, end);

                    log.InfoFormat("Processing user {0} for {1}", login, syncWindow);

                    EventFeed feed = gcalGateway.QueryGCal(
                        user.Email,
                        GCalVisibility.Private,
                        ConfigCache.FreeBusyDetailLevel,
                        modifiedDate,
                        syncWindow);

                    // if feed is null, then that means no calendar items changed for the user since we last queried.
                    if ( feed == null )
                    {
                        log.DebugFormat(
                            "Calendar has not changed for user {0}.  User will not be synced this round.",
                            login );

                        continue;
                    }

                    if ( !ValidateFeed( feed ) )
                    {
                        /* No Google App Feed was returned,  skip to next user */
                        log.WarnFormat(
                            "GCal feed could not be read for '{0}'.  This user may not have activated their account or may be inactive." +
                            "For resources the admin must share their calendars for the domain and set correctly the timezone.",
                            login );

                        continue;
                    }

                    log.InfoFormat("Calendar Query returned {0} events", feed.Entries.Count);

                    using (BlockTimer freeBusyTimer = new BlockTimer("WriteFreeBusy"))
                    {
                        /* User and feed retrieval was succesful, merge the two datasources down into Exchange */
                        freeBusyWriter.SyncUser( user, feed, exchangeGateway, syncWindow );
                    }

                    // Only update the modified time if we sync'd
                    modifiedDateUtil.UpdateUserModifiedTime(login, currentDate);
                }
                catch ( Exception ex )
                {
                    Interlocked.Increment( ref errorCount );

                    log.Error( string.Format(
                        "Error occured while executing sync process for user '{0}'. [running error count={1}]", login, errorCount ),
                        ex );
                }

                if ( errorCount > errorThreshold )
                    throw new GCalExchangeException(
                        GCalExchangeErrorCode.GenericError,
                        "Error threshold has been surpassed, aborting sync process." );

            }

            log.InfoFormat( "User synchronization complete.  {0} users processed.", userCount );
        }
        /// <summary>
        /// Run the sync process from Google Calendar to Exchange
        /// </summary>
        /// <param name="threadCount">Number of threads to use to sync</param>
        public void RunSyncProcess( int threadCount )
        {
            log.Info( "Exchange synchronization process started." );

            using ( BlockTimer bt = new BlockTimer( "RunSyncProcess" ) )
            {
                gcalGateway =
                    new GCalGateway(googleAppsLogin, googleAppsPassword, googleAppsDomain, null);
                exchangeGateway =
                    new ExchangeService(exchangeServer, networkLogin, networkPassword);

                ExchangeUserDict users;

                using ( BlockTimer loadUserTimer = new BlockTimer( "LoadUserList" ) )
                {
                    users = QueryExchangeForValidUsers();
                }

                exchangeUsers = new List<ExchangeUser>();

                freeBusyWriter = FreeBusyWriterFactory.GetWriter( exchangeUsers );

                foreach ( ExchangeUser user in users.Values )
                {
                    exchangeUsers.Add( user );
                }

                if ( exchangeUsers.Count == 0 )
                {
                    log.Warn( "No eligible users found for synchronization.  Aborting sync process." );
                    return;
                }

                if ( threadCount < 1 )
                    threadCount = 1;

                modifiedDateUtil = new ModifiedDateUtil( ConfigCache.ServiceModifiedXmlFileName );
                modifiedDateUtil.LoadModifiedTimes();

                if ( threadCount == 1 )
                {
                    SyncUsers();
                }
                else
                {
                    StartSyncUserThreads( threadCount );
                }

                modifiedDateUtil.PersistModifiedTimes();

                gcalGateway = null;
                exchangeGateway = null;
                freeBusyWriter = null;
                modifiedDateUtil = null;
                exchangeUsers = null;

                System.GC.Collect();
                log.DebugFormat("Memory after sync: {0}", System.GC.GetTotalMemory(false));
            }
        }
        /// <summary>
        /// Combines the free busy and appointment blocks supplied to the exchange user object
        /// If no appointments are supplied the user will still have free busy time blocks assigned
        /// to them, with a null appointment assigned to the free busy time.
        /// </summary>
        /// <param name="exchangeUser">Exchange users to apply freeBusy and appointments</param>
        /// <param name="freeBusy">The collection of FreeBusy blocks to assign to exchangeUser</param>
        /// <param name="appointments">The collection of appointment blocks to assign to exchangeUser</param>
        /// <param name="window">Window to merge for</param>
        protected void MergeFreeBusyWithAppointments(
            ExchangeUser exchangeUser,
            FreeBusy freeBusy,
            List<Appointment> appointments,
            DateTimeRange window)
        {
            using (BlockTimer bt = new BlockTimer("MergeFreeBusyWithAppointments"))
            {
                IntervalTree<FreeBusyTimeBlock> busyIntervals =
                    new IntervalTree<FreeBusyTimeBlock>();
                FreeBusyCollection busyTimes = new FreeBusyCollection();
                int appointmentsCount = 0;
                List<DateTimeRange> combinedTimes =
                    FreeBusyConverter.MergeFreeBusyLists(freeBusy.All, freeBusy.Tentative);

                /* Add the date ranges from each collection in the FreeBusy object */
                ConvertFreeBusyToBlocks(window,
                                        combinedTimes,
                                        busyTimes,
                                        busyIntervals);

                if (appointments != null && appointments.Count > 0)
                {
                    appointmentsCount = appointments.Count;
                    foreach (Appointment appt in appointments)
                    {
                        log.DebugFormat("Appt \"{0}\" {1} {2} response = {3} status = {4} busy = {5}",
                                        appt.Subject,
                                        appt.Range,
                                        appt.StartDate.Kind,
                                        appt.ResponseStatus,
                                        appt.MeetingStatus,
                                        appt.BusyStatus);

                        if (appt.BusyStatus == BusyStatus.Free)
                        {
                            continue;
                        }

                        DateTimeRange range = new DateTimeRange(appt.StartDate, appt.EndDate);
                        List<FreeBusyTimeBlock> result =
                            busyIntervals.FindAll(range, IntervalTreeMatch.Overlap);

                        log.DebugFormat("Found {0} ranges overlap {1}", result.Count, range);

                        foreach (FreeBusyTimeBlock block in result)
                        {
                            log.DebugFormat("Adding \"{0}\" to FB {1} {2}",
                                            appt.Subject,
                                            block.Range,
                                            block.StartDate.Kind);
                            block.Appointments.Add(appt);
                        }

                        busyTimes.Appointments.Add(appt);
                    }
                }

                foreach (FreeBusyTimeBlock block in busyTimes.Values)
                {
                    block.Appointments.Sort(CompareAppointmentsByRanges);
                }

                log.InfoFormat("Merge Result of {0} + Appointment {1} -> {2}",
                               combinedTimes.Count,
                               appointmentsCount,
                               busyTimes.Count);

                /* Assign the data structure to the exchange user */
                exchangeUser.BusyTimes = busyTimes;
            }
        }
        private EventFeed QueryGCal(FeedQuery query, string userName, DateTime modifiedSince)
        {
            if (log.IsInfoEnabled)
                log.Info(String.Format("Querying GCal for User '{0}': {1}", userName, query.Uri));

            EventFeed feed = null;

            // Wait as necessary before making new request.

            connectionThrottler.WaitBeforeNewConnection();

            try
            {
                using (BlockTimer bt = new BlockTimer("QueryGCal"))
                {
                    feed = service.Query(query, modifiedSince) as EventFeed;
                }

            }
            catch (GDataNotModifiedException e)
            {
                // Content was not modified
                log.InfoFormat("NotModified: {0}", e.Message);
            }
            catch (Exception)
            {
                // Report a failure, regardless of the exception, as long as it is not GDataNotModifiedException.
                // This could be a bit overly aggressive, but it is hard to make bets
                // what exception was caught and rethrown in GData and what was let to fly.
                connectionThrottler.ReportFailure();

                throw;
            }

            // Everything went well, report it.
            // Note this is valid even when we caught GDataNotModifiedException.
            connectionThrottler.ReportSuccess();

            LogResponse(feed, userName);

            return feed;
        }