/**
 * Base implementation of a pivot axis. You may customize multiple dimensions
 * on an axis. Basically this class stores all labels that were generated
 * for the configured dimensions.
 *
 * Example:
 *
 *      leftAxis: [{
 *          dataIndex:  'person',
 *          header:     'Person',
 *          sortable:   false
 *      },{
 *          dataIndex:  'country',
 *          header:     'Country',
 *          direction:  'DESC'
 *      }]
 *
 */
Ext.define('Ext.pivot.axis.Base', {
    alternateClassName: [
        'Mz.aggregate.axis.Abstract'
    ],

    alias: 'pivotaxis.base',

    mixins: [
        'Ext.mixin.Factoryable'
    ],

    requires: [
        'Ext.pivot.MixedCollection',
        'Ext.pivot.dimension.Item',
        'Ext.pivot.axis.Item'
    ],

    /**
     * @cfg {Ext.pivot.dimension.Item[]} dimensions All dimensions configured for this axis.
     *
     */
    dimensions: null,

    /**
     * @property {Ext.pivot.matrix.Base} matrix Matrix instance this axis belongs to.
     *
     */
    matrix: null,

    /**
     * @property {Ext.pivot.MixedCollection} items All items generated for this axis are stored
     * in this collection.
     *
     */
    items: null,

    /**
     * When the tree is built for this axis it is stored in this property.
     *
     * @private
     */
    tree: null,

    /**
     * @property {Number} levels No of levels this axis tree has
     * @readonly
     *
     */
    levels: 0,

    /**
     * @property {Boolean} isLeftAxis Internal flag to know which axis is this one
     * @readonly
     */
    isLeftAxis: false,

    constructor: function(config) {
        var me = this;

        if (!config || !config.matrix) {
            //<debug>
            Ext.log('Wrong initialization of the axis!');
            //</debug>

            return;
        }

        me.isLeftAxis = config.isLeftAxis || me.isLeftAxis;
        me.matrix = config.matrix;
        me.tree = [];

        // init dimensions
        me.dimensions = new Ext.pivot.MixedCollection();

        me.dimensions.getKey = function(item) {
            return item.getId();
        };

        me.items = new Ext.pivot.MixedCollection();

        me.items.getKey = function(item) {
            return item.key;
        };

        Ext.Array.each(Ext.Array.from(config.dimensions || []), me.addDimension, me);
    },

    destroy: function() {
        var me = this;

        Ext.destroyMembers(me, 'dimensions', 'items', 'tree');
        me.matrix = me.dimensions = me.items = me.tree = null;
    },

    /**
     * Create an {@link Ext.pivot.dimension.Item} object with the specified config and add it to the
     * internal collection of dimensions.
     *
     * @param {Object} config Config object for the {@link Ext.pivot.dimension.Item} that is created
     */
    addDimension: function(config) {
        var item = config;

        if (!config) {
            return;
        }

        if (!config.isInstance) {
            item = new Ext.pivot.dimension.Item(config);
        }

        item.matrix = this.matrix;
        this.dimensions.add(item);
    },

    /**
     * Add the specified item to the internal collection of items.
     *
     * @param {Object} item Config object for the {@link Ext.pivot.axis.Item} that is added
     */
    addItem: function(item) {
        var me = this;

        if (!Ext.isObject(item) || Ext.isEmpty(item.key) || Ext.isEmpty(item.value) ||
            Ext.isEmpty(item.dimensionId)) {
            return false;
        }

        item.key = String(item.key);
        item.dimension = me.dimensions.getByKey(item.dimensionId);
        item.name = item.name ||
                    Ext.callback(item.dimension.labelRenderer,
                                 item.dimension.scope || 'self.controller', [item.value], 0,
                                 me.matrix.cmp) ||
                    item.value;

        item.dimension.addValue(item.value, item.name);
        item.axis = me;

        if (!me.items.map[item.key] && item.dimension) {
            me.items.add(new Ext.pivot.axis.Item(item));

            return true;
        }

        return false;
    },

    /**
     * Clear all items and the tree.
     *
     */
    clear: function() {
        this.items.clear();
        this.tree = null;
    },

    /**
     * This function parses the internal collection of items and builds a tree.
     * This tree is used by the Matrix class to generate the pivot store and column headers.
     *
     */
    getTree: function() {
        if (!this.tree) {
            this.buildTree();
        }

        return this.tree;
    },

    /**
     * Expand all groups
     */
    expandAll: function() {
        var me = this,
            items = me.getTree(),
            len = items.length,
            i;

        for (i = 0; i < len; i++) {
            items[i].expandCollapseChildrenTree(true);
        }

        // we fire a single groupexpand event without any item
        if (len > 0) {
            me.matrix.fireEvent('groupexpand', me.matrix, (me.isLeftAxis ? 'row' : 'col'), null);
        }
    },

    /**
     * Collapse all groups
     */
    collapseAll: function() {
        var me = this,
            items = me.getTree(),
            len = items.length,
            i;

        for (i = 0; i < len; i++) {
            items[i].expandCollapseChildrenTree(false);
        }

        // we fire a single groupcollapse event without any item
        if (len > 0) {
            me.matrix.fireEvent('groupcollapse', me.matrix, (me.isLeftAxis ? 'row' : 'col'), null);
        }
    },

    /**
     * Find the first element in the tree that matches the criteria (attribute = value).
     *
     * It returns an object with the tree element and depth level.
     *
     * @return {Object}
     */
    findTreeElement: function(attribute, value) {
        var items = this.items,
            len = items.getCount(),
            found = false,
            i, item;

        for (i = 0; i < len; i++) {
            item = items.items[i];

            // eslint-disable-next-line max-len
            if (Ext.isDate(value) ? Ext.Date.isEqual(item[attribute], value) : item[attribute] === value) {
                found = true;
                break;
            }
        }

        return found ? { level: item.level, node: item } : null;
    },

    /**
     * This function builds the internal tree after all records were processed
     *
     * @private
     */
    buildTree: function() {
        var me = this,
            items = me.dimensions.items,
            len = items.length,
            i, item, keys, parentKey, el;

        for (i = 0; i < len; i++) {
            // sort unique values for each dimension on this axis
            items[i].sortValues();
        }

        me.tree = [];

        // transform the flat items collection into a tree
        items = me.items.items;
        len = items.length;

        for (i = 0; i < len; i++) {
            item = items[i];
            keys = String(item.key).split(me.matrix.keysSeparator);
            keys = Ext.Array.slice(keys, 0, keys.length - 1);
            parentKey = keys.join(me.matrix.keysSeparator);

            el = me.items.map[parentKey];

            if (el) {
                item.level = el.level + 1;
                item.data = Ext.clone(el.data || {});
                el.children = el.children || [];
                el.children.push(item);
            }
            else {
                item.level = 0;
                item.data = {};
                me.tree.push(item);
            }

            item.data[item.dimension.getId()] = item.name;
            // item.data[item.dimension.getId()] = item.value;
            me.levels = Math.max(me.levels, item.level);
        }

        me.sortTree();
    },

    rebuildTree: function() {
        var items = this.items.items,
            len = items.length,
            i;

        this.tree = null;

        for (i = 0; i < len; i++) {
            items[i].children = null;
        }

        this.buildTree();
    },

    /**
     * Sort the tree using the sorters defined on the axis dimensions
     *
     * @private
     */
    sortTree: function() {
        var tree = arguments[0] || this.tree,
            len = tree.length,
            dimension, i, item;

        if (tree.length > 0) {
            dimension = tree[0].dimension;
        }

        if (dimension) {
            // let's sort this array
            Ext.Array.sort(tree, dimension.sortFn);
        }

        for (i = 0; i < len; i++) {
            item = tree[i];

            if (item.children) {
                this.sortTree(item.children);
            }
        }
    },

    /**
     * Sort the tree by the specified field and direction.
     *
     * If the field is one of the axis dimension then sort by that otherwise go to the records
     * and sort them by that field.
     *
     * @param field
     * @param direction
     *
     * @returns {Boolean}
     * @private
     */
    sortTreeByField: function(field, direction) {
        var me = this,
            sorted = false,
            dimension, len, i;

        if (field === me.matrix.compactViewKey) {
            // in compact view we need to sort by all axis dimensions
            sorted = me.sortTreeByDimension(me.tree, me.dimensions.items, direction);
            len = me.dimensions.items.length;

            for (i = 0; i < len; i++) {
                me.dimensions.items[i].direction = direction;
            }
        }
        else {
            direction = direction || 'ASC';
            dimension = me.dimensions.getByKey(field);

            if (dimension) {
                // we need to sort the tree level where this dimension exists
                sorted = me.sortTreeByDimension(me.tree, dimension, direction);
                dimension.direction = direction;
            }
            else {
                // the field is not a dimension defined on the axis, so it's probably a generated
                // field on the pivot record which means we need to sort by calculated values
                sorted = me.sortTreeByRecords(me.tree, field, direction);
            }
        }

        return sorted;
    },

    /**
     * Sort tree by a specified dimension. The dimension's sorter function is used for sorting.
     *
     * @param tree
     * @param dimension
     * @param direction
     * @returns {Boolean}
     *
     * @private
     */
    sortTreeByDimension: function(tree, dimension, direction) {
        var sorted = false,
            dimensions = Ext.Array.from(dimension),
            aDimension, len, i, temp;

        tree = tree || [];
        len = tree.length;

        if (len > 0) {
            aDimension = tree[0].dimension;
        }

        if (Ext.Array.indexOf(dimensions, aDimension) >= 0) {
            if (aDimension.sortable) {
                // we have to sort this tree items by the dimension sortFn
                temp = aDimension.direction;
                aDimension.direction = direction;
                Ext.Array.sort(tree, aDimension.sortFn);
                aDimension.direction = temp;
                // we do not change the dimension direction now since we didn't finish yet
            }

            sorted = aDimension.sortable;
        }

        // the dimension we want to sort may be on leaves
        // in compact view mode we need to sort everything
        for (i = 0; i < len; i++) {
            sorted = this.sortTreeByDimension(tree[i].children, dimension, direction) || sorted;
        }

        // ready now so exit
        return sorted;
    },

    /**
     * Sort tree by values on a generated field on the pivot model.
     *
     * @param tree
     * @param field
     * @param direction
     * @returns {boolean}
     *
     * @private
     */
    sortTreeByRecords: function(tree, field, direction) {
        var i, len;

        tree = tree || [];
        len = tree.length;

        if (len <= 0) {
            return false;
        }

        if (tree[0].record) {
            this.sortTreeRecords(tree, field, direction);
        }
        else {
            this.sortTreeLeaves(tree, field, direction);
        }

        for (i = 0; i < len; i++) {
            this.sortTreeByRecords(tree[i].children, field, direction);
        }

        return true;
    },

    /**
     * Sort the records array of each item in the tree
     *
     * @param tree
     * @param field
     * @param direction
     *
     * @private
     */
    sortTreeRecords: function(tree, field, direction) {
        var sortFn = this.matrix.naturalSort;

        direction = direction || 'ASC';

        // let's sort the records of this item
        Ext.Array.sort(tree || [], function(a, b) {
            var result,
                o1 = a.record,
                o2 = b.record;

            if (!(o1 && o1.isModel && o2 && o2.isModel)) {
                return 0;
            }

            result = sortFn(o1.get(field) || '', o2.get(field) || '');

            if (result < 0 && direction === 'DESC') {
                return 1;
            }

            if (result > 0 && direction === 'DESC') {
                return -1;
            }

            return result;
        });
    },

    /**
     * Sort tree leaves by their group summary.
     *
     * @param tree
     * @param field
     * @param direction
     *
     * @returns {Boolean}
     *
     * @private
     */
    sortTreeLeaves: function(tree, field, direction) {
        var sortFn = this.matrix.naturalSort,
            results = this.matrix.results,
            matrixModel = this.matrix.model,
            idx = Ext.Array.indexOf(Ext.Array.pluck(matrixModel, 'name'), field),
            col, agg;

        if (idx < 0) {
            return false;
        }

        col = matrixModel[idx].col;
        agg = matrixModel[idx].agg;

        direction = direction || 'ASC';

        // let's sort the records of this item
        Ext.Array.sort(tree || [], function(a, b) {
            var result,
                o1, o2;

            o1 = results.get(a.key, col);

            if (o1) {
                o1 = o1.getValue(agg);
            }
            else {
                o1 = 0;
            }

            o2 = results.get(b.key, col);

            if (o2) {
                o2 = o2.getValue(agg);
            }
            else {
                o2 = 0;
            }

            result = sortFn(o1, o2);

            if (result < 0 && direction === 'DESC') {
                return 1;
            }

            if (result > 0 && direction === 'DESC') {
                return -1;
            }

            return result;
        });
    }
});
