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

/**
 * @class Zarafa.core.data.JsonReader
 * @extends Ext.data.JsonReader
 *
 * This extension of the {@link Ext.data.JsonReader} supports {@link Zarafa.core.data.IPMStore stores}
 * which can hold different type of {@link Zarafa.core.data.IPMRecord records}.
 *
 * If in the constructor no recordType is provided, dynamic {@link Zarafa.core.data.IPMRecord record} type
 * support is assumed. With dynamic types, the incoming response data is used to determine which
 * {@link Zarafa.core.data.MAPIRecord MAPIRecord} must be constructed for each individual root element.
 */
Zarafa.core.data.JsonReader = Ext.extend(Ext.data.JsonReader, {
	/**
	 * In {@link #getEfMapping} we generate a mapping of all Record Fields per ObjectType,
	 * all mappings are stored in a special cache to prevent them from being regenerated
	 * each time.
	 * @property
	 * @type Object
	 * @private
	 */
	efCache : undefined,

	/**
	 * @cfg {Boolean} dynamicRecord Enable dynamic detection of the records
	 * which are read by this JsonReader. When enabled this will prefer using
	 * the {@link Zarafa.core.data.RecordFactory} to detect the recordType rather
	 * then using the {@link #recordType} directly. (defaults to true)
	 */
	dynamicRecord : true,

	/**
	 * @constructor
	 * @param {Object} meta Metadata configuration options.
	 * @param {Object} recordType (optional) Optional Record type matches the type
	 * which must be read from response. If no type is given, {@link Zarafa.core.data.JsonReader}
	 * will dynamicly detect the record type based on the response.
	 */
	constructor : function(meta, recordType)
	{
		meta = Ext.applyIf(meta || {}, {
			totalProperty : 'count',
			root : 'item',
			id : 'entryid',
			idProperty : 'entryid'
		});

		// Check if the meta object contained the successProperty.
		// Note that this must be called before the superclass constructor,
		// because the meta object will be altered there.
		var hasSuccessProperty = Ext.isDefined(meta.successProperty);

		// If no recordType is provided, enable the dynamic record handling
		// of the JsonReader.
		if (!Ext.isDefined(recordType)) {
			// FIXME: We shouldn't for IPM as base recordclass here, instead
			// this should be handled as configuration option, or something even smarter.
			recordType = Zarafa.core.data.RecordFactory.getRecordClassByMessageClass('IPM');
		}

		// Check we dynamic records are disabled. 
		if (Ext.isDefined(meta.dynamicRecord)) {
			this.dynamicRecord = meta.dynamicRecord;
		}

		if (this.dynamicRecord !== false) {
			this.efCache = {};
		}

		Zarafa.core.data.JsonReader.superclass.constructor.call(this, meta, recordType);

		// This fixes a bug in the Ext.data.JsonReader. Even when no successProperty
		// is given, the getSuccess function will still be implemented after which
		// the function will often fail due to the lack of the success property within
		// the response data.
		if (!hasSuccessProperty) {
			this.getSuccess = function(o) { return true; };
		}
	},

	/**
	 * Build the extractors which are used when reading the Json data. This initialized
	 * functions like {@link #getTotal}, {@link #getSuccess}, {@link #getId}.
	 * @private
	 */
	buildExtractors : function()
	{
		var s = this.meta;

		Zarafa.core.data.JsonReader.superclass.buildExtractors.call(this);

		// Wrap the original getId function to check if the data is the raw
		// data which has wrapped the 'props' field, or if this is the unwrapped
		// data.
		if (s.id || s.idProperty) {
			var old = this.getId;
			this.getId = function(rec) {
				if (rec.props) {
					var id = old(rec.props);
					if (!Ext.isEmpty(id)) {
						return id;
					}
				}

				return old(rec);
			};
		}
	},

	/**
	 * Obtain the mapping between response objects and {@link Zarafa.core.data.IPMRecord record} fields for
	 * the given recordType. If no mapping yet exist for this recordType one will be constructed
	 * and added to the {@link Zarafa.core.data.IPMRecord.efCache cache}.
	 *
	 * @param {String} key The unique key for this record type (used for caching purposes).
	 * @param {Array} items The array of {@link Zarafa.core.data.IPMRecord record} items.
	 * @param {Number} len The length of the items array. 
	 * @return {Array} The name/value list of response to {@link Zarafa.core.data.IPMRecord record} fields.
	 * @private
	 */
	getEfMapping : function(key, items, len)
	{
		if (Ext.isString(key)) {
			key = key.toUpperCase();
		}

		var ef = this.efCache[key];

		if (!Ext.isDefined(ef))
		{
			ef = [];
        	for(var i = 0; i < len; i++){
				var f = items[i];
				var map = (!Ext.isEmpty(f.mapping)) ? f.mapping : f.name;
				ef.push(this.createAccessor.call(this, map));
			}
			this.efCache[key] = ef;
		}

		return ef;
	},

	/**
	 * Type-casts a single row of raw-data from server
	 * @param {Object} data The data object which must be deserialized.
	 * @param {Array} items The {@link Ext.data.Field Field} used for deserialization.
	 * @param {Integer} len The length of the items array.
	 * @private
	 */
	extractValues : function(data, items, len)
	{
		var values = {};
	
		// If the data object is wrapped (it contains objects like 'props', 'attachments',
		// 'recipients', etc... Then we must call extractValues for each individual subobject.
		if (Ext.isDefined(data.props)) {
			values = Ext.apply({}, data);
			values.props = this.extractValues(data.props, items, len);
			return values;
		}

		if (this.dynamicRecord === true)
		{
			var recordType = Zarafa.core.data.RecordFactory.getRecordClassByRecordData(data);
			if (!Ext.isDefined(recordType)) {
				recordType = this.recordType;
			}

			items = recordType.prototype.fields.items;
			len = recordType.prototype.fields.length;

			// Normally the caller has initialized this.ef for us, but only at this time
			// do we know the real recordType. As such we have to override the previous
			// this.ef mapping.
			this.ef = this.getEfMapping(data.message_class || data.object_type, items, len);
		}

		// Extract the values per object which we want to deserialize.
		for (var j = 0; j < len; j++) {
			var f = items[j];
			var v = this.ef[j](data);
			if (Ext.isDefined(v)) {
				values[f.name] = f.convert(v, data);
			}
		}

		return values;
	},

	/**
	 * Returns extracted, type-cast rows of data.  Iterates to call #extractValues for each row
	 *
	 * This function is exactly copied from {@link Ext.data.DataReader.extractData} with the only
	 * difference is using the RecordFactory for record allocation.
	 *
	 * @param {Object|Array} data-root from server response
	 * @param {Boolean} returnRecords [false] Set true to return instances of {@link Zarafa.core.data.MAPIRecord MAPIRecord}.
	 * @private
	 */
	extractData : function(root, returnRecords)
	{
		// A bit ugly this, too bad the Record's raw data couldn't be saved in a common property named "raw" or something.
		var rawName = (this instanceof Ext.data.JsonReader) ? 'json' : 'node';

		var rs = [];

		// Had to add Check for XmlReader, #isData returns true if root is an Xml-object.  Want to check in order to re-factor
		// #extractData into DataReader base, since the implementations are almost identical for JsonReader, XmlReader
		if (this.isData(root) && !(this instanceof Ext.data.XmlReader)) {
			root = [root];
		}
		if (returnRecords === true) {
			for (var i = 0; i < root.length; i++) {
				var n = root[i];

				var record = undefined;
				var id = this.getId(n);
				var data = n.props || n;

				// Clear all data from the object which must be deserialized,
				// we only want the 'object_type' and 'message_class' properties.
				data = { message_class : data.message_class, object_type : data.object_type };

				if (this.dynamicRecord === true) {
					record = Zarafa.core.data.RecordFactory.createRecordObjectByRecordData(data, id);
				}

				if (!record) {
					record = new this.recordType({}, id);
				}

				// move primitive properties and identification properties at same level
				this.moveIdProperties(n, record.baseIdProperties);

				var f		= record.fields,
					fi		= f.items,
					fl		= f.length;
				this.update(record, this.extractValues(n, fi, fl));
				record[rawName] = n;    // <-- There's implementation of ugly bit, setting the raw record-data.
				rs.push(record);
			}
		} else {
			for (var i = 0; i < root.length; i++) {
				var n = root[i];

				var Record = undefined;
				if (this.dynamicRecord === true) {
					Record = Zarafa.core.data.RecordFactory.getRecordClassByRecordData(n.props || n);
				}
				
				// Fall back to specified record type if we can't get the type from the data
				if (!Record) {
					Record = this.recordType;
				}

				// move primitive properties and identification properties at same level
				this.moveIdProperties(n, Record.prototype.baseIdProperties);

				var f		= Record.prototype.fields,
					fi		= f.items,
					fl		= f.length;

				// here we can't do anything about complex structures so its ignored here
				var data = this.extractValues(n, fi, fl);
				data[this.meta.idProperty] = this.getId(n.props || n);
				rs.push(data);
			}
		}
		return rs;
	},

	/**
	 * Function will merge all identification properties and primitive properties
	 * to the props field and return the merged data. so {@link Zarafa.core.JsonReader JsonReader}
	 * can read the data and extract the properties.
	 * @param {Object} data data that is passed to {@link #extractData} to extract properties.
	 * @param {String|Array} idProperties The id properties which should be moved into the properties object.
	 * @return {Object} The updated data object.
	 */
	moveIdProperties : function(data, idProperties)
	{
		// If there is not data then return no data
		if (!data) {
			return data;
		}

		if (!data.props) {
			data.props = {};
		}

		// move the base identification property to <props> tag level
		var idProperty = this.meta.idProperty;
		if (idProperty) {
			var value = data[idProperty];
			if (Ext.isDefined(value)) {
				data.props[idProperty] = value;
				delete data[idProperty];
			}
		}

		// move all extra identification properties to <props> tag level
		if (Ext.isString(idProperties)) {
			var value = data[idProperties];
			if (Ext.isDefined(value)) {
				data.props[idProperties] = value;
				delete data[idProperties];
			}
		} else if (idProperties) {
			for (var i = 0, len = idProperties.length; i < len; i++) {
				var idProp = idProperties[i];
				var value = data[idProp];

				if(Ext.isDefined(value)) {
					data.props[idProp] = value;
					delete data[idProp];
				}
			}
		}

		return data;
	},

	/**
	 * Used for un-phantoming a record after a successful database insert.
	 * Sets the records pk along with new data from server.
	 * You must return at least the database pk using the idProperty defined in
	 * your DataReader configuration. The incoming data from server will be merged
	 * with the data in the local record. In addition, you must return record-data
	 * from the server in the same order received. Will perform a commit as well,
	 * un-marking dirty-fields. Store's "update" event will be suppressed.
	 *
	 * @param {Record/Record[]} record The phantom record to be realized.
	 * @param {Object/Object[]} data The new record data to apply. Must include the primary-key from database defined in idProperty field.
	 * @private
	 */
	realize : function(record, data)
	{
		// This function is copy & pasted from Ext.js Ext.data.JsonReader#realize.
		// Our only difference is the assignment of the record.data field.
		if (Array.isArray(record)) {
			for (var i = record.length - 1; i >= 0; i--) {
				// recurse
				if (Array.isArray(data)) {
					this.realize(record.splice(i,1).shift(), data.splice(i,1).shift());
				} else {
					// weird...record is an array but data isn't??  recurse but just send in the whole invalid data object.
					// the else clause below will detect !this.isData and throw exception.
					this.realize(record.splice(i,1).shift(), data);
				}
			}
		} else {
			// If records is NOT an array but data IS, see if data contains just 1 record.  If so extract it and carry on.
			if (Array.isArray(data)) {
				data = data.shift();
			}
			if (!this.isData(data)) {
				// TODO: Let exception-handler choose to commit or not rather than blindly records.commit() here.
				// record.commit();
				throw new Ext.data.DataReader.Error('realize', record);
			}
			record.phantom = false; // <-- That's what it's all about
			record._phid = record.id;  // <-- copy phantom-id -> _phid, so we can remap in Store#onCreateRecords
			record.id = this.getId(data);

			// And now the infamous line for which we copied this entire function.
			// Extjs expects that we transfer _all_ properties back from the server to the client after
			// a new item was created. Since this has a negative impact on the performance.
			//
			// This has been solved to let the server-side only return the properties which have
			// changed, or are new (like the entryid). We can then simply use Ext.apply to apply
			// the updated data over the already available data.
			//
			// But for those who have paid attention in the data flow of Extjs, know that this
			// sounds quite a lot like the function description of Ext.data.JsonReader#update.
			// 
			// So to make everything even simpler, we don'e update the record.data object here,
			// but instead we simply continue to the Ext.data.JsonReader#update function to
			// handle the rest of the work. 
			//
			//record.data = data;

			// Since we postpone the record.data update, there is no need to commit,
			// this too is done in update().
			//record.commit();

			// Time for the real work...
			this.update(record, data);

			// During realize the record might have received a new
			// id value. We have to reMap the store to update the keys.
			if (record.store) {
				record.store.reMap(record);
			}
		}
	},

	/**
	 * Used for updating a non-phantom or "real" record's data with fresh data from
	 * server after remote-save. If returning data from multiple-records after a batch-update,
	 * you must return record-data from the server in the same order received.
	 * Will perform a commit as well, un-marking dirty-fields. Store's "update" event
	 * will be suppressed as the record receives fresh new data-hash
	 *
	 * @param {Record/Record[]} record 
	 * @param {Object/Object[]} data
	 * @private
	 */
	update : function(record, data)
	{
		// Recursively call into update to update each record individually.
		if (Array.isArray(record)) {
			for (var i = 0; i < record.length; i++) {
				if(Array.isArray(data)) {
					this.update(record[i], data[i]);
				} else {
					this.update(record[i], data);
				}
			}
			return;
		}

		// It can happen that the data is wrapped in a array of length 1.
		if (Array.isArray(data)) {
			data = data.shift();
		}

		if (this.isData(data)) {
			// move primitive properties and identification properties at same level
			data = this.moveIdProperties(data, record.baseIdProperties);

			// All preprocessing has been done. All remaining data
			// can be applied into the IPMRecord directly.
			record.data = Ext.apply(record.data, data.props || data);

			// scalar values from props are applied so remove it from data object
			delete data.props;

			// Put the action response from the server in the record
			if(data.action_response){
				record.action_response = data.action_response;
				delete data.action_response;
			}

			// If the record contains substores to store complex data then we have to first 
			// serialize those data into its consecutive stores and then we can continue
			// with normal processing
			Ext.iterate(data, function(key, value) {
				if (Array.isArray(value) || Ext.isObject(value)) {
					var store;

					if (record.supportsSubStore(key)) {
						store = record.getSubStore(key);
						if (!store) {
							store = record.createSubStore(key);
						} else {
							store.removeAll(true);
						}

						// Load data into the SubStore, and remove
						// it from the data object.
						if (!Ext.isEmpty(value)) {
							store.loadData(value);
							delete data[key];
						}
					}
				}
			}, this);

			// Discard any additional data which was set on the data object,
			// this data has probably been set by plugins, but they have sufficient
			// alternatives to fit their custom data into the IPMRecord structure.
		}

		record.commit();
	}
});