/**
 * @class Ext.chart.series.Pie3D
 * @extends Ext.chart.series.Polar
 *
 * Creates a 3D Pie Chart.
 *
 * **Note:** Labels, legends, and lines are not currently available when using the
 * 3D Pie chart series.
 *
 *     @example
 *     Ext.create({
 *        xtype: 'polar', 
 *        renderTo: document.body,
 *        width: 600,
 *        height: 400,
 *        theme: 'green',
 *        interactions: 'rotate',
 *        store: {
 *            fields: ['data3'],
 *            data: [{
 *                'data3': 14
 *            }, {
 *                'data3': 16
 *            }, {
 *                'data3': 14
 *            }, {
 *                'data3': 6
 *            }, {
 *                'data3': 36
 *            }]
 *        },
 *        series: {
 *            type: 'pie3d',
 *            angleField: 'data3',
 *            donut: 30
 *        }
 *     });
 */
Ext.define('Ext.chart.series.Pie3D', {
    extend: 'Ext.chart.series.Polar',
 
    requires: [
        'Ext.chart.series.sprite.Pie3DPart',
        'Ext.draw.PathUtil'
    ],
 
    type: 'pie3d',
    seriesType: 'pie3d',
    alias: 'series.pie3d',
    is3D: true,
 
    config: {
        rect: [0, 0, 0, 0],
        thickness: 35,
        distortion: 0.5,
 
        /**
         * @cfg {String} angleField (required)
         * The store record field name to be used for the pie angles.
         * The values bound to this field name must be positive real numbers.
         */
 
        /**
         * @private
         * @cfg {String} radiusField
         * Not supported.
         */
 
        /**
         * @cfg {Number} donut Specifies the radius of the donut hole, as a percentage
         * of the chart's radius.
         * Defaults to 0 (no donut hole).
         */
        donut: 0,
 
        /**
         * @cfg {Array} hidden Determines which pie slices are hidden.
         */
        hidden: [], // Populated by the coordinateX method.
 
        /**
         * @cfg {Object} highlightCfg Default {@link #highlight} config for the 3D pie series.
         * Slides highlighted pie sector outward.
         */
        highlightCfg: {
            margin: 20
        },
 
        /**
         * @cfg {Number} [rotation=0] The starting angle of the pie slices.
         */
 
        /**
         * @private
         * @cfg {Boolean/Object} [shadow=false]
         */
        shadow: false
    },
 
    // Subtract 90 degrees from rotation, so that `rotation` config's default
    // zero value makes first pie sector start at noon, rather than 3 o'clock.
    rotationOffset: -Math.PI / 2,
 
    setField: function(value) {
        return this.setXField(value);
    },
 
    getField: function() {
        return this.getXField();
    },
 
    updateRotation: function(rotation) {
        var attributes = { baseRotation: rotation + this.rotationOffset };
 
        this.forEachSprite(function(sprite) {
            sprite.setAttributes(attributes);
        });
    },
 
    updateColors: function(colors) {
        var chart;
 
        this.setSubStyle({ baseColor: colors });
 
        if (!this.isConfiguring) {
            chart = this.getChart();
 
            if (chart) {
                chart.refreshLegendStore();
            }
        }
    },
 
    applyShadow: function(shadow) {
        if (shadow === true) {
            shadow = {
                shadowColor: 'rgba(0,0,0,0.8)',
                shadowBlur: 30
            };
        }
        else if (!Ext.isObject(shadow)) {
            shadow = {
                shadowColor: Ext.util.Color.RGBA_NONE
            };
        }
 
        return shadow;
    },
 
    updateShadow: function(shadow) {
        var me = this,
            sprites = me.getSprites(),
            spritesPerSlice = me.spritesPerSlice,
            ln = sprites && sprites.length,
            i, sprite;
 
        for (= 1; i < ln; i += spritesPerSlice) {
            sprite = sprites[i];
 
            if (sprite.attr.part === 'bottom') {
                sprite.setAttributes(shadow);
            }
        }
    },
 
    // This is a temporary solution until the Series.getStyleByIndex is fixed
    // to give user styles the priority over theme ones. Also, for sprites of
    // this particular series, the fillStyle shouldn't be set directly. Instead,
    // the 'baseColor' attribute should be set, from which the stops of the
    // gradient (used for fillStyle) will be calculated. Themes can't handle
    // situations like that properly.
    getStyleByIndex: function(i) {
        var indexStyle = this.callParent([i]),
            style = this.getStyle(),
            // 'fill' and 'color' are 'fillStyle' aliases
            // (see Ext.draw.sprite.Sprite.inheritableStatics.def.aliases)
            fillStyle = indexStyle.fillStyle || indexStyle.fill || indexStyle.color,
            strokeStyle = style.strokeStyle || style.stroke;
 
        if (fillStyle) {
            indexStyle.baseColor = fillStyle;
            delete indexStyle.fillStyle;
            delete indexStyle.fill;
            delete indexStyle.color;
        }
 
        if (strokeStyle) {
            indexStyle.strokeStyle = strokeStyle;
        }
 
        return indexStyle;
    },
 
    doUpdateStyles: function() {
        var me = this,
            sprites = me.getSprites(),
            spritesPerSlice = me.spritesPerSlice,
            ln = sprites && sprites.length,
            i = 0,
            j = 0,
            k,
            style;
 
        for (; i < ln; i += spritesPerSlice, j++) {
            style = me.getStyleByIndex(j);
 
            for (= 0; k < spritesPerSlice; k++) {
                sprites[+ k].setAttributes(style);
            }
        }
    },
 
    coordinateX: function() {
        var me = this,
            store = me.getStore(),
            records = store.getData().items,
            recordCount = records.length,
            xField = me.getXField(),
            animation = me.getAnimation(),
            rotation = me.getRotation(),
            hidden = me.getHidden(),
            sprites = me.getSprites(true),
            spriteCount = sprites.length,
            spritesPerSlice = me.spritesPerSlice,
            center = me.getCenter(),
            offsetX = me.getOffsetX(),
            offsetY = me.getOffsetY(),
            radius = me.getRadius(),
            thickness = me.getThickness(),
            distortion = me.getDistortion(),
            renderer = me.getRenderer(),
            rendererData = me.getRendererData(),
            highlight = me.getHighlight(), // eslint-disable-line no-unused-vars
            lastAngle = 0,
            twoPi = Math.PI * 2,
            // To avoid adjacent start/end part blinking (z-index jitter)
            // when rotating a translucent pie chart.
            delta = 1e-10,
            endAngles = [],
            sum = 0,
            value, unit,
            sprite, style,
            i, j;
 
        for (= 0; i < recordCount; i++) {
            value = Math.abs(+records[i].get(xField)) || 0;
 
            if (!hidden[i]) {
                sum += value;
            }
 
            endAngles[i] = sum;
 
            if (>= hidden.length) {
                hidden[i] = false;
            }
        }
 
        if (sum === 0) {
            return;
        }
 
        // Angular value of 1 in radians.
        unit = 2 * Math.PI / sum;
 
        for (= 0; i < recordCount; i++) {
            endAngles[i] *= unit;
        }
 
        for (= 0; i < recordCount; i++) {
            style = this.getStyleByIndex(i);
 
            for (= 0; j < spritesPerSlice; j++) {
                sprite = sprites[* spritesPerSlice + j];
                sprite.setAnimation(animation);
                sprite.setAttributes({
                    centerX: center[0] + offsetX,
                    centerY: center[1] + offsetY - thickness / 2,
                    endRho: radius,
                    startRho: radius * me.getDonut() / 100,
                    baseRotation: rotation + me.rotationOffset,
                    startAngle: lastAngle,
                    endAngle: endAngles[i] - delta,
                    thickness: thickness,
                    distortion: distortion,
                    globalAlpha: 1
                });
                sprite.setAttributes(style);
                sprite.setConfig({
                    renderer: renderer,
                    rendererData: rendererData,
                    rendererIndex: i
                });
                // if (highlight) {
                //     if (!sprite.modifiers.highlight) {
                //         debugger
                //         sprite.addModifier(highlight, true);
                //     }
                //     // sprite.modifiers.highlight.setConfig(highlight);
                // }
            }
 
            lastAngle = endAngles[i];
        }
 
        for (*= spritesPerSlice; i < spriteCount; i++) {
            sprite = sprites[i];
            sprite.setAnimation(animation);
            sprite.setAttributes({
                startAngle: twoPi,
                endAngle: twoPi,
                globalAlpha: 0,
                baseRotation: rotation + me.rotationOffset
            });
        }
    },
 
    updateHighlight: function(highlight, oldHighlight) {
        this.callParent([highlight, oldHighlight]);
 
        this.forEachSprite(function(sprite) {
            if (highlight) {
                if (sprite.modifiers.highlight) {
                    sprite.modifiers.highlight.setConfig(highlight);
                }
                else {
                    sprite.config.highlight = highlight;
                    sprite.addModifier(highlight, true);
                }
            }
        });
    },
 
    updateLabelData: function() {
        var me = this,
            store = me.getStore(),
            items = store.getData().items,
            sprites = me.getSprites(),
            label = me.getLabel(),
            labelField = label && label.getTemplate().getField(),
            hidden = me.getHidden(),
            spritesPerSlice = me.spritesPerSlice,
            ln, labels, sprite,
            name = 'labels',
            i, // sprite index
            j; // record index
 
        if (sprites.length) {
            if (labelField) {
                labels = [];
 
                for (= 0, ln = items.length; j < ln; j++) {
                    labels.push(items[j].get(labelField));
                }
            }
 
            // Only set labels for the sprites that compose the top lid of the pie.
            for (= 0, j = 0, ln = sprites.length; i < ln; i += spritesPerSlice, j++) {
                sprite = sprites[i];
 
                if (label) {
                    if (!sprite.getMarker(name)) {
                        sprite.bindMarker(name, label);
                    }
 
                    if (labels) {
                        sprite.setAttributes({ label: labels[j] });
                    }
 
                    sprite.putMarker(name, { hidden: hidden[j] }, sprite.attr.attributeId);
                }
                else {
                    sprite.releaseMarker(name);
                }
            }
        }
    },
 
    // The radius here will normally be set by the PolarChart.performLayout,
    // where it's half the width or height (whichever is smaller) of the chart's rect.
    // But for 3D pie series we have to take the thickness of the pie and the
    // distortion into account to calculate the proper radius.
    // The passed value is never used (or derived from) since the radius config
    // is not really meant to be used directly, as it will be reset by the next layout.
    applyRadius: function() {
        var me = this,
            chart = me.getChart(),
            padding = chart.getInnerPadding(),
            rect = chart.getMainRect() || [0, 0, 1, 1],
            width = rect[2] - padding * 2,
            height = rect[3] - padding * 2 - me.getThickness(),
            horizontalRadius = width / 2,
            verticalRadius = horizontalRadius * me.getDistortion(),
            result;
 
        if (verticalRadius > height / 2) {
            result = height / (me.getDistortion() * 2);
        }
        else {
            result = horizontalRadius;
        }
 
        return Math.max(result, 0);
    },
 
    forEachSprite: function(fn) {
        var sprites = this.sprites,
            ln = sprites.length,
            i;
 
        for (= 0; i < ln; i++) {
            fn(sprites[i], Math.floor(/ this.spritesPerSlice));
        }
    },
 
    updateRadius: function(radius) {
        var donut;
 
        // The side effects of the 'getChart' call will result
        // in the 'coordinateX' method call, which we want to have called
        // first, to coordinate the data and create sprites for pie slices,
        // before we set their attributes here.
        // updateChart -> onChartAttached -> processData -> coordinateX
        this.getChart();
 
        donut = this.getDonut();
 
        this.forEachSprite(function(sprite) {
            sprite.setAttributes({
                endRho: radius,
                startRho: radius * donut / 100
            });
        });
    },
 
    updateDonut: function(donut) {
        var radius;
 
        // See 'updateRadius' comments.
        this.getChart();
 
        radius = this.getRadius();
 
        this.forEachSprite(function(sprite) {
            sprite.setAttributes({
                startRho: radius * donut / 100
            });
        });
    },
 
    updateCenter: function(center) {
        var offsetX, offsetY, thickness;
 
        // See 'updateRadius' comments.
        this.getChart();
 
        offsetX = this.getOffsetX();
        offsetY = this.getOffsetY();
        thickness = this.getThickness();
 
        this.forEachSprite(function(sprite) {
            sprite.setAttributes({
                centerX: center[0] + offsetX,
                centerY: center[1] + offsetY - thickness / 2
            });
        });
    },
 
    updateThickness: function(thickness) {
        var center, offsetY;
 
        // See 'updateRadius' comments.
        this.getChart();
 
        // Radius depends on thickness and distortion,
        // this will trigger its recalculation in the applier.
        this.setRadius();
 
        center = this.getCenter();
        offsetY = this.getOffsetY();
 
        this.forEachSprite(function(sprite) {
            sprite.setAttributes({
                thickness: thickness,
                centerY: center[1] + offsetY - thickness / 2
            });
        });
    },
 
    updateDistortion: function(distortion) {
        // See 'updateRadius' comments.
        this.getChart();
 
        // Radius depends on thickness and distortion,
        // this will trigger its recalculation in the applier.
        this.setRadius();
 
        this.forEachSprite(function(sprite) {
            sprite.setAttributes({
                distortion: distortion
            });
        });
    },
 
    updateOffsetX: function(offsetX) {
        var center;
 
        // See 'updateRadius' comments.
        this.getChart();
 
        center = this.getCenter();
 
        this.forEachSprite(function(sprite) {
            sprite.setAttributes({
                centerX: center[0] + offsetX
            });
        });
    },
 
    updateOffsetY: function(offsetY) {
        var center, thickness;
 
        // See 'updateRadius' comments.
        this.getChart();
 
        center = this.getCenter();
        thickness = this.getThickness();
 
        this.forEachSprite(function(sprite) {
            sprite.setAttributes({
                centerY: center[1] + offsetY - thickness / 2
            });
        });
    },
 
    updateAnimation: function(animation) {
        // See 'updateRadius' comments.
        this.getChart();
 
        this.forEachSprite(function(sprite) {
            sprite.setAnimation(animation);
        });
    },
 
    updateRenderer: function(renderer) {
        var rendererData;
 
        // See 'updateRadius' comments.
        this.getChart();
 
        rendererData = this.getRendererData();
 
        this.forEachSprite(function(sprite, itemIndex) {
            sprite.setConfig({
                renderer: renderer,
                rendererData: rendererData,
                rendererIndex: itemIndex
            });
        });
    },
 
    getRendererData: function() {
        return {
            store: this.getStore(),
            angleField: this.getXField(),
            radiusField: this.getYField(),
            series: this
        };
    },
 
    getSprites: function(createMissing) {
        var me = this,
            store = me.getStore(),
            sprites = me.sprites;
 
        if (!store) {
            return Ext.emptyArray;
        }
 
        if (sprites && !createMissing) {
            return sprites;
        }
 
        // eslint-disable-next-line vars-on-top, one-var
        var surface = me.getSurface(),
            records = store.getData().items,
            spritesPerSlice = me.spritesPerSlice,
            partCount = me.partNames.length,
            recordCount = records.length,
            sprite,
            i, j;
 
        for (= 0; i < recordCount; i++) {
            if (!sprites[* spritesPerSlice]) {
                for (= 0; j < partCount; j++) {
                    sprite = surface.add({
                        type: 'pie3dPart',
                        part: me.partNames[j],
                        series: me
                    });
                    sprite.getAnimation().setDurationOn('baseRotation', 0);
                    sprites.push(sprite);
                }
            }
        }
 
        return sprites;
    },
 
    betweenAngle: function(x, a, b) {
        var pp = Math.PI * 2,
            offset = this.rotationOffset;
 
        a += offset;
        b += offset;
 
        x -= a;
        b -= a;
 
        // Normalize, so that both x and b are in the [0,360) interval.
        // Since 360 * n angles will be normalized to 0,
        // we need to treat b === 0 as a special case.
        x %= pp;
        b %= pp;
        x += pp;
        b += pp;
        x %= pp;
        b %= pp;
 
        return x < b || b === 0;
    },
 
    getItemForPoint: function(x, y) {
        var me = this,
            sprites = me.getSprites(),
            spritesPerSlice = me.spritesPerSlice,
            result = null,
            store, records, hidden, i, ln, sprite, topPartIndex;
 
        if (!sprites) {
            return result;
        }
 
        store = me.getStore();
        records = store.getData().items;
        hidden = me.getHidden();
 
        for (= 0, ln = records.length; i < ln; i++) {
            if (hidden[i]) {
                continue;
            }
 
            topPartIndex = i * spritesPerSlice;
            sprite = sprites[topPartIndex];
 
            // This is CPU intensive on mousemove (no visial slowdown
            // on a fast machine, but some throttling might be desirable
            // on slower machines).
            // On touch devices performance/battery hit is negligible.
            if (sprite.hitTest([x, y])) {
                result = {
                    series: me,
                    sprite: sprites.slice(topPartIndex, topPartIndex + spritesPerSlice),
                    index: i,
                    record: records[i],
                    category: 'sprites',
                    field: me.getXField()
                };
                break;
            }
        }
 
        return result;
    },
 
    provideLegendInfo: function(target) {
        var me = this,
            store = me.getStore(),
            items, labelField, field, hidden, style, color, i;
 
        if (store) {
            items = store.getData().items;
            labelField = me.getLabel().getTemplate().getField();
            field = me.getField();
            hidden = me.getHidden();
 
            for (= 0; i < items.length; i++) {
                style = me.getStyleByIndex(i);
                color = style.baseColor;
                target.push({
                    name: labelField ? String(items[i].get(labelField)) : field + ' ' + i,
                    mark: color || 'black',
                    disabled: hidden[i],
                    series: me.getId(),
                    index: i
                });
            }
        }
    }
}, function() {
    var proto = this.prototype,
        definition = Ext.chart.series.sprite.Pie3DPart.def.getInitialConfig().processors.part;
 
    proto.partNames = definition.replace(/^enums\(|\)/g, '').split(',');
    proto.spritesPerSlice = proto.partNames.length;
});