/**
 * @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) {
        this.setSubStyle({baseColor: colors});
 
        if (!this.isConfiguring) {
            var 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(),
            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) {
        // 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();
 
        var donut = this.getDonut();
 
        this.forEachSprite(function (sprite) {
            sprite.setAttributes({
                endRho: radius,
                startRho: radius * donut / 100
            });
        });
    },
 
    updateDonut: function (donut) {
        // See 'updateRadius' comments. 
        this.getChart();
 
        var radius = this.getRadius();
 
        this.forEachSprite(function (sprite) {
            sprite.setAttributes({
                startRho: radius * donut / 100
            });
        });
    },
 
    updateCenter: function (center) {
        // See 'updateRadius' comments. 
        this.getChart();
 
        var 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) {
        // See 'updateRadius' comments. 
        this.getChart();
        // Radius depends on thickness and distortion, 
        // this will trigger its recalculation in the applier. 
        this.setRadius();
 
        var 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) {
        // See 'updateRadius' comments. 
        this.getChart();
        var center = this.getCenter();
 
        this.forEachSprite(function (sprite) {
            sprite.setAttributes({
                centerX: center[0] + offsetX
            });
        });
    },
 
    updateOffsetY: function (offsetY) {
        // See 'updateRadius' comments. 
        this.getChart();
 
        var 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) {
        // See 'updateRadius' comments. 
        this.getChart();
 
        var 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;
        }
 
        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(),
            result = null;
 
        if (!sprites) {
            return result;
        }
 
        var store = me.getStore(),
            records = store.getData().items,
            spritesPerSlice = me.spritesPerSlice,
            hidden = me.getHidden(),
            i, ln, sprite, topPartIndex;
 
        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();
 
        if (store) {
            var items = store.getData().items,
                labelField = me.getLabel().getTemplate().getField(),
                field = me.getField(),
                hidden = me.getHidden(),
                i, style, color;
 
            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;
});