/**
 * This type of matrix does all calculations in the browser.
 *
 * You need to provide at least a store that contains the records
 * that need to be processed.
 *
 * The store records are processed in batch jobs to avoid freezing the browser.
 * You can also configure how many records should be processed per job
 * and time to wait between jobs.
 *
 *
 * Example:
 *
 *      {
 *          xtype: 'pivotgrid',
 *          matrix: {
 *              type: 'local',
 *              store: 'yourStore',
 *              leftAxis: [...],
 *              topAxis: [...],
 *              aggregate: [...]
 *          }
 *      }
 *
 */
Ext.define('Ext.pivot.matrix.Local', {
    alternateClassName: [
        'Mz.aggregate.matrix.Local'
    ],
 
    extend: 'Ext.pivot.matrix.Base',
    
    alias:  'pivotmatrix.local',
 
    requires: [
        'Ext.pivot.matrix.Base',
        'Ext.pivot.axis.Local',
        'Ext.pivot.result.Local'
    ],
 
    resultType:     'local',
    leftAxisType:   'local',
    topAxisType:    'local',
 
    /**
     * Fires before updating the matrix data due to a change in the bound store.
     *
     * @event beforeupdate
     * @param {Ext.pivot.matrix.Base} matrix Reference to the Matrix object
     * @private
     */
 
    /**
     * Fires after updating the matrix data due to a change in the bound store.
     *
     * @event afterupdate
     * @param {Ext.pivot.matrix.Base} matrix Reference to the Matrix object
     * @private
     */
 
    /**
     * @cfg {Ext.data.Store/String} store (required)
     *
     * This is the store that needs to be processed. The store should contain all records
     * and cannot be paginated or buffered.
     */
    store:              null,
    
    /**
     * @cfg {Number} recordsPerJob
     *
     * The matrix processes the records in multiple jobs.
     * Specify here how many records should be processed in a single job.
     */
    recordsPerJob:      1000,
    
    /**
     * @cfg {Number} timeBetweenJobs
     *
     * How many milliseconds between processing jobs?
     */
    timeBetweenJobs:    2,
 
    onInitialize: function(){
        var me = this;
        
        me.localDelayedTask = new Ext.util.DelayedTask(me.delayedProcess, me);
        me.newRecordsDelayedTask = new Ext.util.DelayedTask(me.onOriginalStoreAddDelayed, me);
        me.updateRecordsDelayedTask = new Ext.util.DelayedTask(me.onOriginalStoreUpdateDelayed, me);
 
        me.initializeStore({store: me.store});
 
        me.callParent(arguments);
    },
 
    initializeStore: function(config){
        var me = this,
            store, newStore;
 
        if(config.store){
            // a new store was passed to
            newStore = config.store;
        }else{
            if(me.store){
                if(me.store.isStore && !me.storeListeners){
                    // we have a store but no listeners were attached to it
                    store = me.store;
                }else{
                    // we need to initialize the store that we got
                    newStore = me.store;
                }
            }
        }
 
        if(newStore){
            store = Ext.getStore(newStore || '');
            if(Ext.isEmpty(store) && Ext.isString(newStore)){
                store = Ext.create(newStore);
            }
        }
 
        if(store && store.isStore){
            Ext.destroy(me.storeListeners);
 
            if(me.store && me.store.autoDestroy && store != me.store){
                Ext.destroy(me.store);
            }
 
            // let's initialize the store (if needed)
            me.store = store;
            // add listeners to the store
            me.storeListeners = me.store.on({
                refresh:        me.startProcess,
                //datachanged:    me.startProcess,
                beforeload:     me.onOriginalStoreBeforeLoad,
                add:            me.onOriginalStoreAdd,
                update:         me.onOriginalStoreUpdate,
                remove:         me.onOriginalStoreRemove,
                clear:          me.startProcess,
                scope:          me,
                destroyable:    true
            });
 
            if(store.isLoaded()){
                me.startProcess();
            }
        }
    },
    
    onReconfigure: function(config){
        this.initializeStore(config);
        this.callParent(arguments);
    },
    
    onDestroy: function(){
        var me = this;
        
        me.localDelayedTask.cancel();
        me.localDelayedTask = null;
        me.newRecordsDelayedTask.cancel();
        me.newRecordsDelayedTask = null;
        me.updateRecordsDelayedTask.cancel();
        me.updateRecordsDelayedTask = null;
        
        if(Ext.isArray(me.records)){
            me.records.length = 0;
        }
        me.records = null;
        
        Ext.destroy(me.storeListeners);
        if(me.store && me.store.isStore && me.store.autoDestroy){
            Ext.destroy(me.store);
        }
        me.store = me.storeListeners = null;
        
        me.callParent(arguments);
    },
    
    /**
     * @private
     */
    onOriginalStoreBeforeLoad: function(store){
        this.fireEvent('start', this);
    },
    
    /**
     * @private
     */
    onOriginalStoreAdd: function(store, records){
        var me = this;
        
        me.newRecords = me.newRecords || [];
        me.newRecords = Ext.Array.merge(me.newRecords, Ext.Array.from(records));
        
        me.newRecordsDelayedTask.delay(100);
    },
    
    /**
     * @private
     */
    onOriginalStoreAddDelayed: function(){
        var me = this,
            i, records;
        
        records = Ext.Array.from(me.newRecords || []);
        for(= 0; i < records.length; i++){
            me.processRecord(records[i], i, records.length);
        }
        me.newRecords = [];
        
        me.leftAxis.tree = null;
        me.leftAxis.buildTree();
        
        me.topAxis.tree = null;
        me.topAxis.buildTree();
        
        me.recalculateResults(me.store, records);
    },
    
    /**
     * @private
     */
    onOriginalStoreUpdate: function(store, records){
        var me = this;
        
        me.updateRecords = me.updateRecords || [];
        me.updateRecords = Ext.Array.merge(me.updateRecords, Ext.Array.from(records));
        
        me.updateRecordsDelayedTask.delay(100);
    },
    
    /**
     * @private
     */
    onOriginalStoreUpdateDelayed: function(){
        var me = this;
        
        me.recalculateResults(me.store, me.updateRecords);
        me.updateRecords.length = 0;
    },
    
    /**
     * @private
     */
    onOriginalStoreRemove: function(store, record, index, isMove){
        if(isMove){
            //don't do anything. nothing changed in the data
            return;
        }
        
        // this can also be optimized to just remove axis items if necessary
        this.startProcess();
    },
    
    /**
     * @private
     */
    isReallyDirty: function(store, records){
        var found = true,
            items = this.leftAxis.dimensions,
            lenD = items.getCount(),
            i, j, lenR, record, dimension;
        
        records = Ext.Array.from(records);
        lenR = records.length;
 
        // for all records find out if there's a new axis value
        for(= 0; i < lenD; i++){
            dimension = items.getAt(i);
            for(= 0; j < lenR; j++){
                record = records[j];
                found = (record && record.isModel && dimension.getValues().containsKey(record.get(dimension.dataIndex)));
                if(!found){
                    break;
                }
            }
            if(!found){
                break;
            }
        }
 
        return !found;
    },
    
    /**
     * @private
     */
    recalculateResults: function(store, records){
        var me = this;
        
        if(me.isReallyDirty(store, records)){
            me.startProcess();
            return;
        }
        
        me.fireEvent('beforeupdate', me);
 
        // recalculate all results
        me.results.calculate();
        // now update the pivot store records
        Ext.Array.each(me.leftAxis.getTree(), me.updateRecordToPivotStore, me);
        // update all grand totals
        me.updateGrandTotalsToPivotStore();
 
        me.fireEvent('afterupdate', me);
    },
 
    /**
     * @private
     */
    updateGrandTotalsToPivotStore: function(){
        var me = this,
            totals = [],
            i;
        
        if(me.totals.length <= 0){
            return;
        }
 
        totals.push({
            title:      me.textGrandTotalTpl,
            values:     me.preparePivotStoreRecordData({key: me.grandTotalKey})
        });
        
        // additional grand totals can be added. collect these using events or 
        if(Ext.isFunction(me.onBuildTotals)){
            me.onBuildTotals(totals);
        }
        me.fireEvent('buildtotals', me, totals);
        
        // update records to the pivot store for each grand total
        if(me.totals.length === totals.length){
            for(= 0; i < me.totals.length; i++){
                if(Ext.isObject(totals[i]) && Ext.isObject(totals[i].values) && (me.totals[i].record instanceof Ext.data.Model) ){
                    delete(totals[i].values.id);
                    me.totals[i].record.set(totals[i].values);
                }
            }
        }
    },
    
    /**
     * @private
     */
    updateRecordToPivotStore: function(item){
        var me = this,
            dataIndex, data;
 
        if(!item.children){
            if(item.record){
                data = me.preparePivotStoreRecordData(item);
                delete(data['id']);
                item.record.set(data);
            }
        }else{
            // update all pivot store records of this item
            if(item.records){
                dataIndex = (me.viewLayoutType == 'outline' ? item.dimensionId : me.compactViewKey);
                data = me.preparePivotStoreRecordData(item);
                delete(data['id']);
                delete(data[dataIndex]);
                item.records.collapsed.set(data);
                if(me.rowSubTotalsPosition == 'first'){
                    item.records.expanded.set(data);
                }else{
                    if(me.rowSubTotalsPosition == 'last') {
                        item.records.footer.set(data);
                    }
                }
            }
 
            Ext.Array.each(item.children, me.updateRecordToPivotStore, me);
        }
    },
    
    startProcess: function(){
        var me = this;
        
        // if we don't have a store then do nothing
        if(!me.store || (me.store && !me.store.isStore) || me.isDestroyed || me.store.isLoading()){
            // nothing to do
            return;
        }
 
        me.clearData();
        
        me.localDelayedTask.delay(50);
    },
    
    delayedProcess: function(){
        var me = this;
        
        // let's start the process
        me.fireEvent('start', me);
        
        me.records = me.store.getRange();
 
        if(me.records.length == 0){
            me.endProcess();
            return;
        }
        
        me.statusInProgress = false;
 
        me.processRecords(0);
    },
    
    processRecords: function(position){
        var me = this,
            i = position,
            totalLength;
        
        // don't do anything if the matrix was destroyed while doing calculations.
        if(me.isDestroyed){
            return;
        }
 
        totalLength = me.records.length;
        
        me.statusInProgress = true;
 
        while(< totalLength && i < position + me.recordsPerJob && me.statusInProgress){
            me.processRecord(me.records[i], i, totalLength);
            i++;
        }
        
        // if we reached the last record then stop the process
        if(>= totalLength){
            me.statusInProgress = false;
            
            // now that the cells matrix was built let's calculate the aggregates
            me.results.calculate();
 
            // let's build the trees and apply value filters
            me.leftAxis.buildTree();
            me.topAxis.buildTree();
 
            // recalculate everything after applying the value filters
            if(me.filterApplied){
                me.results.calculate();
            }
 
            me.records = null;
            me.endProcess();
            return;
        }
        
        // if the matrix was not reconfigured meanwhile then start a new job
        if(me.statusInProgress && totalLength > 0){
            Ext.defer(me.processRecords, me.timeBetweenJobs, me, [i]);
        }
    },
    
    /**
     * Process the specified record and fire the 'progress' event
     *
     * @private
     */
    processRecord: function(record, index, length){
        var me = this,
            grandTotalKey = me.grandTotalKey,
            leftItems, topItems, i, j;
        
        // if null is returned that means it was filtered out
        // if array was returned that means it is valid
        leftItems = me.leftAxis.processRecord(record);
        topItems = me.topAxis.processRecord(record);
 
        // left and top items are added to their respective axis if the record
        // is not filtered out on both axis
        if(leftItems && topItems){
            me.results.add(grandTotalKey, grandTotalKey).addRecord(record);
 
            for (= 0; i < topItems.length; i++) {
                me.topAxis.addItem(topItems[i]);
                me.results.add(grandTotalKey, topItems[i].key).addRecord(record);
            }
 
            for (= 0; i < leftItems.length; i++) {
                me.leftAxis.addItem(leftItems[i]);
 
                me.results.add(leftItems[i].key, grandTotalKey).addRecord(record);
 
                for (= 0; j < topItems.length; j++) {
                    me.results.add(leftItems[i].key, topItems[j].key).addRecord(record);
                }
            }
        }
 
        me.fireEvent('progress', me, index + 1, length);
    },
    
    /**
     * Fetch all records that belong to the specified row group
     *
     * @param {String} key Row group key
     */
    getRecordsByRowGroup: function(key){
        var results = this.results.getByLeftKey(key),
            length = results.length,
            records = [], 
            i;
            
        for(= 0; i < length; i++){
            records = Ext.Array.merge(records, results[i].records || []);
        }
        
        return records;
    },
    
    /**
     * Fetch all records that belong to the specified col group
     *
     * @param {String} key Col group key
     */
    getRecordsByColGroup: function(key){
        var results = this.results.getByTopKey(key),
            length = results.length,
            records = [], 
            i;
            
        for(= 0; i < length; i++){
            records = Ext.Array.merge(records, results[i].records || []);
        }
        
        return records;
    },
    
    /**
     * Fetch all records that belong to the specified row/col group
     *
     * @param {String} rowKey Row group key
     * @param {String} colKey Col group key
     */
    getRecordsByGroups: function(rowKey, colKey){
        var result = this.results.get(rowKey, colKey);
        
        return ( result ? result.records || [] : []);
    }
    
});