Ext.namespace('Zarafa.core.data');

Ext.data.Api.actions.open = 'open';

/**
 * @class Zarafa.core.data.MAPIStore
 * @extends Ext.data.GroupingStore
 * @xtype zarafa.mapistore
 *
 * Extension of the Extjs store which adding support for the 'open' command,
 * which is used by MAPI to request additional data for a record.
 */
Zarafa.core.data.MAPIStore = Ext.extend(Ext.data.GroupingStore, {
	/**
	 * @cfg {Boolean} persistentFilter True when the {@link #filter} which
	 * has been applied on this store should be reapplied when the store
	 * has been {@link #load loaded}
	 */
	persistentFilter : true,

	/**
	 * The currently active function which was given to {@link #filterBy}.
	 * @property
	 * @type Function
	 * @private
	 */
	filterFn : undefined,

	/**
	 * The currently active {@link #filterFn function} scope which was given to {@link #filterBy}.
	 * @property
	 * @type Object
	 * @private
	 */
	filterScope : undefined,
	
	/**
	 * Set to true when the {Zarafa.core.data.MAPIStore} starts saving, set to false when done.
	 * @property
	 * @type Boolean
	 * @private
	 */
	isSaving : false,

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

		Ext.applyIf(config, {
			// Don't automatically update changes to records to the server.
			autoSave : false,

			// When autoSave is false, indicates that CRUD operations are batched into a single request.
			batch : true
		});

		this.addEvents(
			/**
			 * @event open
			 * Fires when the {@link Zarafa.core.data.MAPIStore MAPIStore} gets extra data for a specific
			 * {@link Zarafa.core.data.MAPIRecord MAPIRecord}.
			 * @param {Zarafa.core.data.MAPIStore} store The {@link Zarafa.core.data.MAPIStore MAPIStore} which issues
			 * open request to get extra data for specific record.
			 * @param {Zarafa.core.data.MAPIRecord} record Record which is being opened to get extra information.
			 */
			'open'
		);

		Zarafa.core.data.MAPIStore.superclass.constructor.call(this, config);

		// Update the getKey function inside the MixedCollection to match
		// the one provided getRecordKey.
		this.data.getKey = this.getRecordKey;

		this.initEvents();
	},

	/**
	 * Initialize all events which Zarafa.core.data.MAPIStore MAPIStore} will listen to.
	 * @protected
	 */
	initEvents : function()
	{
		this.on('beforeload', this.onBeforeLoad, this);
		this.on('add', this.onAdd, this);
		this.on('remove', this.onRemove, this);
		this.on('write', this.onWrite, this);

		this.on('beforesave', function(){ this.isSaving = true; }, this);
		this.on('save', function(){ this.isSaving = false; }, this);
	},

	/**
	 * The {@link Ext.util.MixedCollection#getKey} function which must be
	 * applied to the {@link #data}{@link Ext.util.MixedCollection#getKey #getKey}
	 * function. This is assigned by the constructor and allows subclasses to
	 * simply override this function rather then apply it manually to {@link #data}
	 * themselves.
	 * @param {Ext.data.Record} o The record for which the key is requested
	 * @return {String} The key by which the record must be saved into the {@link Ext.util.MixedCollection}.
	 * @protected
	 */
	getRecordKey : Ext.util.MixedCollection.prototype.getKey,

	/**
	 * Check if a particular {@link Zarafa.core.Action action} is being executed
	 * by the {@link #proxy} of this store. When no action is given, this function
	 * will check if the proxy is busy with any action.
	 *
	 * @param {Zarafa.core.Action} action The action which is being checked
	 * @return {Boolean} True if the given action is being executed  by the proxy
	 */
	isExecuting : function(action)
	{
		return this.proxy.isExecuting(action);
	},

	/**
	 * Determine if a {@link #isExecuting 'list'} or {@link #isExecuting 'open'}
	 * request is still pending. And if so
	 * {@link Zarafa.core.data.MAPIProxy#cancelRequest cancel} those requests.
	 */
	cancelLoadRequests : function()
	{
		// If we are loading data, we want to cancel
		// the request as we don't want the data anymore.
		if (this.isExecuting('list')) {
			this.proxy.cancelRequests('list');
		}

		// If we are opening the record, we want to cancel
		// the request as we don't want the data anymore.
		if (this.isExecuting('open')) {
			this.proxy.cancelRequests('open');
		}

		// Saving is still interesting as the user might
		// not expect that action to be still pending.
	},

	/**
	 * Get the {@link Date#getTime timestamp} of the last time a response was given
	 * for the given action.
	 * @param {Zarafa.core.Action} action The action which is being checked
	 * @return {Number} The timestamp of the last action time
	 */
	lastExecutionTime : function(action)
	{
		return this.proxy.lastExecutionTime(action);
	},	

	/**
	 * <p>Reloads the Record cache from the configured Proxy. See the superclass {@link Ext.data.Store#reload documentation}
	 * for more detaiils.
	 * During reload we add an extra option into the {@link #load} argument which marks the action as a reload
	 * action.
	 */
	reload : function(options)
	{
		options = Ext.applyIf(options || {}, { reload : true });
		Zarafa.core.data.MAPIStore.superclass.reload.call(this, options);
	},

	/**
	 * Saves all pending changes to the store. See the superclass {@link Ext.data.Store#save documentation}
	 * for more details. Where the superclass saves all {@link #removed} and {@link #modified} records,
	 * this function will only save the records which are passed as argument.
	 *
	 * @param {Zarafa.core.data.MAPIRecord/Zarafa.core.data.MAPIRecord[]} records The records which
	 * must be saved to the server.
	 * @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(records) {
		// When no records are provided, fall back to the default behavior of the superclass.
		if (!Ext.isDefined(records)) {
			return Zarafa.core.data.MAPIStore.superclass.save.call(this);
		}

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

		if (!this.writer) {
			throw new Ext.data.Store.Error('writer-undefined');
		}

		var destroyed = [],
			created = [],
			updated = [],
			queue = [],
			trans,
			batch,
			data = {};

		for (var i = 0, len = records.length; i < len; i++) {
			var record = records[i];

			if (this.removed.indexOf(record) >= 0) {
				// Check for removed records first, a record located in this.removed is
				// guarenteed to be a non-phantom. See store.remove().
				destroyed.push(record);
			} else if (this.modified.indexOf(record) >= 0) {
				// Only accept valid records.
				if (record.isValid()) {
					if (record.phantom) {
						created.push(record);
					} else {
						updated.push(record);
					}
				}
			}
		}

		if (destroyed.length > 0) {
			queue.push(['destroy', destroyed]);
		}
		if (created.length > 0) {
			queue.push(['create', created]);
		}
		if (updated.length > 0) {
			queue.push(['update', updated]);
		}

		var len = queue.length;
		if(len){
			batch = ++this.batchCounter;
			for(var i = 0; i < len; ++i){
				trans = queue[i];
				data[trans[0]] = trans[1];
			}
			if(this.fireEvent('beforesave', this, data) !== false){
				for(var i = 0; i < len; ++i){
					trans = queue[i];
					this.doTransaction(trans[0], trans[1], batch);
				}
				return batch;
			}
		}
		return -1;
	},

	/**
	 * Event handler which is fired when we are about to (re)load the store.
	 * When this happens we should cancel all pending {@link #open} requests,
	 * as they cannot be completed anymore (the record will have been deleted,
	 * so the opened record has become useless.
	 * @private
	 */
	onBeforeLoad : function()
	{
		if (this.isExecuting('open')) {
			this.proxy.cancelRequests('open');
		}
	},

	/**
	 * Event handler which is raised when a {@link Zarafa.core.data.MAPIRecord MAPIRecord} has been added
	 * to this {@link Zarafa.core.data.MAPIStore MAPIStore}.
	 *
	 * @param {Zarafa.core.data.MAPIStore} store The {@link Zarafa.core.data.MAPIStore MAPIStore} to which the store was added.
	 * @param {Zarafa.core.data.MAPIRecord[]} records The array of {@link Zarafa.core.data.MAPIRecord records} which have been added.
	 * @param {Number} index The index at which the record(s) were added
	 * @private
	 */
	onAdd : function(store, records, index)
	{
		this.setRecordsStore(store, records);
	},

	/**
	 * Event handler which is raised when a {@link Zarafa.core.data.MAPIRecord MAPIRecord} has been removed
	 * from this {@link Zarafa.core.data.MAPIStore MAPIStore}.
	 *
	 * @param {Zarafa.core.data.MAPIStore} store The {@link Zarafa.core.data.MAPIStore MAPIStore} from which the records were removed.
	 * @param {Zarafa.core.data.MAPIRecord[]} records The array of {@link Zarafa.core.data.MAPIRecord records} which have been removed.
	 * @param {Number} index The index at which the record(s) were removed.
	 * @private
	 */
	onRemove: function(store, records, index)
	{
		this.setRecordsStore(undefined, records);
	},

	/**
	 * Event handler which is raised when the {@link #write} event has been fired. This will clear
	 * all {@link Zarafa.core.data.MAPIRecord#actions Message Actions} from the given records.
	 *
	 * @param {Zarafa.core.data.MAPIStore} store The store which fired the event
	 * @param {String} action [Ext.data.Api.actions.create|update|destroy]
	 * @param {Object} result The 'data' picked-out out of the response for convenience
	 * @param {Ext.Direct.Transaction} res The transaction
	 * @param {Record/Record[]} records The records which were written to the server
	 * @private
	 */
	onWrite : function(store, action, result, res, records)
	{
		if (!Array.isArray(records)) {
			records = [ records ];
		}

		for (var i = 0, len = records.length; i < len; i++) {
			records[i].clearMessageActions();
			records[i].clearActionResponse();
		}
	},

	/**
	 * Iterates through all {@link Zarafa.core.data.MAPIRecord records} and sets the
	 * reference to the {@link Zarafa.core.data.MAPIStore MAPIStore} to which it belongs.
	 *
	 * @param {Zarafa.core.data.MAPIStore} store The {@link Zarafa.core.data.MAPIStore MAPIStore} to which the
	 * {@link Zarafa.core.data.MAPIRecord records} must be assigned.
	 * @param {Zarafa.core.data.MAPIRecord[]} records The array of
	 * {@link Zarafa.core.data.MAPIRecord records} which must be updated.
	 * @private
	 */
	setRecordsStore : function(store, records)
	{
		records = Array.isArray(records) ? records : [ records ];
		Ext.each(records, function(record) { record.join(store); }, this);
	},

	/**
	 * Function is used to get extra properties from the server, which are not received in
	 * 'list' action. function will call {@link #execute} event, which is entry point for every
	 * CRUD operation, {@link #execute} will internall call {@link #createCallback} to create a
	 * callback function based on operation type ('open' -> onOpenRecords).
	 * @param {Zarafa.core.data.MAPIRecord} record record for which we need extra properties.
	 * @param {Object} options Extra options which can be used for opening the record
	 */
	open : function(record, options)
	{
		try {
			return this.execute('open', record, options);
		} catch (e) {
			this.handleException(e);
			return false;
		}
	},

	/**
	 * Function will work as callback function for 'open' operation, and update the
	 * existing record with the new data that is received from server.
	 * @param {Boolean} success true if operation completed successfully else false.
	 * @param {Zarafa.core.data.MAPIRecord} record updated record.
	 * @param {Object} data properties of record which is received from server (in key/value pair).
	 */
	onOpenRecords : function(success, record, data)
	{
		if (success === true && this.indexOf(record) > -1) {
			try {
				// call reader to update record data
				var oldRecord = record;
				
				this.reader.update(record, data);
				record.afterOpen();
				
				this.fireEvent('open', this, record, oldRecord);
			} catch (e) {
				this.handleException(e);
				if (Array.isArray(record)) {
					// Recurse to run back into the try {}.  DataReader#update splices-off the rs until empty.
					this.onOpenRecords(success, record, data);
				}
			}
		}
	},

	/**
	 * Get the Record with the specified id.
	 * If the {@link #reader} has the {@link Ext.data.JsonReader#idProperty} set to 'entryid',
	 * then this function will also use {@link Zarafa.core.EntryId#compareEntryIds}. For
	 * 'store_entryid' then {@link Zarafa.core.EntryId#compareStoreEntryIds} is used.
	 * @param {String} id The id of the Record to find.
	 * @return {Ext.data.Record} The Record with the passed id. Returns undefined if not found.
	 */
	getById : function(id)
	{
		// First use the original implementation
		var item = Zarafa.core.data.MAPIStore.superclass.getById.call(this, id);

		// If no item was found, and the reader uses the 'entryid' property,
		// we should retry searching using the compareEntryIds function. If that
		// fails as well, then the item is really not present.
		if (!item) {
			var index = this.findBy(function(record) { return this.idComparison(id, record.id); }, this);
			if (index >= 0) {
				item = this.getAt(index);
			}
		}

		return item;
	},

	/**
	 * Compare a {@link Ext.data.Record#id ids} to determine if they are equal.
	 * @param {String} a The first id to compare
	 * @param {String} b The second id to compare
	 * @protected
	 */
	idComparison : function(a, b)
	{
		return a === b;
	},

	/**
	 * Filter by a function. Returns a <i>new</i> collection that has been filtered.
	 * The passed function will be called with each object in the collection.
	 * If the function returns true, the value is included otherwise it is filtered.
	 * @param {Function} fn The function to be called, it will receive the args o (the object), k (the key)
	 * @param {Object} scope (optional) The scope (<code>this</code> reference) in which the function is executed. Defaults to this MixedCollection.
	 * @return {MixedCollection} The new filtered collection
	 */
	filterBy : function(fn, scope)
	{
		// Save the function for later usage.
		this.filterFn = fn;
		this.filterScope = scope;

		Zarafa.core.data.MAPIStore.superclass.filterBy.apply(this, arguments);
	},

	/**
	 * Revert to a view of the Record cache with no filtering applied.
	 * @param {Boolean} suppressEvent If <tt>true</tt> the filter is cleared silently without firing the
	 * {@link #datachanged} event.
	 */
	clearFilter : function()
	{
		// Reset the filter
		delete this.filterFn;
		delete this.filterScope;

		Zarafa.core.data.MAPIStore.superclass.clearFilter.apply(this, arguments);
	},

	/**
	 * Callback function which will be called when 'read' action is executed 
	 * and {@link Zarafa.core.data.JsonReader JsonReader} has deserialized data
	 * into {@link Zarafa.core.data.MAPIRecord MAPIRecord},
	 * so the records can be added to the {@link Zarafa.core.data.NoSyncStore NoSyncStore}.
	 * @param {Object} o response object containing array of {@link Zarafa.core.data.MAPIRecord MAPIRecord}
	 * and optionally a property indicating total number of records.
	 * @param {Object} options optionally can contain 'add' which will append {@link Zarafa.core.data.MAPIRecord MAPIRecord}
	 * to the existing set of cached {@link Zarafa.core.data.MAPIRecord MAPIRecord}.
	 * @private
	 */
	loadRecords : function(o, options, success)
	{
		Zarafa.core.data.MAPIStore.superclass.loadRecords.apply(this, arguments);

		if (this.persistentFilter === true && !this.isDestroyed && (!options || options.add !== true)) {
			if (this.filterFn) {
				this.filterBy(this.filterFn, this.filterScope);
			}
		}
	},

	/**
	 * Clear all data in the store
	 * @private
	 */
	clearData : function()
	{
		this.data.each(function(rec) {
			rec.destroy();
		});
		Zarafa.core.data.MAPIStore.superclass.clearData.apply(this, arguments);
	},

	/**
	 * Sort the data in the store using the given sort function.
	 * This will call {@link Ext.util.MixedCollection#sort sort} on the
	 * {@link #data} object.
	 * @param {String} direction (optional) 'ASC' or 'DESC'. Defaults to 'ASC'.
	 * @param {Function} fn (optional) Comparison function that defines the sort order. Defaults to sorting by numeric value.
	 */
	sortBy : function(direction, fn)
	{
		this.data.sort(direction, fn);
		if (this.snapshot && this.snapshot != this.data) {
			this.snapshot.sort(direction, fn);
		}
		this.fireEvent('datachanged', this);
	},

	/**
	 * Clears any existing grouping and refreshes the data using the default sort.
	 */
	clearGrouping : function()
	{
		// Only clear grouping when
		// grouping was previously applied
		if (this.groupField) {
			Zarafa.core.data.MAPIStore.superclass.clearGrouping.apply(this, arguments);
		}
	},

	/**
	 * Destroys the store
	 */
	destroy : function()
	{
		// Make sure we cancel all load requests
		// to the server as we are no longer
		// interested in the results.
		if (!this.isDestroyed) {
			this.cancelLoadRequests();
		}

		Zarafa.core.data.MAPIStore.superclass.destroy.apply(this, arguments);
	}
});

Ext.reg('zarafa.mapistore', Zarafa.core.data.MAPIStore);
