/**
 * Base implementation of a pivot matrix.
 *
 * This class contains basic methods that are used to generate the pivot data. It
 * needs to be extended by other classes to properly generate the results.
 */
Ext.define('Ext.pivot.matrix.Base', {
    alternateClassName: [
        'Mz.aggregate.matrix.Abstract'
    ],
 
    extend: 'Ext.util.Observable',
    
    alias: 'pivotmatrix.base',
 
    mixins: [
        'Ext.mixin.Factoryable'
    ],
 
    requires: [
        'Ext.util.DelayedTask',
        'Ext.data.ArrayStore',
        'Ext.XTemplate',
        'Ext.pivot.Aggregators',
        'Ext.pivot.MixedCollection',
        'Ext.pivot.axis.Base',
        'Ext.pivot.dimension.Item',
        'Ext.pivot.result.Collection'
    ],
 
    /**
     * Fires before the generated data is destroyed.
     * The components that uses the matrix should unbind this pivot store before is destroyed.
     * The grid panel will trow errors if the store is destroyed and the grid is refreshed.
     *
     * @event cleardata
     * @param {Ext.pivot.matrix.Base} matrix Reference to the Matrix object
     */
 
    /**
     * Fires before the matrix is reconfigured.
     *
     * Return false to stop reconfiguring the matrix.
     *
     * @event beforereconfigure
     * @param {Ext.pivot.matrix.Base} matrix Reference to the Matrix object
     * @param {Object} config Object used to reconfigure the matrix
     */
 
    /**
     * Fires when the matrix is reconfigured.
     *
     * @event reconfigure
     * @param {Ext.pivot.matrix.Base} matrix Reference to the Matrix object
     * @param {Object} config Object used to reconfigure the matrix
     */
 
    /**
     * Fires when the matrix starts processing the records.
     *
     * @event start
     * @param {Ext.pivot.matrix.Base} matrix Reference to the Matrix object
     */
 
    /**
     * Fires during records processing.
     *
     * @event progress
     * @param {Ext.pivot.matrix.Base} matrix Reference to the Matrix object
     * @param {Integer} index Current index of record that is processed
     * @param {Integer} total Total number of records to process
     */
 
    /**
     * Fires when the matrix finished processing the records
     *
     * @event done
     * @param {Ext.pivot.matrix.Base} matrix Reference to the Matrix object
     */
 
    /**
     * Fires after the matrix built the store model.
     *
     * @event modelbuilt
     * @param {Ext.pivot.matrix.Base} matrix Reference to the Matrix object
     * @param {Ext.data.Model} model The built model
     */
 
    /**
     * Fires after the matrix built the columns.
     *
     * @event columnsbuilt
     * @param {Ext.pivot.matrix.Base} matrix Reference to the Matrix object
     * @param {Array} columns The built columns
     */
 
    /**
     * Fires after the matrix built a pivot store record.
     *
     * @event recordbuilt
     * @param {Ext.pivot.matrix.Base} matrix Reference to the Matrix object
     * @param {Ext.data.Model} record The built record
     * @param {Ext.pivot.axis.Item} item The left axis item the record was built for
     */
 
    /**
     * Fires before grand total records are created in the pivot store.
     * Push additional objects to the array if you need to create additional grand totals.
     *
     * @event buildtotals
     * @param {Ext.pivot.matrix.Base} matrix Reference to the Matrix object
     * @param {Array} totals Array of objects that will be used to create grand total records
     * in the pivot store. Each object should have:
     * @param {String} totals.title Name your grand total
     * @param {Object} totals.values Values used to generate the pivot store record
     */
 
    /**
     * Fires after the matrix built the pivot store.
     *
     * @event storebuilt
     * @param {Ext.pivot.matrix.Base} matrix Reference to the Matrix object
     * @param {Ext.data.Store} store The built store
     */
 
    /**
     * @cfg {String} [type=abstract]
     *
     * Used when you define a filter on a dimension to set what kind of filter is to be
     * instantiated.
     */
 
    /**
     * @cfg {String} resultType
     *
     * Define the type of Result this class uses. Specify here the pivotresult alias.
     */
    resultType: 'base',
 
    /**
     * @cfg {String} leftAxisType
     *
     * Define the type of left Axis this class uses. Specify here the pivotaxis alias.
     */
    leftAxisType: 'base',
 
    /**
     * @cfg {String} topAxisType
     *
     * Define the type of top Axis this class uses. Specify here the pivotaxis alias.
     */
    topAxisType: 'base',
 
    /**
     * @cfg {String} textRowLabels
     *
     * In compact layout only one column is generated for the left axis dimensions.
     * This is value of that column header.
     */
    textRowLabels: 'Row labels',
    
    /**
     * @cfg {String} textTotalTpl Configure the template for the group total.
     * (i.e. '{name} ({rows.length} items)')
     * @cfg {String} textTotalTpl.groupField The field name being grouped by.
     * @cfg {String} textTotalTpl.name Group name
     * @cfg {Ext.data.Model[]} textTotalTpl.rows An array containing the child records for the group
     * being rendered.
     */
    textTotalTpl: 'Total ({name})',
 
    /**
     * @cfg {String} textGrandTotalTpl Configure the template for the grand total.
     */
    textGrandTotalTpl: 'Grand total',
 
    /**
     * @cfg {String} keysSeparator
     *
     * An axis item has a key that is a combination of all its parents keys. This is the keys
     * separator.
     *
     * Do not use regexp special chars for this.
     */
    keysSeparator: '#_#',
    
    /**
     * @cfg {String} grandTotalKey
     *
     * Generic key used by the grand total records.
     */
    grandTotalKey: 'grandtotal',
    
    /**
     * @cfg {String} compactViewKey
     *
     * In compact view layout mode the matrix generates only one column for all left axis
     * dimensions. This is the 'dataIndex' field name on the pivot store model.
     */
    compactViewKey: '_compactview_',
 
    /**
     * @cfg {Number} compactViewColumnWidth
     *
     * In compact view layout mode the matrix generates only one column for all left axis
     * dimensions. This is the width of that column.
     */
    compactViewColumnWidth: 200,
    
    /**
     * @cfg {String} viewLayoutType Type of layout used to display the pivot data.
     * Possible values: outline, compact, tabular
     */
    viewLayoutType: 'outline',
 
    /**
     * @cfg {String} rowSubTotalsPosition Possible values: `first`, `none`, `last`
     */
    rowSubTotalsPosition: 'first',
 
    /**
     * @cfg {String} rowGrandTotalsPosition Possible values: `first`, `none`, `last`
     */
    rowGrandTotalsPosition: 'last',
 
    /**
     * @cfg {String} colSubTotalsPosition Possible values: `first`, `none`, `last`
     */
    colSubTotalsPosition: 'last',
 
    /**
     * @cfg {String} colGrandTotalsPosition Possible values: `first`, `none`, `last`
     */
    colGrandTotalsPosition: 'last',
 
    /**
     * @cfg {Boolean} showZeroAsBlank Should 0 values be displayed as blank?
     *
     */
    showZeroAsBlank: false,
 
    /**
     * @cfg {Boolean} calculateAsExcel
     *
     * Set to true if you want calculations to be done like in Excel.
     *
     * Set to false if you want all non numeric values to be treated as 0.
     *
     * Let's say we have the following values: 2, null, 4, 'test'.
     * - when `calculateAsExcel` is true then we have the following results: sum: 6; min: 2; max: 4;
     *   avg: 3; count: 3
     * - if `calculateAsExcel` is false then the results are: sum = 6, min: null; max: 4; avg: 1.5;
     *   count: 4
     */
    calculateAsExcel: false,
 
    /**
     * @cfg {Ext.pivot.axis.Base} leftAxis
     *
     * Left axis object stores all generated groups for the left axis dimensions
     */
    leftAxis: null,
 
    /**
     * @cfg {Ext.pivot.axis.Base} topAxis
     *
     * Top axis object stores all generated groups for the top axis dimensions
     */
    topAxis: null,
 
    /**
     * @cfg {Ext.pivot.MixedCollection} aggregate
     *
     * Collection of configured aggregate dimensions
     */
    aggregate: null,
    
    /**
     * @property {Ext.pivot.result.Collection} results
     * @readonly
     *
     * Stores the calculated results
     */
    results: null,
    
    /**
     * @property {Ext.data.ArrayStore} pivotStore
     * @readonly
     *
     * The generated pivot store
     *
     * @private
     */
    pivotStore: null,
    
    /**
     * @property {Boolean} isDestroyed
     * @readonly
     *
     * This property is set to true when the matrix object is destroyed.
     * This is useful to check when functions are deferred.
     */
    isDestroyed: false,
 
    /**
     * @cfg {Ext.Component} cmp (required)
     *
     * Reference to the pivot component that monitors this matrix.
     */
    cmp: null,
 
    /**
     * @cfg {Boolean} useNaturalSorting
     *
     * Set to true if you want to use natural sorting algorithm when sorting dimensions.
     *
     * For performance reasons this is turned off by default.
     */
    useNaturalSorting: false,
 
    /**
     * @cfg {Boolean} collapsibleRows
     *
     * Set to false if you want row groups to always be expanded and the buttons that
     * expand/collapse groups to be hidden in the UI.
     */
    collapsibleRows: true,
 
    /**
     * @cfg {Boolean} collapsibleColumns
     *
     * Set to false if you want column groups to always be expanded and the buttons that
     * expand/collapse groups to be hidden in the UI.
     */
    collapsibleColumns: true,
 
    isPivotMatrix: true,
 
    serializeProperties: [
        'viewLayoutType', 'rowSubTotalsPosition', 'rowGrandTotalsPosition',
        'colSubTotalsPosition', 'colGrandTotalsPosition', 'showZeroAsBlank',
        'collapsibleRows', 'collapsibleColumns'
    ],
 
    constructor: function(config) {
        var ret = this.callParent(arguments);
 
        this.initialize(true, config);
 
        return ret;
    },
    
    destroy: function() {
        var me = this;
        
        me.delayedTask.cancel();
        me.delayedTask = null;
        
        if (Ext.isFunction(me.onDestroy)) {
            me.onDestroy();
        }
        
        Ext.destroy(me.results, me.leftAxis, me.topAxis, me.aggregate, me.pivotStore);
        me.results = me.leftAxis = me.topAxis = me.aggregate = me.pivotStore = null;
        
        if (Ext.isArray(me.columns)) {
            me.columns.length = 0;
        }
 
        if (Ext.isArray(me.model)) {
            me.model.length = 0;
        }
 
        if (Ext.isArray(me.totals)) {
            me.totals.length = 0;
        }
 
        me.columns = me.model = me.totals = me.keysMap = me.cmp = me.modelInfo = null;
        
        me.isDestroyed = true;
 
        me.callParent(arguments);
    },
    
    /**
     * The arguments are combined in a string and the function returns the crc32
     * for that key
     *
     * @deprecated 6.0.0 This method is deprecated.
     * @method formatKeys
     * @returns {String} 
     */
 
    /**
     * Return a unique id for the specified value. The function builds a keys map so that
     * same values get same ids.
     *
     * @param value
     * @returns {String} 
     *
     * @private
     */
    getKey: function(value) {
        var me = this;
 
        me.keysMap = me.keysMap || {};
 
        if (!Ext.isDefined(me.keysMap[value])) {
            me.keysMap[value] = Ext.id();
        }
 
        return me.keysMap[value];
    },
 
    /**
     * Natural Sort algorithm for Javascript - Version 0.8 - Released under MIT license
     * Author: Jim Palmer (based on chunking idea from Dave Koelle)
     * https://github.com/overset/javascript-natural-sort/blob/master/naturalSort.js
     *
     * @private
     */
    naturalSort: (function() {
        /* eslint-disable no-useless-escape */
        var re = /(^([+\-]?(?:\d*)(?:\.\d*)?(?:[eE][+\-]?\d+)?)?$|^0x[\da-fA-F]+$|\d+)/g,
            sre = /^\s+|\s+$/g,   // trim pre-post whitespace
            snre = /\s+/g,        // normalize all whitespace to single ' ' character
            dre = /(^([\w ]+,?[\w ]+)?[\w ]+,?[\w ]+\d+:\d+(:\d+)?[\w ]?|^\d{1,4}[\/\-]\d{1,4}[\/\-]\d{1,4}|^\w+\w+ \d+\d{4})/,
            hre = /^0x[0-9a-f]+$/i,
            ore = /^0/,
            normChunk = function(s, l) {
                // normalize spaces; find floats not starting with '0', string or 0 if not defined
                // (Clint Priest)
                s = s || '';
 
                return (!s.match(ore) || l === 1) &&
                       parseFloat(s) || s.replace(snre, ' ').replace(sre, '') || 0;
            };
        /* eslint-enable no-useless-escape */
 
        return function(a, b) {
            // convert all to strings strip whitespace
            var x = String(instanceof Date ? a.getTime() : (|| '')).replace(sre, ''),
                y = String(instanceof Date ? b.getTime() : (|| '')).replace(sre, ''),
                // chunk/tokenize
                xN = x.replace(re, '\0$1\0').replace(/\0$/, '').replace(/^\0/, '').split('\0'),
                yN = y.replace(re, '\0$1\0').replace(/\0$/, '').replace(/^\0/, '').split('\0'),
                // numeric, hex or date detection
                xD = parseInt(x.match(hre), 16) || (xN.length !== 1 && Date.parse(x)),
                yD = parseInt(y.match(hre), 16) || xD && y.match(dre) && Date.parse(y) || null,
                oFxNcL, oFyNcL, cLoc, xNl, yNl, numS;
 
            // first try and sort Hex codes or Dates
            if (yD) {
                if (xD < yD) {
                    return -1;
                }
                else if (xD > yD) {
                    return 1;
                }
            }
 
            // natural sorting through split numeric strings and default strings
            // eslint-disable-next-line max-len
            for (cLoc = 0, xNl = xN.length, yNl = yN.length, numS = Math.max(xNl, yNl); cLoc < numS; cLoc++) {
                oFxNcL = normChunk(xN[cLoc], xNl);
                oFyNcL = normChunk(yN[cLoc], yNl);
 
                // handle numeric vs string comparison - number < string - (Kyle Adams)
                if (isNaN(oFxNcL) !== isNaN(oFyNcL)) {
                    return (isNaN(oFxNcL)) ? 1 : -1;
                }
                // rely on string comparison if different types - i.e. '02' < 2 != '02' < '2'
                else if (typeof oFxNcL !== typeof oFyNcL) {
                    oFxNcL += '';
                    oFyNcL += '';
                }
 
                if (oFxNcL < oFyNcL) {
                    return -1;
                }
 
                if (oFxNcL > oFyNcL) {
                    return 1;
                }
            }
 
            return 0;
        };
    }()),
 
    /**
     *
     * Initialize the matrix with the new config object
     *
     * @param firstTime
     * @param config
     *
     * @private
     *
     */
    initialize: function(firstTime, config) {
        var me = this,
            props = me.serializeProperties,
            i;
 
        config = config || {};
 
        // initialize the results object
        me.initResults();
        
        if (firstTime || config.aggregate) {
            // initialize aggregates
            me.initAggregates(config.aggregate || []);
        }
 
        if (firstTime || config.leftAxis) {
            // initialize dimensions and build axis tree
            me.initLeftAxis(config.leftAxis || []);
        }
 
        if (firstTime || config.topAxis) {
            // initialize dimensions and build axis tree
            me.initTopAxis(config.topAxis || []);
        }
 
        for (= 0; i < props.length; i++) {
            if (Ext.isDefined(config[props[i]])) {
                me[props[i]] = config[props[i]];
            }
        }
 
        me.totals = [];
        me.modelInfo = {};
        me.keysMap = null;
 
        if (!firstTime) {
            if (!me.collapsibleRows) {
                me.leftAxis.expandAll();
            }
 
            if (!me.collapsibleColumns) {
                me.topAxis.expandAll();
            }
        }
 
        if (firstTime) {
            me.pivotStore = new Ext.data.ArrayStore({
                autoDestroy: false,
                fields: []
            });
            
            me.delayedTask = new Ext.util.DelayedTask(me.startProcess, me);
            
            if (Ext.isFunction(me.onInitialize)) {
                me.onInitialize();
            }
        }
 
        // after initializing the matrix we can start processing data
        // we do it in a delayed task because a reconfigure may follow shortly
        me.delayedTask.delay(5);
    },
    
    /**
     * @method
     * Template method called to do your internal initialization when you extend this class.
     */
    onInitialize: Ext.emptyFn,
    
    /**
     * @method
     * Template method called before destroying the instance.
     */
    onDestroy: Ext.emptyFn,
    
    /**
     * Call this function to reconfigure the matrix with a new set of configs
     *
     * @param {Object} config Config object which has all configs that you want to change
     * on this instance
     */
    reconfigure: function(config) {
        var me = this,
            cfg = Ext.clone(config || {});
 
        if (me.fireEvent('beforereconfigure', me, cfg) !== false) {
            if (Ext.isFunction(me.onReconfigure)) {
                me.onReconfigure(cfg);
            }
 
            // the user can change values on the config before reinitializing the matrix
            me.fireEvent('reconfigure', me, cfg);
            me.initialize(false, cfg);
            me.clearData();
        }
        else {
            me.delayedTask.cancel();
        }
    },
    
    /**
     * @method
     *
     * Template function called when the matrix has to be reconfigured with a new set of configs.
     *
     * @param {Object} config Config object which has all configs that need to be changed on
     * this instance
     */
    onReconfigure: Ext.emptyFn,
 
    /**
     * Returns the serialized matrix configs.
     *
     * @return {Object} 
     */
    serialize: function() {
        var me = this,
            props = me.serializeProperties,
            len = props.length,
            cfg = {},
            i, prop;
 
        for (= 0; i < len; i++) {
            prop = props[i];
            cfg[prop] = me[prop];
        }
 
        cfg.leftAxis = me.serializeDimensions(me.leftAxis.dimensions);
        cfg.topAxis = me.serializeDimensions(me.topAxis.dimensions);
        cfg.aggregate = me.serializeDimensions(me.aggregate);
 
        return cfg;
    },
 
    /**
     * Serialize the given dimensions collection
     *
     * @param dimensions
     * @return {Array} 
     * @private
     */
    serializeDimensions: function(dimensions) {
        var len = dimensions.getCount(),
            cfg = [],
            i;
 
        for (= 0; i < len; i++) {
            cfg.push(dimensions.getAt(i).serialize());
        }
 
        return cfg;
    },
 
    /**
     * Initialize the Results object
     *
     * @private
     */
    initResults: function() {
        Ext.destroy(this.results);
        this.results = new Ext.pivot.result.Collection({
            resultType: this.resultType,
            matrix: this
        });
    },
    
    /**
     * @private
     */
    initAggregates: function(dimensions) {
        var me = this,
            i, item;
        
        Ext.destroy(me.aggregate);
        me.aggregate = new Ext.pivot.MixedCollection();
 
        me.aggregate.getKey = function(item) {
            return item.getId();
        };
        
        if (Ext.isEmpty(dimensions)) {
            return;
        }
 
        dimensions = Ext.Array.from(dimensions);
        
        for (= 0; i < dimensions.length; i++) {
            item = dimensions[i];
 
            if (!item.isInstance) {
                Ext.applyIf(item, {
                    isAggregate: true,
                    align: 'right',
                    showZeroAsBlank: me.showZeroAsBlank
                });
                item = new Ext.pivot.dimension.Item(item);
            }
 
            item.matrix = this;
 
            me.aggregate.add(item);
        }
    },
 
    /**
     * @private
     */
    initLeftAxis: function(dimensions) {
        var me = this;
 
        dimensions = Ext.Array.from(dimensions || []);
        Ext.destroy(me.leftAxis);
        me.leftAxis = Ext.Factory.pivotaxis({
            type: me.leftAxisType,
            matrix: me,
            dimensions: dimensions,
            isLeftAxis: true
        });
    },
 
    /**
     * @private
     */
    initTopAxis: function(dimensions) {
        var me = this;
 
        dimensions = Ext.Array.from(dimensions || []);
        Ext.destroy(me.topAxis);
        me.topAxis = Ext.Factory.pivotaxis({
            type: me.topAxisType,
            matrix: me,
            dimensions: dimensions,
            isLeftAxis: false
        });
    },
 
    /**
     * This function clears any data that was previously calculated/generated.
     *
     */
    clearData: function() {
        var me = this;
        
        me.fireEvent('cleardata', me);
        
        me.leftAxis.clear();
        me.topAxis.clear();
        me.results.clear();
        
        if (Ext.isArray(me.columns)) {
            me.columns.length = 0;
        }
        
        if (Ext.isArray(me.model)) {
            me.model.length = 0;
        }
        
        me.totals = [];
        me.modelInfo = {};
        me.keysMap = null;
        
        if (me.pivotStore) {
            me.pivotStore.removeAll(true);
        }
    },
    
    /**
     * Template function called when the calculation process is started.
     * This function needs to be implemented in the subclass.
     */
    startProcess: Ext.emptyFn,
    
    /**
     * Call this function after you finished your matrix processing.
     * This function will build up the pivot store and column headers.
     */
    endProcess: function() {
        var me = this;
        
        // force tree creation on both axis
        me.leftAxis.getTree();
        me.topAxis.getTree();
        
        // build pivot store model and column headers
        me.buildModelAndColumns();
        
        // build pivot store rows
        me.buildPivotStore();
 
        if (Ext.isFunction(me.onBuildStore)) {
            me.onBuildStore(me.pivotStore);
        }
 
        me.fireEvent('storebuilt', me, me.pivotStore);
        
        me.fireEvent('done', me);
    },
    
    /**
     * @method
     *
     * Template function called after the pivot store model was created.
     * You could extend the model in a subclass if you implement this method.
     *
     * @param {Array} model 
     */
    onBuildModel: Ext.emptyFn,
    
    /**
     * @method
     *
     * Template function called after the pivot columns were created.
     * You could extend the columns in a subclass if you implement this method.
     *
     * @param {Array} columns 
     */
    onBuildColumns: Ext.emptyFn,
    
    /**
     * @method
     *
     * Template function called after a pivot store record was created.
     * You can use this to populate the record with your data.
     *
     * @param {Ext.data.Model} record 
     * @param {Ext.pivot.axis.Item} item 
     */
    onBuildRecord: Ext.emptyFn,
    
    /**
     * @method
     *
     * Template function called before building grand total records.
     * Use it to add additional grand totals to the pivot grid.
     * You have to push objects into the totals array with properties for each matrix.model fields.
     * For each object that you add a new record will be added to the pivot store
     * and will be styled as a grand total.
     *
     * @param {Array} totals 
     */
    onBuildTotals: Ext.emptyFn,
    
    /**
     * @method
     *
     * Template function called after the pivot store was created.
     *
     * @param {Ext.data.ArrayStore} store 
     */
    onBuildStore: Ext.emptyFn,
    
    /**
     * This function dynamically builds the model of the pivot records.
     *
     * @private
     */
    buildModelAndColumns: function() {
        var me = this;
 
        me.model = [
            { name: 'id', type: 'string' },
            { name: 'isRowGroupHeader', type: 'boolean', defaultValue: false },
            { name: 'isRowGroupTotal', type: 'boolean', defaultValue: false },
            { name: 'isRowGrandTotal', type: 'boolean', defaultValue: false },
            { name: 'leftAxisKey', type: 'string', defaultValue: null }
        ];
 
        me.internalCounter = 0;
        me.columns = [];
 
        if (me.viewLayoutType === 'compact') {
            me.generateCompactLeftAxis();
        }
        else {
            me.leftAxis.dimensions.each(me.parseLeftAxisDimension, me);
        }
 
        if (me.colGrandTotalsPosition === 'first') {
            me.columns.push(me.parseAggregateForColumn(null, {
                text: me.textGrandTotalTpl,
                grandTotal: true
            }));
        }
 
        Ext.Array.each(me.topAxis.getTree(), me.parseTopAxisItem, me);
 
        if (me.colGrandTotalsPosition === 'last') {
            me.columns.push(me.parseAggregateForColumn(null, {
                text: me.textGrandTotalTpl,
                grandTotal: true
            }));
        }
 
        // call the hook functions
        if (Ext.isFunction(me.onBuildModel)) {
            me.onBuildModel(me.model);
        }
 
        me.fireEvent('modelbuilt', me, me.model);
 
        if (Ext.isFunction(me.onBuildColumns)) {
            me.onBuildColumns(me.columns);
        }
 
        me.fireEvent('columnsbuilt', me, me.columns);
    },
 
    getDefaultFieldInfo: function(config) {
        return Ext.apply({
            isColGroupTotal: false,
            isColGrandTotal: false,
            leftAxisColumn: false,
            topAxisColumn: false,
            topAxisKey: null
        }, config);
    },
 
    /**
     * @private
     */
    parseLeftAxisDimension: function(dimension) {
        var me = this,
            id = dimension.getId();
 
        me.model.push({
            name: id,
            type: 'auto'
        });
        me.columns.push(Ext.merge({
            dataIndex: id,
            text: dimension.header,
            dimension: dimension,
            leftAxis: true
        }, dimension.column));
        me.modelInfo[id] = me.getDefaultFieldInfo({
            leftAxisColumn: true
        });
    },
    
    /**
     * @private
     */
    generateCompactLeftAxis: function() {
        var me = this;
 
        me.model.push({
            name: me.compactViewKey,
            type: 'auto'
        });
        me.columns.push({
            dataIndex: me.compactViewKey,
            text: me.textRowLabels,
            leftAxis: true,
            width: me.compactViewColumnWidth
        });
        me.modelInfo[me.compactViewKey] = me.getDefaultFieldInfo({
            leftAxisColumn: true
        });
    },
    
    /**
     * @private
     */
    parseTopAxisItem: function(item) {
        var me = this,
            columns = [],
            retColumns = [],
            o1, o2,
            len, i, ret;
        
        if (!item.children) {
            columns = me.parseAggregateForColumn(item, null);
 
            if (item.level === 0) {
                me.columns.push(columns);
            }
            else {
                // we reached the deepest level so we can add to the model now
                return columns;
            }
        }
        else {
            if (me.colSubTotalsPosition === 'first') {
                o2 = me.addColSummary(item);
 
                if (o2) {
                    retColumns.push(o2);
                }
            }
            
            // this part has to be done no matter if the column is added to the grid or not
            // the dataIndex is generated incrementally
            len = item.children.length;
 
            for (= 0; i < len; i++) {
                ret = me.parseTopAxisItem(item.children[i]);
 
                if (Ext.isArray(ret)) {
                    Ext.Array.insert(columns, columns.length, ret);
                }
                else {
                    columns.push(ret);
                }
            }
 
            o1 = {
                text: item.name,
                group: item,
                columns: columns,
                key: item.key,
                xexpandable: true,
                xgrouped: true
            };
 
            if (item.level === 0) {
                me.columns.push(o1);
            }
 
            retColumns.push(o1);
 
            if (me.colSubTotalsPosition === 'last') {
                o2 = me.addColSummary(item);
 
                if (o2) {
                    retColumns.push(o2);
                }
            }
 
            if (me.colSubTotalsPosition === 'none') {
                o2 = me.addColSummary(item);
 
                if (o2) {
                    retColumns.push(o2);
                }
            }
 
            return retColumns;
        }
    },
    
    /**
     * @private
     */
    addColSummary: function(item) {
        var me = this,
            o2;
            
        // add subtotal columns if required
        o2 = me.parseAggregateForColumn(item, {
            text: item.expanded ? item.getTextTotal() : item.name,
            group: item,
            subTotal: true
        });
 
        if (item.level === 0) {
            me.columns.push(o2);
        }
 
        Ext.apply(o2, {
            key: item.key,
            xexpandable: true,
            xgrouped: true
        });
 
        return o2;
    },
    
    /**
     * @private
     */
    parseAggregateForColumn: function(item, config) {
        var me = this,
            columns = [],
            column = {},
            dimensions = me.aggregate.getRange(),
            length = dimensions.length,
            i, agg;
 
        for (= 0; i < length; i++) {
            agg = dimensions[i];
 
            me.internalCounter++;
            me.model.push({
                name: 'c' + me.internalCounter,
                type: 'auto',
                defaultValue: undefined,
                useNull: true,
                col: item ? item.key : me.grandTotalKey,
                agg: agg.getId()
            });
 
            columns.push(Ext.merge({
                dataIndex: 'c' + me.internalCounter,
                text: agg.header,
                topAxis: true,   // generated based on the top axis
                subTotal: (config ? config.subTotal === true : false),
                grandTotal: (config ? config.grandTotal === true : false),
                dimension: agg
            }, agg.column));
 
            me.modelInfo['c' + me.internalCounter] = me.getDefaultFieldInfo({
                isColGroupTotal: (config ? config.subTotal === true : false),
                isColGrandTotal: (config ? config.grandTotal === true : false),
                topAxisColumn: true,
                topAxisKey: item ? item.key : me.grandTotalKey
            });
        }
 
        if (columns.length === 0 && me.aggregate.getCount() === 0) {
            me.internalCounter++;
            column = Ext.apply({
                text: item ? item.name : '',
                dataIndex: 'c' + me.internalCounter
            }, config || {});
        }
        else if (columns.length === 1) {
            column = Ext.applyIf({
                text: item ? item.name : ''
            }, columns[0]);
            Ext.apply(column, config || {});
 
            // if there is only one aggregate available then don't show the grand total text
            // use the aggregate header instead.
            if (config && config.grandTotal && me.aggregate.getCount() === 1) {
                column.text = me.aggregate.getAt(0).header || config.text;
            }
        }
        else {
            column = Ext.apply({
                text: item ? item.name : '',
                columns: columns
            }, config || {});
        }
 
        return column;
    },
    
    /**
     * @private
     */
    buildPivotStore: function() {
        var me = this;
        
        if (Ext.isFunction(me.pivotStore.model.setFields)) {
            me.pivotStore.model.setFields(me.model);
        }
        else {
            // ExtJS 5 has no "setFields" anymore so fallback to "replaceFields"
            me.pivotStore.model.replaceFields(me.model, true);
        }
 
        me.pivotStore.removeAll(true);
 
        Ext.Array.each(me.leftAxis.getTree(), me.addRecordToPivotStore, me);
        me.addGrandTotalsToPivotStore();
    },
    
    /**
     * @private
     */
    addGrandTotalsToPivotStore: function() {
        var me = this,
            totals = [],
            len, i, t;
            
        // first of all add the grand total
        totals.push({
            title: me.textGrandTotalTpl,
            values: me.preparePivotStoreRecordData({
                key: me.grandTotalKey
            }, {
                isRowGrandTotal: true
            })
        });
        
        // additional grand totals can be added. collect these using events or 
        if (Ext.isFunction(me.onBuildTotals)) {
            me.onBuildTotals(totals);
        }
 
        me.fireEvent('buildtotals', me, totals);
        
        // add records to the pivot store for each grand total
        len = totals.length;
 
        for (= 0; i < len; i++) {
            t = totals[i];
 
            if (Ext.isObject(t) && Ext.isObject(t.values)) {
                Ext.applyIf(t.values, {
                    isRowGrandTotal: true
                });
                me.totals.push({
                    title: t.title || '',
                    record: me.pivotStore.add(t.values)[0]
                });
            }
        }
    },
    
    /**
     * @private
     */
    addRecordToPivotStore: function(item) {
        var me = this,
            record, dataIndex;
 
        if (!item.children) {
            // we are on the deepest level so it's time to build the record and add it to the store
            record = me.pivotStore.add(me.preparePivotStoreRecordData(item))[0];
            item.record = record;
 
            // this should be moved into the function "preparePivotStoreRecordData"
            if (Ext.isFunction(me.onBuildRecord)) {
                me.onBuildRecord(record, item);
            }
 
            me.fireEvent('recordbuilt', me, record, item);
        }
        else {
 
            // This group is expandable so let's generate records for the following use cases
            // - expanded group
            // - collapsed group
            // - footer for an expanded group that has rowSubTotalsPosition = last.
            // We define all these records on the group item so that we can update them as well
            // when we have an editable pivot. Without doing this we can't mark dirty records
            // in the pivot grid cells
            item.records = {};
            dataIndex = (me.viewLayoutType === 'compact' ? me.compactViewKey : item.dimensionId);
 
            // a collapsed group will always be the same
            item.records.collapsed = me.pivotStore.add(me.preparePivotStoreRecordData(item, {
                isRowGroupHeader: true,
                isRowGroupTotal: true
            }))[0];
 
            if (me.rowSubTotalsPosition === 'first' && me.viewLayoutType !== 'tabular') {
                item.records.expanded = me.pivotStore.add(me.preparePivotStoreRecordData(item, {
                    isRowGroupHeader: true
                }))[0];
            }
            else {
                record = {};
                record = me.preparePivotStoreRecordData(item, {
                    isRowGroupHeader: true
                });
                record[dataIndex] = item.name;
 
                item.records.expanded = me.pivotStore.add(record)[0];
 
                if (me.rowSubTotalsPosition === 'last' || me.viewLayoutType === 'tabular') {
                    record = me.preparePivotStoreRecordData(item, {
                        isRowGroupTotal: true
                    });
                    record[dataIndex] = item.getTextTotal();
                    item.records.footer = me.pivotStore.add(record)[0];
                }
            }
 
            Ext.Array.each(item.children, me.addRecordToPivotStore, me);
        }
    },
    
    /**
     * Create an object using the pivot model and data of an axis item.
     * This object is used to create a record in the pivot store.
     *
     * @private
     */
    preparePivotStoreRecordData: function(group, values) {
        var me = this,
            data = {},
            len = me.model.length,
            i, field, result;
 
        if (group) {
            if (group.dimensionId) {
                data[group.dimensionId] = group.name;
            }
 
            data.leftAxisKey = group.key;
 
            for (= 0; i < len; i++) {
                field = me.model[i];
 
                if (field.col && field.agg) {
                    // result = me.results.get(group.key, field.col);
                    // optimize this call
                    result = me.results.items.map[group.key + '/' + field.col];
                    // data[field.name] = result.getValue(field.agg);
                    // optimize this call
                    data[field.name] = result ? result.values[field.agg] : null;
                }
            }
 
            if (me.viewLayoutType === 'compact') {
                data[me.compactViewKey] = group.name;
            }
        }
        
        return Ext.applyIf(data, values);
    },
    
    /**
     * Returns the generated model fields
     *
     * @returns {Object[]} Array of config objects used to build the pivot store model fields
     */
    getColumns: function() {
        return this.model;
    },
    
    /**
     * Returns all generated column headers
     *
     * @returns {Object[]} Array of config objects used to build the pivot grid columns
     */
    getColumnHeaders: function() {
        var me = this;
        
        if (!me.model) {
            me.buildModelAndColumns();
        }
 
        return me.columns;
    },
    
    /**
     *    Find out if the specified key belongs to a row group.
     *
     *    Returns FALSE if the key is not found.
     *
     *    Returns 0 if the current key doesn't belong to a group. That means that group children
     * items will always be 0.
     *
     *    If it'a a group then it returns the level number which is always > 0.
     *
     * @param {String} key 
     * @returns {Number/Boolean}
     */
    isGroupRow: function(key) {
        var obj = this.leftAxis.findTreeElement('key', key);
 
        if (!obj) {
            return false;
        }
 
        return (obj.node.children && obj.node.children.length === 0) ? 0 : obj.level;
    },
    
    /**
     *    Find out if the specified key belongs to a col group.
     *
     *    Returns FALSE if the key is not found.
     *
     *    Returns 0 if the current key doesn't belong to a group. That means that group children
     * items will always be 0.
     *
     *    If it'a a group then it returns the level number which is always > 0.
     *
     * @param {String} key 
     * @returns {Number/Boolean}
     */
    isGroupCol: function(key) {
        var obj = this.topAxis.findTreeElement('key', key);
 
        if (!obj) {
            return false;
        }
 
        return (obj.node.children && obj.node.children.length === 0) ? 0 : obj.level;
    },
 
    deprecated: {
        '6.0': {
            properties: {
                /**
                 * @cfg {String} mztype
                 *
                 * @deprecated 6.0 Use {@link #type} instead.
                 */
                mztype: 'type',
 
                /**
                 * @cfg {String} mztypeLeftAxis
                 * @deprecated 6.0 Use {@link #leftAxisType} instead.
                 *
                 * Define the type of left Axis this class uses. Specify here the pivotaxis alias.
                 */
                mztypeLeftAxis: 'leftAxisType',
 
                /**
                 * @cfg {String} mztypeTopAxis
                 * @deprecated 6.0 Use {@link #topAxisType} instead.
                 *
                 * Define the type of top Axis this class uses. Specify here the pivotaxis alias.
                 */
                mztypeTopAxis: 'topAxisType'
            }
        }
    }
});