/**
 * Base class for the grid exporter plugin. It contains common functions for both classic
 * and modern toolkits.
 *
 * This class is extended by the toolkit specific grid plugin.
 *
 * @private
 */
Ext.define('Ext.grid.plugin.BaseExporter', {
    extend: 'Ext.exporter.Plugin',

    /**
     * @event beforedocumentsave
     * Fires on the grid panel before a document is exported and saved.
     * @param {Ext.grid.Panel} grid Reference to the grid panel
     * @param {Object} params Additional parameters sent with this event
     * @param {Object} params.config The config object used in the {@link #saveDocumentAs} method
     * @param {Ext.exporter.Base} params.exporter A reference to the exporter object used to save
     * the document
     */

    /**
     * @event documentsave
     * Fires on the grid panel whenever a document is exported and saved.
     * @param {Ext.grid.Panel} grid Reference to the grid panel
     * @param {Object} params Additional parameters sent with this event
     * @param {Object} params.config The config object used in the {@link #saveDocumentAs} method
     * @param {Ext.exporter.Base} params.exporter A reference to the exporter object used to save
     * the document
     */

    /**
     * @event dataready
     * Fires on the grid panel when the {@link Ext.exporter.data.Table data} is ready.
     * You could adjust styles or data before the document is generated and saved.
     * @param {Ext.grid.Panel} grid Reference to the grid panel
     * @param {Object} params Additional parameters sent with this event
     * @param {Object} params.config The config object used in the {@link #saveDocumentAs} method
     * @param {Ext.exporter.Base} params.exporter A reference to the exporter object used to save
     * the document
     */

    /**
     * Save the export file. This method is added to the grid panel as "saveDocumentAs".
     *
     * Pass in exporter specific configs to the config parameter.
     *
     * @method saveDocumentAs
     * @param {Ext.exporter.Base} config Config object used to initialize the proper exporter
     * @param {String} config.type Type of the exporter as defined in the exporter alias.
     * Default is `excel`.
     * @param {String} [config.title] Title added to the export document
     * @param {String} [config.author] Who exported the document?
     * @param {String} [config.fileName] Name of the exported file, including the extension
     * @param {String} [config.charset] Exported file's charset
     *
     */

    /**
     * Fetch the export data. This method is added to the grid panel as "getDocumentData".
     *
     * Pass in exporter specific configs to the config parameter.
     *
     * @method getDocumentData
     * @param {Ext.exporter.Base} config Config object used to initialize the proper exporter
     * @param {String} [config.type] Type of the exporter as defined in the exporter alias.
     * Default is `excel`.
     * @param {String} [config.title] Title added to the export document
     * @param {String} [config.author] Who exported the document?
     * @return {String}
     *
     */

    /**
     * @method prepareData
     * @inheritdoc Ext.exporter.Plugin#method!prepareData
     */
    prepareData: function(config) {
        var me = this,
            store = me.cmp.getStore(),
            table = new Ext.exporter.data.Table(),
            result, columns;

        result = me.getColumnHeaders(config, me.getGridColumns());
        table.setColumns(result.headers);

        if (!store || (store && store.destroyed)) {
            return table;
        }

        //<debug>
        if (store && store.isBufferedStore) {
            Ext.raise("BufferedStore can't be exported because it doesn't have all data available");
        }
        //</debug>

        columns = me.prepareDataIndexColumns(config, result.dataIndexes);

        if (store.isTreeStore) {
            me.extractNodeData(config, table, columns, store.getRoot());
        }
        else {
            if (config && config.includeGroups && store.isGrouped()) {
                me.extractData(config, table, columns, store.getGroups());
                me.extractSummaryRow(config, table, columns, store);
            }
            else {
                me.extractRows(config, table, columns, store);
            }
        }

        return table;
    },

    /**
     *
     * @param config
     * @param columns
     * @return {Object}
     * @private
     */
    getColumnHeaders: function(config, columns) {
        var cols = [],
            dataIndexes = [],
            len = columns.length,
            i, result;

        for (i = 0; i < len; i++) {
            result = this.getColumnHeader(config, columns[i]);

            if (result) {
                cols.push(result.header);
                Ext.Array.insert(dataIndexes, dataIndexes.length, result.dataIndexes);
            }
        }

        return {
            headers: cols,
            dataIndexes: dataIndexes
        };
    },

    /**
     * Fetch grid column headers that will be processed
     *
     * @return {Ext.grid.column.Column[]}
     * @private
     */
    getGridColumns: function() {
        return [];
    },

    /**
     * Check if the column should be exported or not. Columns that are hidden or have
     * ignoreExport = true are ignored.
     *
     * Returns an object that has 2 keys:
     * - header
     * - dataIndexes
     *
     * @param {Object} config
     * @param {Ext.grid.column.Column} column
     * @return {Object}
     * @private
     */
    getColumnHeader: Ext.emptyFn,

    prepareDataIndexColumns: function(config, dataIndexes) {
        var len = dataIndexes.length,
            columns = [],
            i;

        for (i = 0; i < len; i++) {
            columns.push(this.prepareDataIndexColumn(config, dataIndexes[i]));
        }

        return columns;
    },

    prepareDataIndexColumn: function(config, column) {
        return {
            column: column,
            fn: Ext.emptyFn
        };
    },

    /**
     * Fetch data from store groups.
     *
     * @param {Object} config
     * @param {Ext.exporter.data.Group} group
     * @param {Ext.grid.column.Column[]} columns
     * @param {Ext.util.GroupCollection} collection
     * @private
     */
    extractData: function(config, group, columns, collection) {
        var i, len, children, storeGroup, tableGroup;

        if (!collection) {
            return;
        }

        len = collection.getCount();

        for (i = 0; i < len; i++) {
            storeGroup = collection.getAt(i);
            children = storeGroup.getGroups();

            tableGroup = group.addGroup({
                text: storeGroup.getGroupKey()
            });

            if (children) {
                this.extractData(config, tableGroup, columns, children);
            }
            else {
                this.extractRows(config, tableGroup, columns, storeGroup);
            }
        }
    },

    /**
     * Fetch data from TreeStore node.
     *
     * @param {Object} config
     * @param {Ext.exporter.data.Group} group
     * @param {Ext.grid.column.Column[]} columns
     * @param {Ext.data.NodeInterface} node
     * @private
     */
    extractNodeData: function(config, group, columns, node) {
        var me = this,
            store = me.cmp.getStore(),
            lenCols = columns.length,
            i, j, record, row, cell, column, children, len;

        if (node && node.hasChildNodes()) {
            children = node.childNodes;
            len = children.length;

            for (i = 0; i < len; i++) {
                record = children[i];
                row = {
                    cells: []
                };

                for (j = 0; j < lenCols; j++) {
                    column = columns[j];

                    cell = me.getCell(store, record, column) || {
                        value: null
                    };

                    if (column.column.isTreeColumn && cell) {
                        cell.style = Ext.merge(cell.style || {}, {
                            alignment: {
                                indent: record.getDepth() - 1
                            }
                        });
                    }

                    row.cells.push(cell);
                }

                group.addRow(row);

                if (record.hasChildNodes()) {
                    me.extractNodeData(config, group, columns, record);
                }
            }
        }
    },

    /**
     *
     * @param {Object} config
     * @param {Ext.exporter.data.Group} group
     * @param {Ext.grid.column.Column[]} columns
     * @param {Ext.data.Store/Ext.util.Group} collection
     * @private
     */
    extractRows: function(config, group, columns, collection) {
        var cmp = this.cmp,
            store = cmp.getStore(),
            len = collection.getCount(),
            lenCols = columns.length,
            rows = [],
            i, j, record, row, cell;

        for (i = 0; i < len; i++) {
            record = collection.getAt(i);
            row = {
                cells: []
            };

            for (j = 0; j < lenCols; j++) {
                cell = this.getCell(store, record, columns[j]);
                row.cells.push(cell || {
                    value: null
                });
            }

            rows.push(row);
        }

        group.setRows(rows);
        this.extractSummaryRow(config, group, columns, collection);
    },

    extractSummaryRow: function(config, group, columns, collection) {
        var lenCols = columns.length,
            i, record, row, cell;

        if (config.includeSummary) {
            row = {
                cells: []
            };

            record = this.getSummaryRecord(collection, columns);

            for (i = 0; i < lenCols; i++) {
                cell = this.getSummaryCell(collection, record, columns[i]);
                row.cells.push(cell || {
                    value: null
                });
            }

            group.setSummaries(row);
        }
    },

    /**
     *
     * @param {Ext.data.Store} store
     * @param {Ext.data.Model} record
     * @param {Object} colDef
     * @param {String} colDef.dataIndex
     * @param {Ext.grid.column.Column} colDef.column
     * @param {Function} colDef.fn
     * @param {Function/String} colDef.summaryType
     * @param {Function} colDef.summaryFn
     * @return {Ext.exporter.data.Cell} A cell config object
     * @private
     */
    getCell: Ext.emptyFn,
    /**
     *
     * @param {Ext.data.Store/Ext.util.Group} collection
     * @param {Object} colDef
     * @param {Ext.grid.column.Column} colDef.column
     * @param {Function} colDef.fn
     * @return {Ext.exporter.data.Cell} A cell config object
     * @private
     */
    getSummaryCell: Ext.emptyFn,

    getSummaryRecord: function(collection, columns) {
        var len = columns.length,
            summaryRecord = collection.getSummaryRecord(),
            record = new Ext.data.Model({
                id: 'summary-record'
            }),
            i, colDef, records;

        record.beginEdit();
        record.set(summaryRecord.getData());

        for (i = 0; i < len; i++) {
            colDef = columns[i];

            /* eslint-disable max-len */
            if (colDef.summary) {
                records = collection.isStore ? collection.data.items.slice() : collection.items.slice();
                record.set(colDef.summaryIndex, colDef.summary.calculate(records, colDef.summaryIndex, 'data', 0, records.length));
            }
            else if (colDef.summaryType) {
                record.set(colDef.summaryIndex, this.getSummary(collection, colDef.summaryType, colDef.summaryIndex));
            }
            /* eslint-enable max-len */
        }

        record.endEdit();
        record.commit(true);

        return record;
    },

    /**
     * Get the summary data for a field.
     * @param {Ext.data.Store/Ext.util.Group} item The store or group to get the data from
     * @param {String/Function} type The type of aggregation. If a function is specified it will
     * be passed to the stores aggregate function.
     * @param {String} field The field to aggregate on
     * @return {Number/String/Object} See the return type for the store functions.
     * if the group parameter is `true` An object is returned with a property named for each group
     * whose value is the summary value.
     * @private
     */
    getSummary: function(item, type, field) {
        var isStore = item.isStore;

        if (type) {
            if (Ext.isFunction(type)) {
                if (isStore) {
                    return item.aggregate(type, null, false, [field]);
                }
                else {
                    return item.aggregate(field, type);
                }
            }

            switch (type) {
                case 'count':
                    return item.count();

                case 'min':
                    return item.min(field);

                case 'max':
                    return item.max(field);

                case 'sum':
                    return item.sum(field);

                case 'average':
                    return item.average(field);

                default:
                    return null;
            }
        }
    }
});
