/*
 * #dependsFile client/zarafa/hierarchy/data/HierarchyProxy.js
 */
Ext.namespace('Zarafa.hierarchy.data');

/**
 * @class Zarafa.hierarchy.data.HierarchyStore
 * @extends Zarafa.core.data.IPFStore
 * @xtype zarafa.hierarchystore
 *
 * {@link Zarafa.hierarchy.data.HierarchyStore HierarchyStore} holds
 * {@link Zarafa.hierarchy.data.MAPIStoreRecord MAPIStoreRecord} as records, which defines store information
 * of all opened stores(own, shared, public).
 */
Zarafa.hierarchy.data.HierarchyStore = Ext.extend(Zarafa.core.data.IPFStore, {
	/**
	 * @cfg {Boolean} stateful A flag which causes the store to create a {@link #state} object in which
	 * the state for the various {@link Zarafa.hierarchy.data.MAPIFolderRecord folders} can be saved.
	 * This class will not automatically get/set the state, but assumes that other {@link Ext.Component components}
	 * or {@link Zarafa.core.data.StatefulObservable stateful objects} will {@link #getState get} or
	 * {@link #applyState apply} the state.
	 */
	stateful : true,

	/**
	 * When {@link #stateful} the interaction object in which the state can be saved or obtained from.
	 * Typically access to this object can be done through {@link #getState} and {@link #applyState}.
	 * @property
	 * @type Zarafa.hierarchy.data.HierarchyState
	 */
	state : undefined,

	/**
	 * @constructor
	 * @param {Object} config Configuration object
	 */
	constructor : function (config)
	{
		config = config || {};

		Ext.applyIf(config, {
			// The HierarchyStore is special and doesn't need
			// to register itself to the IPFStoreMgr, this because
			// the contents of the hierarchyStore is a list
			// of more IPFStores which will register themselves.
			standalone : true,
			proxy: new Zarafa.hierarchy.data.HierarchyProxy(),
			writer : new Zarafa.core.data.JsonWriter(),
			reader: new Zarafa.core.data.JsonReader({
				id : 'store_entryid',
				idProperty : 'store_entryid'
			})
		});

		this.addEvents(
			/**
			 * @event addFolder
			 * Fires when a folder has been created.
			 * @param {Zarafa.hierarchy.data.HierarchyStore} store The hierarchy store that fired the event
			 * @param {Zarafa.hierarchy.data.MAPIStoreRecord} storeRecord The MAPI store that contains the folder that was added
			 * @param {Zarafa.hierarchy.data.IPFRecord/Zarafa.hierarchy.data.IPFRecord[]} folder folder record that is added to the hierachy store.
			 */
			'addFolder',
			/**
			 * @event removeFolder
			 * Fires when a folder has been removed.
			 * @param {Zarafa.hierarchy.data.HierarchyStore} store This hierarchy store that fired the event
			 * @param {Zarafa.hierarchy.data.MAPIStoreRecord} storeRecord The MAPI store that contained the folder that was removed
			 * @param {Zarafa.hierarchy.data.IPFRecord} folder folder which is removed from the hierarchy store.
			 */
			'removeFolder',
			/**
			 * @event updateFolder
			 * Fires when a folder has been updated
			 * @param {Zarafa.hierarchy.data.HierarchyStore} store
			 * @param {Zarafa.hierarchy.data.MAPIStoreRecord} storeRecord
			 * @param {Zarafa.hierarchy.data.IPFRecord} record folder which is updated in the hierarchy store.
			 * @param {String} operation The update operation being performed. Value may be one of
			 * {@link Ext.data.Record#EDIT}, {@link Ext.data.Record#REJECT}, {@link Ext.data.Record#COMMIT}.
			 */
			'updateFolder'
		);

		Zarafa.hierarchy.data.HierarchyStore.superclass.constructor.call(this, config);

		if (this.stateful !== false) {
			this.initState();
		}

		this.on({
			'beforesave' : this.onBeforeSave,
			'load' : this.onAfterLoad,
			'add' : this.onAdd,
			'remove' : this.onRemove,
			scope : this
		});
	},

	/**
	 * Compare a {@link Ext.data.Record#id ids} to determine if they are equal.
	 * This will apply the {@link Zarafa.core.EntryId#compareStoreEntryIds compareStoreEntryIds} function
	 * on both ids, as all records in this store will have a StoreEntryId as unique key.
	 * @param {String} a The first id to compare
	 * @param {String} b The second id to compare
	 * @protected
	 */
	idComparison : function(a, b)
	{
		return Zarafa.core.EntryId.compareStoreEntryIds(a, b);
	},

	/**
	 * <p>Loads the Record cache from the configured <tt>{@link #proxy}</tt> using the configured <tt>{@link #reader}</tt>.</p>
	 * <br> Function just adds 'list' as actionType in options and calls parent {@link Zarafa.core.data.IPFStore#load} method.
	 * <br> Check documentation of {@link Ext.data.Store#load} for more information.
	 */
	load : function(options)
	{
		if (!Ext.isObject(options)) {
			options = {};
		}

		if (!Ext.isObject(options.params)) {
			options.params = {};
		}

		// Load should always cancel the previous actions.
		if (!Ext.isDefined(options.cancelPreviousRequest)) {
			options.cancelPreviousRequest = true;
		}

		/*
		 * This store will only be used to get whole hierarchy data, so it will have
		 * actionType as 'list' always.
		 */
		Ext.applyIf(options, {
			actionType : Zarafa.core.Actions['list']
		});

		return Zarafa.hierarchy.data.HierarchyStore.superclass.load.call(this, options);
	},

	/**
	 * Remove records from the Store and fire the {@link #remove} event.
	 * On a HierarchyStore it is not possible to remove the Private or Public Store,
	 * a special event will be send to the server when a Shared Store is removed
	 * from the Hierarchy.
	 * @param {Zarafa.hierarchy.data.MAPIStore} record The MAPIStore which must be
	 * removed from the HierarchyStore.
	 */
	remove : function(record)
	{
		if (Array.isArray(record)) {
			Ext.each(record, this.remove, this);
			return;
		}

		// Only allow Shared Stores to be removed
		if (record.isSharedStore()) {
			// Remove all favorites marked folders which are
			// belongs to shared user because we are going to
			// close the shared user hierarchy store.
			var favoritesStore = record.getFavoritesStore();
			var records = favoritesStore.query('store_entryid',record.get('store_entryid'));
			favoritesStore.remove(records.getRange());

			Zarafa.hierarchy.data.HierarchyStore.superclass.remove.call(this, record);
		}
	},

	/**
	 * Open a Store in the Hierarchy. This will {@link #load} the hierarchy
	 * requesting the data for the given user store/folder. The returned data
	 * will be appended to the already loaded data.
	 * @param {String} username The name of the user for which the store will be opened
	 * @param {Zarafa.hierarchy.data.SharedFolderTypes} foldertype The foldertype which will be opened
	 * @param {Boolean} subfolders True if subfolders must be loaded (when foldertype is not 'all').
	 * @return {Boolean} False if the substore could not be opened
	 */
	open : function(username, foldertype, subfolders)
	{
		// The username must always be lowercase
		username = username.toLowerCase();

		// Check if the user is already present in the settings,
		// if that is the case we have to check if we can actually open
		// this new folder or not.
		var settings = container.getSettingsModel().get('zarafa/v1/contexts/hierarchy/shared_stores/' + username, true);
		if (settings && ((settings[foldertype] || settings['all']))) {
			return false;
		}

		var options = Ext.applyIf({
			params : {
				user_name : username,
				folder_type : foldertype,
				show_subfolders : subfolders
			},
			add : true,
			actionType : 'opensharedfolder',
			cancelPreviousRequest : false
		}, this.lastOptions);

		try {
			return this.execute('read', null, options); // <-- null represents rs. No rs for load actions.
		} catch (e) {
			this.handleException(e);
			return false;
		}
	},

	/**
	 * Called when the response from the server has been returned for the {@link #load} call.
	 * If the actionType indicates that a shared folder was opened for this call, this will check
	 * if the returned Store object is already present inside the store (this could happen when
	 * we already shared the 'Calendar' and are now adding the 'Contacts'). If that is the case
	 * the received {@link Zarafa.hierarchy.data.MAPIStoreRecord store} is
	 * {@link Zarafa.core.data.MAPIRecord#applyData merged} into the already existing
	 * {@link Zarafa.hierarchy.data.MAPIStoreRecord store}.
	 * @param {Object} o The cache of {@link Ext.data.Record records} as received by the server
	 * @param {Object} options The options used for loading the store
	 * @param {Boolean} success True if the records were successfully loaded from the store
	 * @private
	 */
	loadRecords : function(o, options, success)
	{
		if (this.isDestroyed) {
			return;
		}
		if (o && success === true && options && options.actionType === 'opensharedfolder') {
			var records = o.records;

			for (var i = 0, len = records.length; i < len; i++) {
				var record = records[i];
				var existingStore = this.getById(record.get('store_entryid'));

				// The store already existed previously, the server will have given
				// us the new folders only. So we obtain the previously loaded folders
				// and inject them into the response.
				if (existingStore) {
					// If we are opening full store of the user then close all the shared folder first.
					if(options.params.folder_type === 'all') {
						this.remove(existingStore);
						this.save(existingStore);
					} else {
						record.createSubStores();

						var existingFolderStore = existingStore.getSubStore('folders');
						var newFolderStore = record.getSubStore('folders');

						// Go over all existing folders, if it doesn't exist yet in the
						// store which we received from the server, copy it into there.
						existingFolderStore.each(function(folder) {
							var newFolder = newFolderStore.getById(newFolderStore.data.getKey(folder));
							if (!newFolder) {
								newFolderStore.add(folder.copy());
							}
						}, this);
					}
				}
			}
		}

		Zarafa.hierarchy.data.HierarchyStore.superclass.loadRecords.call(this, o, options, success);
	},

	/**
	 * This will hook the event handlers {@link #onFolderAdd}, {@link #onFolderRemove} and {@link #onFolderUpdate},
	 * to the {@link Ext.data.Store#add}, {@link Ext.data.Store#remove} and {@link Ext.data.Store#update} events
	 * for the 'folders' substore for each store which is inside the hierarchy. This allows us to fire
	 * the {@link #addFolder}, {@link #removeFolder}, {@link #updateFolder} events.
	 * @param {Zarafa.hierarchy.data.MAPIStoreRecord[]} records The records which are hooked
	 * @private
	 */
	hookStoreRecord : function(records)
	{
		for (var i = 0, len = records.length; i < len; i++) {
			var folders = records[i].getSubStore('folders');
			folders.on({
				'add' : this.onFolderAdd,
				'remove' : this.onFolderRemove,
				'update' : this.onFolderUpdate,
				scope : this
			});

			var favorites = records[i].getSubStore('favorites');
			if(Ext.isDefined(favorites)) {
				favorites.on({
					'add' : this.onFolderAdd,
					'remove' : this.onFolderRemove,
					'update' : this.onFolderUpdate,
					scope : this
				});
			}
		}
	},

	/**
	 * This will unhook the events from {@link #hookStoreRecord}.
	 * @param {Zarafa.hierarchy.data.MAPIStoreRecord} record The record to unhook
	 * @private
	 */
	unhookStoreRecord : function(record)
	{
		var folders = record.getSubStore('folders');
		folders.un('add', this.onFolderAdd, this);
		folders.un('remove', this.onFolderRemove, this);
		folders.un('update', this.onFolderUpdate, this);

		var favorites = record.getSubStore('favorites');
		if(Ext.isDefined(favorites)) {
			favorites.un('add', this.onFolderAdd, this);
			favorites.un('remove', this.onFolderRemove, this);
			favorites.un('update', this.onFolderUpdate, this);
		}
	},

	/**
	 * Event handler which is fired when the Hierarchy has loaded,
	 * this will check if this was called to open a Shared Store or not.
	 *
	 * If this has been called because a shared store has been opened,
	 * this will update the settings so the folder will be automatically
	 * loaded next time the user logs in.
	 *
	 * In all cases, all stores which are loaded will be hooked using
	 * {@link #hookStoreRecord}.
	 *
	 * @param {Ext.data.Store} store The store which fired the event
	 * @param {Ext.data.Record[]} records The records which were loaded
	 * @param {Object} options The options which were send with the load request
	 * @private
	 */
	onAfterLoad : function(store, records, options)
	{
		if (options.actionType === 'opensharedfolder') {
			var settings = container.getSettingsModel();
			var user_name = options['params']['user_name'].toLowerCase();
			var folder_type = options['params']['folder_type'] || 'all';
			var subfolders = options['params']['show_subfolders'] || false;

			var settingsBase = 'zarafa/v1/contexts/hierarchy/shared_stores/' + user_name;

			var shared = settings.get(settingsBase, true);
			if (shared && (shared[folder_type] || shared['all'])) {
				return;
			}

			settings.set(settingsBase + '/' + folder_type, {
				folder_type : folder_type,
				show_subfolders : subfolders
			});

			// when user tries to open shared folder, that particular folder will be opened immediately and context will be
			// switched automatically. if context not having folder than by default shared 'inbox' will be opened.
			// if user tries to open 'entire inbox' than folder is opened as per current context, if current context not having
			// folder than by default shared 'inbox' will be opened, and context will switched automatically.
			if(folder_type === 'all') {
				folder_type = container.getCurrentContext().getName();
				switch(folder_type){
					case 'calendar':
					case 'contacts':
					case 'tasks':
					case 'notes':
						break;
					default :
						folder_type = 'inbox';
						break;
				}
			}
			var folder = this.getFolder((records[0].get('default_folder_'+folder_type)));
			if (Ext.isDefined(folder) && folder_type!=='calendar') {
				Zarafa.hierarchy.Actions.openFolder(folder);
			}

		}

		// Add event handlers which are registered so we can listen for
		// the add, remove and update events for the folders.
		this.hookStoreRecord(records);
	},

	/**
	 * Saves all pending changes to the store.  If the commensurate Ext.data.Api.actions action is not configured, then
	 * the configured <code>{@link #url}</code> will be used.
	 * <pre>
	 * change            url
	 * ---------------   --------------------
	 * removed records   Ext.data.Api.actions.destroy
	 * phantom records   Ext.data.Api.actions.create
	 * {@link #getModifiedRecords modified records}  Ext.data.Api.actions.update
	 * </pre>
	 * @TODO:  Create extensions of Error class and send associated Record with thrown exceptions.
	 * e.g.:  Ext.data.DataReader.Error or Ext.data.Error or Ext.data.DataProxy.Error, etc.
	 * @return {Number} batch Returns a number to uniquely identify the "batch" of saves occurring. -1 will be returned
	 * if there are no items to save or the save was cancelled.
	 */
	save : function()
	{
		// If stores have been removed we have to provide a special
		// treatment, as stores can't be deleted, but only 'closed'.
		if (this.removed.length) {
			var stores = this.removed;

			var data = {
				'close' : stores
			};

			if (this.fireEvent('beforesave', this, data) !== false) {
				try {
					var batch = ++this.batchCounter;
					this.execute('destroy', data['close'], { actionType : 'closesharedfolder' }, batch);
				} catch (e) {
					this.handleException(e);
				}

				// Reset the removed list and
				// continue as planned.
				this.removed = [];
			}
		}

		Zarafa.hierarchy.data.HierarchyStore.superclass.save.apply(this, arguments);
	},

	/**
	 * Event handler which is fired when Hierarchy changes are about to be saved.
	 * This will check if a record is being destroyed, if that is a shared store,
	 * then the settings will be updated to save the state.
	 * @param {Ext.data.Store} store The store which fired the event.
	 * @param {Object} data The data object containing the various changes
	 * which will be send to the server
	 * @private
	 */
	onBeforeSave : function(store, data)
	{
		// The 'close' key is only used for shared stores/folders
		if (!Ext.isEmpty(data['close'])) {
			var settings = container.getSettingsModel();
			var stores = data['close'];

			settings.beginEdit();

			for (var i = 0, len = stores.length; i < len; i++) {
				var sharedstore = stores[i];
				var username = sharedstore.get('user_name').toString().toLowerCase();

				// The entire store is being closed, this can be used
				// when we were sharing the entire store, or we were
				// sharing one or more different default folders.
				// Both cases should be correctly removed using this
				// function, so we just remove all shared_stores settings
				// for the user rather then only the 'all' folder type.
				settings.remove('zarafa/v1/contexts/hierarchy/shared_stores/' + username);

				sharedstore.addIdProp('user_name');
			}

			settings.endEdit();
		}
	},

	/**
	 * Event handler fired when a {@link Zarafa.hierarchy.data.MAPIStoreRecord store}
	 * has been added to the hierarchy. This will call {@link #hookStoreRecord}.
	 *
	 * @param {Zarafa.hierarchy.data.HierarchyStore} store This store
	 * @param {Zarafa.hierarchy.data.MAPIStoreRecord[]} records The record which is added
	 * @param {Number} index The index from where the record was added
	 * @private
	 */
	onAdd : function(store, records, index)
	{
		this.hookStoreRecord(records);
	},

	/**
	 * Event handler fired when a {@link Zarafa.hierarchy.data.MAPIStoreRecord store}
	 * has been added to the hierarchy. This will call {@link #unhookStoreRecord}.
	 *
	 * @param {Zarafa.hierarchy.data.HierarchyStore} store This store
	 * @param {Zarafa.hierarchy.data.MAPIStoreRecord} record The record which is removed
	 * @param {Number} index The index from where the record was removed
	 * @private
	 */
	onRemove : function(store, record, index)
	{
		this.unhookStoreRecord(record);
	},

	/**
	 * Event handler fired when a folder in one of the {@link Zarafa.hierarchy.data.MAPIStoreRecord stores}
	 * in this store has been {@link Ext.data.Store#add added}. This will fire the
	 * {@link #addFolder} event.
	 *
	 * @param {Zarafa.hierarchy.data.IPFSubStore} substore
	 * @param {Zarafa.hierarchy.data.IPFRecord} folder which is added to the hierarchy
	 * @param {Number} index The index in the substore from where the folder was added
	 * @private
	 */
	onFolderAdd : function(store, records, index)
	{
		this.fireEvent('addFolder', this, store.getParentRecord(), records, index);
	},

	/**
	 * Event handler fired when a folder in one of the {@link Zarafa.hierarchy.data.MAPIStoreRecord stores}
	 * in this store has been {@link Ext.data.Store#remove removed}. This will fire the
	 * {@link #removeFolder} event.
	 *
	 * @param {Zarafa.hierarchy.data.IPFSubStore} substore
	 * @param {Zarafa.hierarchy.data.IPFRecord} folder which is removed from the hierarchy
	 * @param {Number} index The index in the substore from where the folder was removed
	 * @private
	 */
	onFolderRemove : function(store, record, index)
	{
		// If the removed folder is a shared folder and not favorites marked then we have to check
		// if this is the last shared folder for the store. Because
		// that means we have to remove the entire store.
		if (record.isSharedFolder() && !record.isFavoritesFolder()) {
			var isEmpty = true;

			// Check if there are any shared folders left in the hierarchy...
			store.each(function(r) {
				if (r.isSharedFolder()) {
					isEmpty = false;
					return false;
				}
			}, this);

			// No shared folders remain, time to cleanup the entire store
			if (isEmpty) {
				// Reject all changes made to the IPFSubStore,
				// we are deleting the entire parent record, and thus
				// any changes made inside the SubStore are irrelevant.
				store.rejectChanges();

				// Remove the store from the Hierarchy
				var root = store.getParentRecord();
				this.remove(root);
				this.save(root);
				return;
			}
		}

		this.fireEvent('removeFolder', this, store.getParentRecord(), record, index);
	},

	/**
	 * Event handler fired when a folder in one of the {@link Zarafa.hierarchy.data.MAPIStoreRecord stores}
	 * in this store has been {@link Ext.data.Store#update updated}. This will fire the
	 * {@link #updateFolder} event.
	 *
	 * @param {Zarafa.hierarchy.data.IPFSubStore} store
	 * @param {Zarafa.hierarchy.data.IPFRecord} record which is updated in the hierarchy.
	 * @param {String} operation The update operation being performed. Value may be one of
	 * {@link Ext.data.Record#EDIT}, {@link Ext.data.Record#REJECT}, {@link Ext.data.Record#COMMIT}.
	 * @private
	 */
	onFolderUpdate : function(store, record, operation)
	{
		this.fireEvent('updateFolder', this, store.getParentRecord(), record, operation);
	},

	/**
	 * @return {Array} list of all {@link Zarafa.hierarchy.data.MAPIStoreRecord Store}
	 */
	getStores : function()
	{
		return this.getRange();
	},

	/**
	 * @return {Zarafa.hierarchy.data.MAPIStoreRecord} default store
	 */
	getDefaultStore : function()
	{
		var index = this.findExact('mdb_provider', Zarafa.core.mapi.MDBProvider.ZARAFA_SERVICE_GUID);

		if (index !== -1) {
			return this.getAt(index);
		}
	},

	/**
	 * @return {Zarafa.hierarchy.data.MAPIStoreRecord} public store
	 */
	getPublicStore : function()
	{
		var index = this.findExact('mdb_provider', Zarafa.core.mapi.MDBProvider.ZARAFA_STORE_PUBLIC_GUID);

		if (index !== -1) {
			return this.getAt(index);
		}
	},

	/**
	 * Function is used get default {@link Zarafa.hierarchy.data.MAPIFolderRecord MAPIFolderRecord}
	 * from {@link Zarafa.hierarchy.data.MAPIStoreRecord MAPIStoreRecord},
	 * if {@link Zarafa.hierarchy.data.MAPIStoreRecord MAPIStoreRecord} is not passed then
	 * it will by default take default store.
	 *
	 * @param {String} name name of the default folder (i.e. 'inbox' or 'contacts')
	 * @param {Zarafa.hierarchy.data.MAPIStoreRecord} mapiStoreRecord (optional) {@link Zarafa.hierarchy.data.MAPIStoreRecord MAPIStoreRecord}
	 * whose default folder we should return, if not passed then it will use default store.
	 * @return {Zarafa.hierarchy.data.MAPIFolderRecord} a folder if a default folder with the given name was found, or undefined otherwise.
	 */
	getDefaultFolder : function(name, mapiStoreRecord)
	{
		if(!mapiStoreRecord) {
			mapiStoreRecord = this.getDefaultStore();

			if(!mapiStoreRecord) {
				// hierarchy store is empty
				return;
			}
		}

		return mapiStoreRecord.getDefaultFolder(name);
	},

	/**
	 * Retrieves a {@link Zarafa.hierarchy.data.MAPIStoreRecord MAPIStoreRecord} by owner entryid.
	 * @param {String} ownerEntryId entry id of the owner of the store.
	 * @return {Zarafa.hierarchy.data.MAPIStoreRecord} store owned by user.
	 */
	getStoreByOwnerEntryId : function(ownerEntryId)
	{
		for(var i = 0; i < this.getCount(); i++) {
			var folderStore = this.getAt(i);

			if(Zarafa.core.EntryId.compareABEntryIds(folderStore.get('mailbox_owner_entryid'), ownerEntryId)) {
				return folderStore;
			}
		}
	},

	/**
	 * Retrieves a folder by MAPI ID from list of stores
	 * @param {String} id MAPI ID of the folder to search for.
	 * @return {Zarafa.hierarchy.data.MAPIFolderRecord} a folder matching the given ID, or undefined if not found.
	 */
	getFolder : function(id)
	{
		for(var i = 0, len = this.getCount(); i < len; i++) {
			var folderStore = this.getAt(i).getFolderStore();

			if(folderStore) {
				var folder = folderStore.getById(id);
				if(folder) {
					return folder;
				}
			}
		}
	},

	/**
	 * Function will return default folder from the hierarchy
	 * for the supplied message_class to the function.
	 *
	 * @param {String} messageClass The message_class of the mapi record.
	 * @return {Zarafa.hierarchy.data.MAPIFolderRecord} a folder
	 * default folder for the supplied message_class
	 */
	getDefaultFolderFromMessageClass : function(messageClass)
	{
		var folderType = Zarafa.core.MessageClass.getDefaultFolderTypeFromMessageClass(messageClass);
		if (!Ext.isEmpty(folderType)) {
			return this.getDefaultStore().getDefaultFolder(folderType);
		}
	},

	/**
	 * Function will return default folder from the hierarchy
	 * for the supplied container_class to the function.
	 *
	 * @param {String} containerClass The container_class of the mapi record.
	 * @return {Zarafa.hierarchy.data.MAPIFolderRecord} a folder
	 * default folder for the supplied message_class
	 */
	getDefaultFolderFromContainerClass : function(containerClass)
	{
		var folderType = Zarafa.core.ContainerClass.getDefaultFolderTypeFromContainerClass(containerClass);
		if (!Ext.isEmpty(folderType)) {
			return this.getDefaultStore().getDefaultFolder(folderType);
		}
	},

	/**
	 * Helper method for getSortedFolders.
	 * @private
	 */
	collectFolders : function(arr, match, scope, folder)
	{
		if (!Ext.isFunction(match) || match.call(scope || this, folder)) {
			arr.push(folder);
		}

		Ext.each(folder.getChildren(), function(childFolder) {
			this.collectFolders(arr, match, scope, childFolder);
		}, this);
	},

	/**
	 * Retrieves a sorted list of folders. The folders will appear in the same order as they do in the hierarchy tree on screen.
	 * This order is partially dictated by the order in which the folders are returned by the server, and the hierarchy of folders.
	 * @param {Function} match (optional) a function that can be used to filter out undesired folders. Will be called with a folder object
	 * as parameter and should return true if that folder should appear in the output.
	 * @return {Array} array of matched folders, in the same order as they appear on screen.
	 */
	getSortedFolders : function(match, scope)
	{
		var allFolders = [];

		var stores = this.getStores();
		for (var i = 0; i < stores.length; i++) {
			this.collectFolders(allFolders, match, scope, stores[i].getSubtreeFolder());
		}

		return allFolders;
	},

	/**
	 * Checks whether any of the stores that were included in the parameters during the last load,
	 * matches the supplied entryid argument.
	 *
	 * Although the HierarchyStore can return true for all storeentryids (it contains the entire
	 * hierarchy after all), it will return false. This ensures that all IPFSubStores which are
	 * found within the HierarchyStore can check if they want to have the notification themself.
	 *
	 * @param {String|Array} entryidList Entryid of the folder
	 * @return {Boolean} Returns true when entryid matches, false when it does not.
	 */
	containsStoreInLastLoad: function(entryidList)
	{
		return false;
	},

	/**
	 * Notification handler called by {@link #onNotify} when
	 * a {@link Zarafa.core.data.Notifications#newMail newMail}
	 * notification has been recieved.
	 *
	 * This will update all stores in which the new mail was delivered.
	 *
	 * @param {Zarafa.core.data.Notifications} action The notification action
	 * @param {Ext.data.Record/Array} records The record or records which have been affected by the notification.
	 * @param {Object} data The data which has been recieved from the PHP-side which must be applied
	 * to the given records.
	 * @param {Number} timestamp The {@link Date#getTime timestamp} on which the notification was received
	 * @param {Boolean} success The success status, True if the notification was successfully recieved.
	 * @private
	 */
	onNotifyNewmail : function(action, records, data, timestamp, success)
	{
		if (!Array.isArray(records)) {
			records = [ records ];
		}

		for (var i = 0, len = records.length; i < len; i++) {
			// If this notification came at the same time as we reloaded this folder,
			// we already obtained created item, and we don't need to reload.
			if (!timestamp || records[i].lastExecutionTime(Zarafa.core.Actions['list']) < timestamp) {
				/*
				 * If this notification come at the time while user live scorlling
				 * then we have to reload the mail store with all mails(limit should
				 * be total loaded mails in grid).
				 */
				if(records[i].lastOptions.actionType === Zarafa.core.Actions['updatelist']) {
					delete records[i].lastOptions.add;
					Ext.apply(records[i].lastOptions.params.restriction,{
						start : 0,
						limit : records[i].getCount()
					});
					records[i].reload(records[i].lastOptions);
				} else {
					records[i].reload();
				}
			}
		}

		// Exclude these folders for notifications.
		var folder_keys = ['wastebasket', 'sent', 'drafts', 'outbox', 'junk', 'journal'];

		//notify user about new mail
		if(!Ext.isEmpty(data)){
			Ext.each(data.item, function(folder){
				var folderStore = this.getFolder(folder.entryid);
				if (!folderStore) {
					return;
				}

				var folderKey = folderStore.getDefaultFolderKey();
				var newmail = folder.content_unread !== 0
					&& folder.content_unread > folderStore.get('content_unread');

				if (newmail && folder_keys.indexOf(folderKey) === -1 && folderStore.isContainerClass('IPF.Note')) {
					var notificationMessage;
					if ( folder.user_display_name ) {
						// This is in a shared store. We have to show the shared store's user's name too
						notificationMessage = String.format(
							ngettext('There is {0} unread message in the folder {1} of {2}', 'There are {0} unread messages in the folder {1} of {2}', folder.content_unread),
							folder.content_unread,
							folder.display_name,
							folder.user_display_name
						);
					} else {
						notificationMessage = String.format(
							ngettext('There is {0} unread message in the folder {1}', 'There are {0} unread messages in the folder {1}', folder.content_unread),
							folder.content_unread,
							folder.display_name
						);
					}

					// New mail notification should show only on main browser window.
					var mainBrowserWindowBody;
					if (!Zarafa.core.BrowserWindowMgr.isMainWindowActive()) {
						mainBrowserWindowBody = Zarafa.core.BrowserWindowMgr.getMainBrowserWindowBody();
					}

					container.getNotifier().notify('info.newmail', _('New Mail'), notificationMessage, {
						container: mainBrowserWindowBody
					});
				}

				folderStore.set('content_unread', folder.content_unread);
				folderStore.set('content_count', folder.content_count);
			}, this);
		}
	},

	/**
	 * Initialize the keepalive requests to the server. Listen to the aftersend event in the
	 * {@link Zarafa.core.Request Request} object to reset the counter everytime the clients sends a
	 * request to the server.
	 */
	startKeepAlive : function()
	{
		var interval = container.getSettingsModel().get('zarafa/v1/contexts/hierarchy/polling_interval');

		// add a listener event that will fire automatically after specified interval
		if (Ext.isNumber(interval) && interval > 0) {
			container.getRequest().on('aftersend', this.sendKeepAlive, this, { buffer : interval * 1000});
		}
	},

	/**
	 * Sends a request to the server with only an action type and no other parameters. It will also not
	 * wait for / handle a response.
	 * @param {Zarafa.core.Actions} action The action type that will be send to the server
	 */
	sendSimpleActionToServer : function(action)
	{
		var options = {
			params : {},
			actionType : action
		};

		// fire request
		this.proxy.request(Ext.data.Api.actions['read'], null, options.params, this.reader, Ext.emptyFn, this, options);
	},

	/**
	 * Function will be used to send <b>keepalive</b> requests to the server so we will be always logged
	 * on server and don't need to relogin after some time even if user doesn't perform any action.
	 * function is created on a fingerprint of {@link #load} method, which performs a 'read' operation
	 * for {@link Zarafa.hierarchy.data.HierarchyStore HierarchyStore}.
	 * Reason to create this function was we don't have any operation type for keepalive requests
	 * other than CRUD so what we do is call {@link Zarafa.hierarchy.data.HierarchyProxy#request}
	 * method of {@link Zarafa.hierarchy.data.HierarchyProxy HierarchyProxy} with 'read' operation
	 * and 'keepalive' as {@link #actionType} in options, we also need to pass Ext.emptyFn as callback function
	 * and this way don't interfere with the working of {@link #load} and {@link #reload} functionalities.
	 * @private
	 */
	sendKeepAlive : function()
	{
		this.sendSimpleActionToServer(Zarafa.core.Actions['keepalive']);
	},

	/**
	 * Send a <b>sharestoreupdate</b> request to make the backend check if there are unread emails
	 * in one of the opened shared stores.
	 * @private
	 */
	sendSharedStoreHierarchyUpdate : function()
	{
		this.sendSimpleActionToServer(Zarafa.core.Actions['sharedstoreupdate']);
	},

	/**
	 * Function will be used to send {Zarafa.core.Actions#destroysession destroysession} requests to the server. This will be used when
	 * CLIENT_TIMEOUT has been set in the configuration and the user has been idle for this time.
	 * Function is created on a fingerprint of {@link #load} method, which performs a 'read' operation
	 * for {@link Zarafa.hierarchy.data.HierarchyStore HierarchyStore}.
	 * Reason to create this function was we don't have any operation type for destroysession requests
	 * other than CRUD so what we do is call {@link Zarafa.hierarchy.data.HierarchyProxy#request}
	 * method of {@link Zarafa.hierarchy.data.HierarchyProxy HierarchyProxy} with 'read' operation
	 * and {Zarafa.core.Actions#destroysession destroysession} as {@link #actionType} in options,
	 * we also need to pass Ext.emptyFn as callback function and this way don't interfere with
	 * the working of {@link #load} and {@link #reload} functionalities.
	 * @private
	 */
	sendDestroySession : function()
	{
		this.sendSimpleActionToServer(Zarafa.core.Actions['destroysession']);
	},

	/**
	 * Intialialize the {@link #state state component}
	 * @private
	 */
	initState : function()
	{
		if (!this.state) {
			this.state = new Zarafa.hierarchy.data.HierarchyState();
		}
	},

	/**
	 * Obtain the path in which the {@link #getState state} must be saved.
	 * This option is only used when the {@link Zarafa.core.data.SettingsStateProvider SettingsStateProvider} is
	 * used in the {@link Ext.state.Manager}.
	 * This calls {@link Zarafa.hierarchy.data.HierarchyState#getStateNameForFolder getStateNameForFolder} on the {@link #state}.
	 *
	 * @param {Zarafa.hierarchy.data.MAPIFolderRecord} folder The folder for which the statename is requested
	 * @param {String} type The category for the folder for which the statename is requested (e.g. 'list' or 'tree').
	 * @return {String} The unique name for this component by which the {@link #getState state} must be saved.
	 */
	getStateName : function(folder, type)
	{
		if (this.stateful !== false) {
			return this.state.getStateNameForFolder(folder, type);
		}
	},

	/**
	 * When {@link #stateful} the State object which should be saved into the
	 * {@link Ext.state.Manager}.
	 * This calls {@link Zarafa.hierarchy.data.HierarchyState#getStateForFolder getStateForFolder} on the {@link #state}.
	 * @param {Zarafa.hierarchy.data.MAPIFolderRecord} folder The folder for which the state is requested
	 * @param {String} type The category for the folder for which the state is requested (e.g. 'list' or 'tree').
	 * @return {Object} The state object
	 */
	getState : function(folder, type)
	{
		if (this.stateful !== false) {
			return this.state.getStateForFolder(folder, type);
		}
	},

	/**
	 * Apply the given state to this object activating the properties which were previously
	 * saved in {@link Ext.state.Manager}.
	 * This calls {@link Zarafa.hierarchy.data.HierarchyState#applyStateForFolder applyStateForFolder} on the {@link #state}.
	 * @param {Zarafa.hierarchy.data.MAPIFolderRecord} folder The folder for which the state is provided
	 * @param {String} type The category for the folder for which the state is provided (e.g. 'list' or 'tree').
	 * @param {Object} state The state object
	 */
	applyState : function(folder, type, state)
	{
		if (this.stateful !== false) {
			this.state.applyStateForFolder(folder, type, state);
		}
	},

	/**
	 * Notification handler called by {@link #onNotify} when
	 * a {@link Zarafa.core.data.Notifications#objectModified objectModified}
	 * notification has been recieved.
	 *
	 * This will update the mapistore's information in hierarhcyStore.
	 *
	 * @param {Zarafa.core.data.Notifications} action The notification action
	 * @param {Ext.data.Record/Array} records The record or records which have been affected by the notification.
	 * @param {Object} data The data which has been recieved from the PHP-side which must be applied
	 * to the given records.
	 * @param {Number} timestamp The {@link Date#getTime timestamp} on which the notification was received
	 * @param {Boolean} success The success status, True if the notification was successfully recieved.
	 * @private
	 */
	onNotifyObjectmodified : function(action, records, data, timestamp, success)
	{
		if (!Array.isArray(records)) {
			records = [ records ];
		}

		// Temporarily disable event propagation, every store (which contains the provided records)
		// will receive this notification. So we have to disable event propagation to prevent
		// bouncing events around.
		for (var i = 0, len = records.length; i < len; i++) {
			var record = records[i];
			var singleData = (Array.isArray(data)) ? data[i] : data;

			record.setEventPropagation(false);

			if (singleData instanceof Ext.data.Record) {
				// Merge the changes into the record without using the JSONReader.
				record.applyData(singleData);
			} else {
				// Simply merge the record using the JsonReader, this will cause a 'update' event to be fired with
				// a COMMIT action. Because it is a commit, this store will not mark the record as dirty.
				this.reader.update(record, singleData);
			}

			record.setEventPropagation(true);
		}
	},

	/**
	 * Returns folder(s) which has the provided container-class except folders
	 * in "deleted items" or "Junk Email".
	 * @param {String} containerClass The container_class of the folders.
	 * @return {Zarafa.core.data.IPFRecord[]} An array of folders
	 */
	getByContainerClass : function(containerClass)
	{
		var finalResult = [];

		this.getStores().forEach( function(store) {
			var subFolders = store.getFolderStore().getRange();
			var subResult = subFolders.filter( function(folder) {
				return folder.get('container_class') === containerClass && !folder.isInDeletedItems();
			});

			finalResult = finalResult.concat(subResult);
		});

		return finalResult;
	}
});

Ext.reg('zarafa.hierarchystore', Zarafa.hierarchy.data.HierarchyStore);
