Ext.namespace('Zarafa.common.freebusy.data');

/**
 * @class Zarafa.common.freebusy.data.FreebusyModel
 * @extends Ext.util.Observable
 */
Zarafa.common.freebusy.data.FreebusyModel = Ext.extend(Ext.util.Observable,
{
	/**
	 * @cfg {Boolean} nonWorkingHoursHidden
	 * The {@link Zarafa.common.freebusy.ui.TimelineView TimelineView} must only
	 * display the working hours for the user.
	 */
	nonWorkingHoursHidden : true,
	/**
	 * @cfg {Zarafa.core.DateRange} daterange
	 * The {@link Zarafa.core.DateRange} object that determines what time period will be shown
	 * (defaults to 7 days before today till 3 weeks after).
	 */
	daterange: null,
	/**
	 * @cfg {Zarafa.core.DateRange} selectorRange
	 * The {@link Zarafa.core.DateRange} object that determines what time period will be selected
	 * (defaults to next whole or half hour and will last by default for 30 minutes).
	 */
	selectorRange: null,
	/**
	 * The {@link Zarafa.core.DateRange} object that indicates the daterange for which the
	 * suggestions for the {@link #suggestionBlockStore} must be calculated.
	 * @property
	 * @type Zarafa.common.freebusy.data.FreebusyBlockStore
	 */
	suggestionRange: null,
	/**
	 * @cfg {Zarafa.core.data.IPMRecipientStore} userStore
	 * The {@link Zarafa.core.data.IPMRecipientStore store} object that handles the freebusy blocks displayed on the timeline.
	 */
	userStore: null,
	/**
	 * @cfg {Zarafa.common.freebusy.data.FreebusyBlockStore} blockStore
	 * The {@link Zarafa.common.freebusy.data.FreebusyBlockStore store} object that handles the freebusy blocks displayed on the timeline.
	 */
	blockStore: null,
	/**
	 * @cfg {Zarafa.common.freebusy.data.FreebusyBlockStore} sumBlockStore
	 * The {@link Zarafa.common.freebusy.data.FreebusyBlockStore} object that handles the freebusy sum blocks displayed in the header of the timeline.
	 */
	sumBlockStore: null,
	/**
	 * freeBlockStore Internal store which keeps track of all blocks which mark
	 * the periods in which no user is occupied. This is used to generate the {@link #suggestionBlockStore}.
	 * @property
	 * @type Zarafa.common.freebusy.data.FreebusyBlockStore
	 */
	freeBlockStore: null,
	/**
	 * @cfg {Zarafa.common.freebusy.data.FreebusyBlockStore} suggestionBlockStore
	 * The {@link Zarafa.common.freebusy.data.FreebusyBlockStore} object that handles the freebusy sum blocks which indicates which blocks are
	 * available for new meetings.
	 */
	suggestionBlockStore: null,
	/**
	 * This property is used to prevent the firing of the selectorrangeupdate event. When an outside
	 * source changes the selector range's values this model does not fire that event.
	 * @property
	 * @type Boolean
	 */
	updatingSelectorRangeExternally: false,
	/**
	 * @constructor
	 * @param {Object} config Configuration object
	 */
	constructor: function(config)
	{
		config = config || {};

		// Use 12:00 as base time, this prevents problems for DST switches
		// at 00:00 like in Brasil.
		var now = new Date();
		now.setHours(12);

		var server = container.getServerConfig();
		var startOffset = server.getFreebusyLoadStartOffset();
		var startDate = now.add(Date.DAY, -startOffset).clearTime();
		var endOffset = server.getFreebusyLoadEndOffset();
		var endDate = now.add(Date.DAY, endOffset).clearTime();

		var selectStart, selectEnd;
		if(config.selectorRange){
			// If selectorRange has been supplied copy the startdate and duedate values.
			selectStart = config.selectorRange.getStartDate();
			selectEnd = config.selectorRange.getDueDate();
		}else{
			// Defaults to next whole or half hour and will last by default for 30 minutes
			selectStart = new Date().ceil(Date.MINUTE, 30);
			selectEnd = selectStart.add(Date.MINUTE, 30);
		}

		Ext.applyIf(config, {
			userStore: config.userStore || new Zarafa.core.data.IPMRecipientStore(),
			daterange: config.daterange || new Zarafa.core.DateRange({ startDate : startDate, dueDate : endDate}),
			selectorRange: new Zarafa.core.DateRange({ startDate : selectStart, dueDate : selectEnd}),
			blockStore: config.blockStore || new Zarafa.common.freebusy.data.FreebusyBlockStore(),
			sumBlockStore: config.sumBlockStore || new Zarafa.common.freebusy.data.FreebusyBlockStore({ remoteSort: false}),
			suggestionBlockStore: config.suggestionBlockStore || new Zarafa.common.freebusy.data.FreebusyBlockStore({ remoteSort: false})
		});

		Ext.apply(this, config);

		this.addEvents(
			/**
			 * @event showworkinghourschange
			 * Fires when the {@link #nonWorkingHoursHidden} has been changed
			 * @param {Boolean} hideNonWorkingHours True if only working hours
			 * should be shown.
			 */
			'showworkinghourschange',
			/**
			 * @event userstorechange
			 * Fires when the user store is changed.
			 * @param {Zarafa.core.data.IPMRecipientStore} newStore The new store
			 * @param {Zarafa.core.data.IPMRecipientStore} oldStore The old store
			 */
			'userstorechange',
			/**
			 * @event blockstorechange
			 * Fires when the block store is changed.
			 * @param {Zarafa.common.freebusy.data.FreebusyBlockStore} newStore The new store
			 * @param {Zarafa.common.freebusy.data.FreebusyBlockStore} oldStore The old store
			 */
			'blockstorechange',
			/**
			 * @event sumblockstorechange
			 * Fires when the sumblock store is changed.
			 * @param {Zarafa.common.freebusy.data.FreebusyBlockStore} newStore The new store
			 * @param {Zarafa.common.freebusy.data.FreebusyBlockStore} oldStore The old store
			 */
			'sumblockstorechange',
			/**
			 * @event suggestionblockstorechange
			 * Fires when the suggestionblock store is changed.
			 * @param {Zarafa.common.freebusy.data.FreebusyBlockStore} newStore The new store
			 * @param {Zarafa.common.freebusy.data.FreebusyBlockStore} oldStore The old store
			 */
			'suggestionblockstorechange',
			/**
			 * @event daterangechange
			 * Fires when the {@link Zarafa.core.DateRange DateRange} has been changed.
			 * @param {Zarafa.core.DateRange} newRange The new DateRange object.
			 * @param {Zarafa.core.DateRange} oldRange The old DateRange object.
			 */
			'daterangechange',
			/**
			 * @event selectorrangeupdate
			 * Fires when the selector {@link Zarafa.core.DateRange DateRange}
			 * has been changed.
			 * @param {Zarafa.core.DateRange} newRange The new DateRange object.
			 * @param {Zarafa.core.DateRange} oldRange The old DateRange object.
			 */
			'selectorrangeupdate'
		);

		Zarafa.common.freebusy.data.FreebusyModel.superclass.constructor.call(this, config);

		this.initEvents();

		this.setDateRange(config.daterange, true);
		this.setUserStore(config.userStore, true);
		this.setSelectorRange(config.selectorRange, true);
		this.setBlockStore(config.blockStore, true);
		this.setSumBlockStore(config.sumBlockStore, true);
		this.setSuggestionBlockStore(config.suggestionBlockStore, true);

		this.createFreeBlockStore();
		this.createSuggestionRange();
	},

	/**
	 * Initialize all {@link Zarafa.common.freebusy.data.FreebusyModel FreebusyModel} related events.
	 * @private
	 */
	initEvents: function()
	{
		this.on('showworkinghourschange', this.onShowWorkingHoursChange, this);
		this.on('userstorechange', this.onUserStoreChange, this);
	},

	/**
	 * Create a {@link Zarafa.common.freebusy.data.FreebusyBlockStore FreebusyBlockStore} which
	 * is used internally to keep track of the TimeBlocks in which all users are available.
	 * @return {Zarafa.common.freebusy.data.FreebusyBlockStore} The created store
	 * @private
	 */
	createFreeBlockStore : function()
	{
		if (!this.freeBlockStore) {
			this.freeBlockStore = new Zarafa.common.freebusy.data.FreebusyBlockStore({ remoteSort: false});
		}
		return this.freeBlockStore;
	},

	/**
	 * Create a {@link Zarafa.core.DateRange DateRange} object which is used internally to keep
	 * track of the range for which the suggestions must be calculated.
	 * @return {Zarafa.core.DateRange} The created daterange
	 * @private
	 */
	createSuggestionRange : function()
	{
		if (!this.suggestionRange) {
			this.suggestionRange = new Zarafa.core.DateRange();
			this.setSuggestionDate(this.selectorRange.getStartDate());
			this.suggestionRange.on('update', this.onSuggestionRangeUpdate, this, { buffer: 5 });
		}
		return this.suggestionRange;
	},

	/**
	 * Get the visibility of the non-working hours.
	 * @return {Boolean} True if the non-working hours are hidden
	 */
	showOnlyWorkingHours : function()
	{
		return this.nonWorkingHoursHidden;
	},

	/**
	 * Set the visibility of the non-working hours.
	 * @param {Boolean} hide True to hide the non-working hours
	 */
	hideNonWorkingHours : function(hide)
	{
		var oldNonWorkingHoursHidden = this.nonWorkingHoursHidden;
		this.nonWorkingHoursHidden = hide;
		if (oldNonWorkingHoursHidden != this.nonWorkingHoursHidden) {
			this.fireEvent('showworkinghourschange', this.nonWorkingHoursHidden);
		}
		return this.nonWorkingHoursHidden;
	},

	/**
	 * Returns the User store.
	 * @return {Zarafa.core.data.IPMRecipientStore} store
	 */
	getUserStore: function()
	{
		return this.userStore;
	},

	/**
	 * Sets the store.
	 * @param {Zarafa.core.data.IPMRecipientStore} store store
	 * @param {Boolean} initial (optional) True if this function is called
	 * during initialization.
	 * @return {Zarafa.core.data.IPMRecipientStore} store
	 */
	setUserStore: function(store, initial)
	{
		if (initial !== true && this.userStore === store) {
			return;
		}

		var oldUserStore = this.userStore;
		if (this.userStore) {
			this.userStore.un('resolved', this.onUserResolved, this);
			this.userStore.un('load', this.onUserLoad, this);
			this.userStore.un('add', this.onUserAdd, this);
			this.userStore.un('remove', this.onUserRemove, this);
			this.userStore.un('clear', this.onUserClear, this);
		}

		this.userStore = Ext.StoreMgr.lookup(store);

		if (this.userStore) {
			this.userStore.on({
				scope: this,
				resolved: this.onUserResolved,
				load: this.onUserLoad,
				add: this.onUserAdd,
				remove: this.onUserRemove,
				clear: this.onUserClear
			});
		}

		this.fireEvent('userstorechange', this.userStore, oldUserStore);
		if (this.userStore !== oldUserStore && oldUserStore && oldUserStore.autoDestroy) {
			oldUserStore.destroy();
		}

		return this.userStore;
	},

	/**
	 * Returns the block store.
	 * @return {Zarafa.common.freebusy.data.FreebusyBlockStore} store
	 */
	getBlockStore: function()
	{
		return this.blockStore;
	},

	/**
	 * Sets the store.
	 * @param {Zarafa.common.freebusy.data.FreebusyBlockStore} store
	 * @param {Boolean} initial (optional) True if this function is called
	 * during initialization.
	 * @return {Zarafa.common.freebusy.data.FreebusyBlockStore} store
	 */
	setBlockStore: function(store, initial)
	{
		if (initial !== true && this.blockStore === store) {
			return;
		}

		var oldBlockStore = this.blockStore;
		if (this.blockStore) {
			this.blockStore.un('load', this.onBlockLoad, this);
			this.blockStore.un('remove', this.onBlockRemove, this, { buffer : 100 });
			this.blockStore.un('clear', this.onBlockRemove, this);
		}

		this.blockStore = Ext.StoreMgr.lookup(store);

		if (this.blockStore) {
			this.blockStore.on('load', this.onBlockLoad, this);
			this.blockStore.on('remove', this.onBlockRemove, this, { buffer : 100 });
			this.blockStore.on('clear', this.onBlockRemove, this);
		}

		this.fireEvent('blockstorechange', this.blockStore, oldBlockStore);
		if (this.blockStore !== oldBlockStore && oldBlockStore && oldBlockStore.autoDestroy) {
			oldBlockStore.destroy();
		}

		return this.blockStore;
	},

	/**
	 * Returns the block store.
	 * @return {Zarafa.common.freebusy.data.FreebusyBlockStore} store
	 */
	getSumBlockStore : function()
	{
		return this.sumBlockStore;
	},

	/**
	 * Sets the block store.
	 * @param {Zarafa.common.freebusy.data.FreebusyBlockStore} store The sum block store
	 * @param {Boolean} initial (optional) True if this function is called
	 * during initialization.
	 * @return {Zarafa.common.freebusy.data.FreebusyBlockStore} The sum block store
	 * @private
	 */
	setSumBlockStore : function(store, initial)
	{
		if (initial !== true && this.sumBlockStore === store) {
			return;
		}

		var oldSumBlockStore = this.sumBlockStore;

		if (this.sumBlockStore) {
			this.sumBlockStore.un('load', this.onSumBlockLoad, this, { buffer: 5 });
		}

		this.sumBlockStore = Ext.StoreMgr.lookup(store);

		if (this.sumBlockStore) {
			this.sumBlockStore.on('load', this.onSumBlockLoad, this, { buffer: 5 });
		}

		this.fireEvent('sumblockstorechange', this.sumBlockStore, oldSumBlockStore);
		if (this.sumBlockStore !== oldSumBlockStore && oldSumBlockStore && oldSumBlockStore.autoDestroy) {
			oldSumBlockStore.destroy();
		}

		return this.sumBlockStore;
	},

	/**
	 * Returns the block store.
	 * @return {Zarafa.common.freebusy.data.FreebusyBlockStore} store
	 */
	getSuggestionBlockStore : function()
	{
		return this.suggestionBlockStore;
	},

	/**
	 * Sets the block store.
	 * @param {Zarafa.common.freebusy.data.FreebusyBlockStore} store The free block store
	 * @param {Boolean} initial (optional) True if this function is called
	 * during initialization.
	 * @return {Zarafa.common.freebusy.data.FreebusyBlockStore} The free block store
	 * @private
	 */
	setSuggestionBlockStore : function(store, initial)
	{
		if (initial !== true && this.suggestionBlockStore === store) {
			return;
		}

		var oldSuggestionBlockStore = this.suggestionBlockStore;

		this.suggestionBlockStore = Ext.StoreMgr.lookup(store);

		this.fireEvent('suggestionblockstorechange', this.suggestionBlockStore, oldSuggestionBlockStore);
		if (this.suggestionBlockStore !== oldSuggestionBlockStore && oldSuggestionBlockStore && oldSuggestionBlockStore.autoDestroy) {
			oldSuggestionBlockStore.destroy();
		}

		return this.suggestionBlockStore;
	},

	/**
	 * This returns the number of users inside the {@link #userStore}.
	 * @return {Number} The number of users inside the userStore.
	 */
	getUserCount : function()
	{
		return this.userStore.getCount();
	},

	/**
	 * Adds user to the user list. The freebusy information is automatically requested when the user is resolved.
	 * @param {String} name name of the user that will be resolved and added to the list
	 * @param {Zarafa.core.mapi.RecipientType} type (optional) The recipientType for the user (defaults to MAPI_TO)
	 * @return {Zarafa.core.data.IPMRecipientRecord} the record representing the newly added user.
	 */
	addUser: function(name, type)
	{
		type = Ext.isNumber(type) ? type : Zarafa.core.mapi.RecipientType.MAPI_TO;

		var record = Zarafa.core.data.RecordFactory.createRecordObjectByCustomType(Zarafa.core.data.RecordCustomObjectType.ZARAFA_RECIPIENT, {
			display_name : name,
			recipient_type : type
		});

		// Adding record to userlist store to update the userlist
		this.userStore.add(record);

		return record;
	},

	/**
	 * Returns the daterange
	 * @return {Zarafa.util.DateRange} Daterange set in this model
	 */
	getDateRange: function()
	{
		return this.daterange;
	},

	/**
	 * Sets the date range.
	 * @param {Zarafa.util.DateRange} dateRange Daterange set for this model
	 * @param {Boolean} initial (optional) True if this function is called
	 * during initialization.
	 * @return {Zarafa.util.DateRange} Daterange set for this model
	 */
	setDateRange: function(dateRange, initial)
	{
		if (initial !== true && this.daterange === dateRange) {
			return;
		}

		var oldDateRange = this.daterange;
		this.daterange = dateRange;
		this.fireEvent('daterangechange', this.daterange, oldDateRange);
		return this.daterange;
	},

	/**
	 * Returns the selector range
	 * @return {Zarafa.util.DateRange} Daterange set for the selector in this model
	 */
	getSelectorRange: function()
	{
		return this.selectorRange;
	},

	/**
	 * Sets the selector range.
	 * @param {Zarafa.util.DateRange} selectorRange Daterange set for the selector in this model
	 * @param {Boolean} initial (optional) True if this function is called
	 * during initialization.
	 * @return {Zarafa.util.DateRange} Daterange set for the selector in this model
	 * @private
	 */
	setSelectorRange: function(selectorRange, initial)
	{
		if (initial !== true && this.selectorRange === selectorRange) {
			return;
		}

		if (this.selectorRange) {
			this.selectorRange.un('update', this.onSelectorRangeUpdate, this, { buffer: 5 });
		}

		this.selectorRange = selectorRange;

		if (this.selectorRange) {
			this.selectorRange.on('update', this.onSelectorRangeUpdate, this, { buffer: 5 });
		}

		return this.selectorRange;
	},

	/**
	 * Modifies the selected range in the timeline by changing the selectorRange.
	 * @param {Date} startDate Start date
	 * @param {Date} dueDate Due date
	 */
	selectRange: function(startDate, dueDate){
		/* Set a temporary state to disable the firing of the selectorrangeupdate event. When the
		 * start and due date are equal to those currently set in the selectorRange the DateRange
		 * will not fire an update. This temporary state is only reset to false when this update is
		 * fired. To prevent problems with this we check whether the selectorRange will fire an
		 * update.
		 */
		if(!this.selectorRange.equals( new Zarafa.core.DateRange({ startDate : startDate, dueDate : dueDate}) )){
			this.updatingSelectorRangeExternally = true;
		}
		this.selectorRange.set(startDate, dueDate);
	},

	/**
	 * Returns the suggestion range
	 * @return {Zarafa.util.DateRange} Daterange set for the suggestions in this model
	 */
	getSuggestionRange: function()
	{
		return this.suggestionRange;
	},

	/**
	 * Set the date for which the suggestionlist must be calculated.
	 * This will set the {@link #suggestionRange} object to the given
	 * date, and will reset the timerange according to the working/non-working
	 * hour settings.
	 * @param {Ext.Date} startDate The start Date for the suggestions
	 * @param {Number} duration (optional) The period for the suggestions
	 */
	setSuggestionDate : function(startDate, duration)
	{
		var start = startDate.clone().clearTime();
		var due = startDate.clone().clearTime();

		if (this.nonWorkingHoursHidden) {
			start = start.add(Date.MINUTE, container.getSettingsModel().get('zarafa/v1/main/start_working_hour'));
			due = due.add(Date.MINUTE, container.getSettingsModel().get('zarafa/v1/main/end_working_hour'));
		} else {
			// Use 12:00 as time, this prevents problems
			// for DST switches at 00:00 like in Brasil.
			due.setHours(12);
			due = due.add(Date.DAY, 1).clearTime();
		}

		// If the current suggestionTime equals the new time, the
		// DateRange will not trigger the update event. However, when
		// the duration has been set, we must assume a forced update
		// must be performed. So just fire the update event manually.
		//
		if (this.suggestionRange.getStartTime() !== start.getTime()) {
			this.suggestionRange.set(start, due);
		} else if (Ext.isDefined(duration)) {
			this.suggestionRange.fireEvent('update', this.suggestionRange);
		}
	},

	/**
	 * Load freebusy data for the {@link #blockStore} for the provided users.
	 * @param {Zarafa.core.data.IPMRecipient[]} userRecords The users for which the
	 * freebusy data is requested
	 * @private
	 */
	loadFreebusyData : function(userRecords)
	{
		var dateRange = this.getDateRange();
		var loadData = {
			add: true, // All blocks will be appended to the existing list.
			actionType : Zarafa.core.Actions['list'],
			params: {
				users: [],
				start: dateRange.getStartTime() / 1000,
				end: dateRange.getDueTime() / 1000
			}
		};

		if (!Array.isArray(userRecords)) {
			userRecords = [ userRecords ];
		}

		// Collect all unique identifiers of the resolved users, we sent
		// both the record ID as the entryid. The entryid is used by the PHP
		// to collect the data for the given recipient, the userid is used
		// in the response to correlate the response to the correct recipientRecord.
		Ext.each(userRecords, function(userRecord) {
			if (userRecord.isResolved()) {
				loadData.params.users.push({
					userid : userRecord.id,
					entryid : userRecord.get('entryid')
				});
			}
		});

		// Perhaps none of the so-called "Resolved" users were
		// resolved. Thats lame, but it does mean less work for us.
		if (!Ext.isEmpty(loadData.params.users)) {
			this.blockStore.load(loadData);
		}
	},

	/**
	 * When the {@link #userStore} has resolved users, we can request all freebusy data
	 * for the resolved users.
	 * @param {Zarafa.core.data.IPMRecipientStore} userStore
	 * @param {Zarafa.core.data.IPMRecipient} userRecords
	 * @private
	 */
	onUserResolved : function(userStore, userRecords)
	{
		this.loadFreebusyData(userRecords);
	},

	/**
	 * When the {@link #userStore} has been loaded, make sure that all users will be
	 * resolved.
	 * @param {Zarafa.core.data.IPMRecipientStore} userStore
	 * @param {Zarafa.core.data.IPMRecipient} userRecords
	 * @private
	 */
	onUserLoad : function(userStore, userRecords)
	{
		this.loadFreebusyData(userRecords);
	},

	/**
	 * When an user is added to the userStore make sure that it is being resolved.
	 * @param {Zarafa.core.data.IPMRecipientStore} userStore Store
	 * @param {Zarafa.core.data.IPMRecipientRecord[]} userRecords Records of the added users
	 * @param {Number} index Index
	 * @private
	 */
	onUserAdd: function(userStore, userRecords, index)
	{
		this.loadFreebusyData(userRecords);
	},

	/**
	 * When an user is removed from the userStore removing the related data in the blockStore.
	 * @param {Zarafa.core.data.IPMRecipientStore} userStore Store
	 * @param {Zarafa.core.data.IPMRecipientRecord[]} userRecords
	 * @param {Number} index Index
	 * @private
	 */
	onUserRemove: function(userStore, userRecords, index)
	{
		Ext.each(userRecords, function(userRecord) {
			var records = this.blockStore.getRange();
			Ext.each(records, function(record) {
				if(record.get('userid') == userRecord.id) {
					this.blockStore.remove(record);
				}
			}, this);
		}, this);
	},

	/**
	 * When all users are removed from the userStore, all blocks can be removed from the blockStore.
	 * @param {Zarafa.core.data.IPMRecipientStore} userStore Store
	 * @param {Zarafa.core.data.IPMRecipientStore[]} userRecords
	 * @private
	 */
	onUserClear : function(userStore, records)
	{
		this.blockStore.removeAll();
	},

	/**
	 * Construct a {@link Ext.data.Record record} for the {@link #sumBlockStore}
	 * based on the start and end information from the given {@link Ext.data.Record record}.
	 * @param {Ext.data.Record} record The original record from which the sum block is based
	 * @return {Ext.data.Record} the record for the sumBlockStore
	 */
	createSumBlock : function(record)
	{
		return new Zarafa.common.freebusy.data.FreebusyBlockRecord({
			start: record.get('start'),
			end: record.get('end'),
			status: record.get('status')
		});
	},

	/**
	 * Construct a number of {@link Ext.data.Record records} for the {@link #suggestionStore}
	 * based on the start and end information. Calculating the number of possible suggestions
	 * is based on the start and end time. Within this range, (starting with 'start') we check
	 * if an suggestion with the given duration is possible. If it is, then a new block is created,
	 * and we move our 'start' value with the 'interval' value. If at this point we still have
	 * sufficient space until the end time, we can create another suggestion. This continues
	 * until we have reached the end of the interval.
	 */
	createSuggestionBlocks : function(start, end, duration, interval)
	{
		var blocks = [];

		// Reduce the end of the period with the duration. This way we can
		// schedule a suggestion for each interval between start and end.
		end -= duration;

		for (var i = start; i <= end; i += interval) {
			blocks.push(new Zarafa.common.freebusy.data.FreebusyBlockRecord({
				start: i,
				end: i + duration
			}));
		}

		return blocks;
	},

	/**
	 * Merge a {@link Ext.data.Record record} from the {@link #blockStore} into
	 * the {@link Ext.data.Record sumRecord} if the two overlap.
	 * @param {Ext.data.Record} record The record to merge into sumRecord
	 * @param {Ext.data.Record} sumRecord The record containing the summed data
	 * @return {Boolean} True if the record has been merged into sumRecord.
	 * @private
	 */
	mergeRecordIntoSumBlock : function(record, sumRecord)
	{
		// Status is different, we can't merge the record into sumRecord
		if (record.get('status') !== sumRecord.get('status')) {
			return false;
		}

		// Obtain the start & end timestamps for each record
		var start = record.get('start');
		var end = record.get('end');
		var sumStart = sumRecord.get('start');
		var sumEnd = sumRecord.get('end');

		// If the block is completely outside the sumBlock,
		// we can't do merge the record.
		if (end < sumStart || sumEnd < start) {
			return false;
		}

		// If the sumblock completely encapsulates the block,
		// we don't need to do anything, except marking the record as merged.
		if (sumStart <= start && end <= sumEnd) {
			return true;
		}

		// If the block completely encapsulates the sumblock,
		// we need to replace the sumblock with the new block.
		if (start < sumStart && sumEnd < end) {
			sumRecord.set('start', start);
			sumRecord.set('end', end);
			return true;
		}

		// If the block starts earlier then the sumblock,
		// we need to expand the sumblock to start earlier.
		if (start < sumStart) {
			sumRecord.set('start', start);
			return true;
		}

		// If the block starts later then the sumblock,
		// we need to expand the sumblock the end later.
		if (sumEnd < end) {
			sumRecord.set('end', end);
			return true;
		}

		// We checked earlier if the record is within sumRecord,
		// outside sumRecord or partially overlaps. Why did we
		// arrive here then? Perhaps the record exists in a
		// parallel universe?
		return false;
	},

	/**
	 * Merge the given {@link Ext.data.Record records} with normal FB block information
	 * into the {@link #sumBlockStore}. Each {@link Ext.data.Record record}
	 * is compared to the existing {@link Ext.data.Record records} from the {@link #sumBlockStore}.
	 * If needed the existing {@link Ext.data.Record record} is expanded to include the updated time.
	 * If the original {@link Ext.data.Record record} does not match any sumBlock then a new
	 * sumBlock will be created.
	 * @param {Ext.data.Record[]} records The records which must be merged into the sum block store
	 * @param {Zarafa.common.freebusy.data.FreebusyBlockStore} sumBlockStore The store containing the sum blocks
	 * @param {Boolean} splitBusyStatus (optional) If true then sumBlocks will be generated per
	 * BusyStatus group, otherwise a single sumBlock will be generated.
	 */
	mergeBlocksToSumBlockStore : function(records, sumBlockStore, splitBusyStatus)
	{
		var lastBlock = {};

		// Sort all records by start date, this ensures that we can apply the merging safely.
		records.sort(function(a, b) {
			return a.get('start') - b.get('start');
		});

		Ext.each(records, function(record) {
			//  We are not generating sumBlocks for the free status.
			var busyStatus = record.get('status');
			if (busyStatus === Zarafa.core.mapi.BusyStatus.UNKNOWN || busyStatus === Zarafa.core.mapi.BusyStatus.FREE) {
				return true;
			}

			if (splitBusyStatus === false) {
				busyStatus = Zarafa.core.mapi.BusyStatus.BUSY;
			}

			// The first item can be inserted directly
			if (sumBlockStore.getCount() === 0) {
				sumBlockStore.add(this.createSumBlock(record));
				return true;
			}

			var sumRecord = lastBlock[busyStatus];

			// Check if the new record can be merged into the sumRecord,
			// if this is not the case, we should add it as a new block.
			if (!Ext.isDefined(sumRecord) || this.mergeRecordIntoSumBlock(record, sumRecord) !== true) {
				var newSumBlock = this.createSumBlock(record);
				sumBlockStore.add(newSumBlock);
				lastBlock[busyStatus] = newSumBlock;
			}
		}, this);
	},

	/**
	 * Walk through the {@link Ext.data.Record records} from the {@link #sumBlockStore}
	 * to determine if any {@link Ext.data.Record record} overlapse. If this is the
	 * case the two {@link Ext.data.Record records} will be merged into a single block.
	 * @param {Zarafa.common.freebusy.data.FreebusyBlockStore} sumBlockStore The store containing the sumblocks
	 */
	mergeSumBlocks : function(sumBlockStore)
	{
		var lastBlock = {};

		// Start sorting the sumblocks based on their start date, they
		// could not have been added sorted into the store, since we
		// have been modifying the start/end values of the blocks after adding.
		sumBlockStore.sort('start', 'ASC');

		sumBlockStore.each(function(record) {
			var prevRecord = lastBlock[record.get('status')];

			// Check if the previous record and the current record overlap,
			// if this is the case the records can be merged. The comparison
			// is very simple since we only need to check if the end-time for
			// the previous item is after the start-time of the current item.
			if (Ext.isDefined(prevRecord)) {
				if (prevRecord.get('end') >= record.get('start')) {
					// Update the previous block
					prevRecord.set('end', record.get('end'));
					// And we don't need the new block anymore
					sumBlockStore.remove(record);
					return true;
				}
			}

			lastBlock[record.get('status')] = record;
		}, this);
	},

	/**
	 * Fires after a new set of Records has been loaded.
	 * @param {Zarafa.common.freebusy.data.FreebusyBlockStore} store
	 * @param {Ext.data.Record[]} records The Records that were loaded
	 * @param {Object} options The loading options that were specified (see {@link #load} for details)
	 * @private
	 */
	onBlockLoad : function(store, records, options)
	{
		// Always start with a clean store
		this.sumBlockStore.removeAll();

		if (this.getUserStore().getCount() == 1) {
			// One user, still easy, sumBlockStore is the same as blockStore
			this.blockStore.each(function(record) {
				//  We are not generating sumBlocks for the free status.
				var busyStatus = record.get('status');
				if (busyStatus !== Zarafa.core.mapi.BusyStatus.UNKNOWN && busyStatus !== Zarafa.core.mapi.BusyStatus.FREE) {
					this.sumBlockStore.add(this.createSumBlock(record));
				}
			}, this);
		} else {
			// Multiple users, tricky, merge the blockstore into the sumBlockStore
			this.mergeBlocksToSumBlockStore(this.blockStore.getRange(), this.sumBlockStore);
		}

		// Sort all sumblocks based on the status. This will force the
		// TENTATIVE records to be rendered before the BUSY which in turn is before
		// the OUTOFOFFICE. This in turn forces the browser to position the OUTOFOFFICE
		// divs on top of the BUSY blocks (which in turn are on top of TENATIVE) when
		// the blocks overlap.
		this.sumBlockStore.sort('status', 'ASC');

		this.sumBlockStore.fireEvent('load', this.sumBlockStore, this.sumBlockStore.getRange(), {});
	},

	/**
	 * Fires when a Record has been removed from the Store
	 *
	 * NOTE: Because of buffering which is applied for the invocation event handler,
	 * this handler will only be called for the first remove event
	 * in a batch. This implies that we cannot rely on the record and
	 * index arguments.
	 *
	 * @param {Zarafa.common.freebusy.data.FreebusyBlockStore} store
	 * @param {Ext.data.Record} record The Record that was removed
	 * @param {Number} index The index at which the record was removed
	 * @private
	 */
	onBlockRemove : function(store, record, index)
	{
		this.onBlockLoad(store, store.getRange(), {});
	},

	/**
	 * Fires after a new set of Records has been loaded.
	 * @param {Zarafa.common.freebusy.data.FreebusyBlockStore} store
	 * @param {Ext.data.Record[]} records The Records that were loaded
	 * @param {Object} options The loading options that were specified (see {@link #load} for details)
	 * @private
	 */
	onSumBlockLoad : function(store, records, options)
	{
		// Always start with a clean store
		this.freeBlockStore.removeAll();

		if (store.getCount() > 0) {
			// FIXME: We should actually build this store while building
			// the busy blocks. This is most likely faster then doing it
			// separately.
			this.mergeBlocksToSumBlockStore(records, this.freeBlockStore, false);
		}

		this.loadSuggestionBlocks();
	},

	/**
	 * Fires when the {@link #nonWorkingHoursHidden} has been updated.
	 * This will updated the {@link #suggestionRange} accordingly.
	 * @param {Boolean} hideNonWorkingHours True to only show working hours.
	 * @private
	 */
	onShowWorkingHoursChange : function(hideNonWorkingHours)
	{
		this.setSuggestionDate(this.suggestionRange.getStartDate());
	},

	/**
	 * Fires when the {@link #userStore} has been updated. This will force
	 * a load on the store to automatically update the {@link #blockStore}.
	 * @param {Zarafa.core.data.IPMRecipientStore} newStore
	 * @param {Zarafa.core.data.IPMRecipientStore} oldStore
	 * @private
	 */
	onUserStoreChange : function(newStore, oldStore)
	{
		newStore.fireEvent('load', newStore, newStore.getRange());
	},

	/**
	 * Fires when the {@link #selectorRange} has been updated. This
	 * will update the {@link #suggestionRange} accordingly.
	 * @param {Zarafa.core.DateRange} newRange The new selected range.
	 * @param {Zarafa.core.DateRange} oldRange The old selected range.
	 * @private
	 */
	onSelectorRangeUpdate : function(newRange, oldRange)
	{
		// Check to see if the update was triggered by outside sources
		if(!this.updatingSelectorRangeExternally){
			this.fireEvent('selectorrangeupdate', newRange, oldRange);
		}
		// Reset the state
		this.updatingSelectorRangeExternally = false;
		this.setSuggestionDate(newRange.getStartDate(), newRange.getDuration());
	},

	/**
	 * Fires when the {@link #suggestionRange} has been updated. This
	 * will recalculate the {@link #suggestionBlockStore} accordingly.
	 * @param {Zarafa.core.DateRange} newRange The new selected range.
	 * @param {Zarafa.core.DateRange} oldRange The old selected range.
	 * @private
	 */
	onSuggestionRangeUpdate : function(newRange, oldRange)
	{
		this.loadSuggestionBlocks();
	},

	/**
	 * This will recalculate the suggestion blocks in the {@link #suggestionBlockStore}
	 * based on the information from the {@link #freeBlockStore} and the {@link #selectorRange}.
	 * @private
	 */
	loadSuggestionBlocks : function()
	{
		// Always start with a clean store
		this.suggestionBlockStore.removeAll();

		if (this.freeBlockStore.getCount() > 0) {
			var start = this.suggestionRange.getStartTime() / 1000;
			var end = this.suggestionRange.getDueTime() / 1000;
			var duration = this.selectorRange.getDuration(Date.SECOND);
			var interval = Ext.min([duration, 30 * 60]); // FIXME: make configurable

			// But what if the appointment takes 0 minutes..
			// That would be dumb, but we won't be fooled!
			if (interval <= 0) {
				interval = 30 * 60;
			}

			this.freeBlockStore.each(function(sumBlock) {
				var sumStart = sumBlock.get('start');
				var sumEnd = sumBlock.get('end');

				if (sumEnd < start || sumStart > end) {
					// The entire block falls before the requested range,
					// just keep looping until we find the desired range.
					return;
				} else if (sumStart > end) {
					// The entire block falls after the requested range
					// we don't need to do anything anymore.
					return false;
				} else if (sumStart <= start) {
					// The block overlap our range, our new start
					// time is the end time of this block.
					start = sumEnd;
				} else {
					// The entire block falls after our start range,
					// simply add a suggestionblock from start to the sumBlock start.
					this.suggestionBlockStore.add(this.createSuggestionBlocks(start, Ext.min([sumStart, end]), duration, interval));
					start = sumEnd;
				}
			}, this);

			// Check if we still have a leftover...
			if (start < end) {
				this.suggestionBlockStore.add(this.createSuggestionBlocks(start, end, duration, interval));
			}
		}

		this.suggestionBlockStore.fireEvent('load', this.suggestionBlockStore, this.suggestionBlockStore.getRange(), {});
	},

	/**
	 * Function can be used to check if all added attendees are free on particular timeslot.
	 * @param {Date} periodStartTime object of start time
	 * @param {Date} periodEndTime object of end time
	 * @param {Boolean} modifiedOnly True if only the modified attendees should be checked
	 * @return {Boolean} return true if all attendees are free else false.
	 */
	checkAttendeesBusyStatus : function(periodStartTime, periodEndTime, modifiedOnly)
	{
		var userStore = this.getUserStore();
		if (!userStore) {
			return false;
		}

		var modified = userStore.getRange();
		for (var i = 0, len = modified.length; i < len; i++) {
			var user = modified[i];
			if ((modifiedOnly !== true || user.phantom === true) && !user.isMeetingOrganizer()) {
				if (this.checkAttendeeBusyStatus(user.id, periodStartTime, periodEndTime)) {
					return true;
				}
			}
		}

		return false;
	},

	/**
	 * Check if the given user is free on the given timeslot
	 * @param {String} userid The userid of the user to check for availability
	 * @param {Date} periodStartTime object of start time
	 * @param {Date} periodEndTime object of end time
	 * @return {Boolean} return true if the attendee is free.
	 */
	checkAttendeeBusyStatus : function(userid, periodStartTime, periodEndTime)
	{
		var blockStore = this.getBlockStore();
		if(!blockStore) {
			return false;
		}

		// Ensure that only the current userid is shown
		blockStore.filter('userid', userid, false, true, true);

		// Sort on start date
		blockStore.sort('start', 'ASC');

		// We need timestamps rather then Date objects
		periodStartTime = periodStartTime.getTime() / 1000;
		periodEndTime = periodEndTime.getTime() / 1000;

		// Lets search the block for Blocks that overlap with the requested period.
		var busy = false;

		for (var index = 0, len = blockStore.getCount(); index < len; index++) {
			var record = blockStore.getAt(index);

			/*
			 * First we need to remove appointments which are occuring extermely before/after our
			 * selected time, because then we have only set of appointments which are overlapping/inside
			 * our time slot.
			 * For that to achieve we first need sort the records based on start time and then to find
			 * out sum block record whose end time is greater then our selected start time
			 * and start time is less then our selected end time then we can say that
			 * the selected time is not proper for all the attendees.
			 */
			// remove appointments occuring extremely before our selected time
			if (record.get('end') > periodStartTime) {
				// check if we are really interested in this block
				if (record.get('status') === Zarafa.core.mapi.BusyStatus.FREE || record.get('status') === Zarafa.core.mapi.BusyStatus.UNKNOWN) {
					continue;
				}

				// before we have sorted the records based on start time so we will be having a record which either
				// overlaps current selected time or doesn't overlap it
				// below condition will check if the record overlaps current selected time
				if (record.get('start') < periodEndTime) {
					busy = true;
				}

				// if the above condition is not satisfied then we can say that
				// record is not overlapping current selected time and therefore
				// break the loop
				break;
			}
		}

		blockStore.clearFilter();

		return busy;
	}
});
