/**
 * @class Ext.chart.series.sprite.Pie3DPart
 * @extends Ext.draw.sprite.Path
 *
 * Pie3D series sprite.
 */
Ext.define('Ext.chart.series.sprite.Pie3DPart', {
    extend: 'Ext.draw.sprite.Path',
    mixins: {
        markerHolder: 'Ext.chart.MarkerHolder'
    },
    alias: 'sprite.pie3dPart',
 
    inheritableStatics: {
        def: {
            processors: {
                /**
                 * @cfg {Number} [centerX=0]
                 * The central point of the series on the x-axis.
                 */
                centerX: 'number',
 
                /**
                 * @cfg {Number} [centerY=0]
                 * The central point of the series on the x-axis.
                 */
                centerY: 'number',
 
                /**
                 * @cfg {Number} [startAngle=0]
                 * The starting angle of the polar series.
                 */
                startAngle: 'number',
 
                /**
                 * @cfg {Number} [endAngle=Math.PI]
                 * The ending angle of the polar series.
                 */
                endAngle: 'number',
 
                /**
                 * @cfg {Number} [startRho=0]
                 * The starting radius of the polar series.
                 */
                startRho: 'number',
 
                /**
                 * @cfg {Number} [endRho=150]
                 * The ending radius of the polar series.
                 */
                endRho: 'number',
 
                /**
                 * @cfg {Number} [margin=0]
                 * Margin from the center of the pie. Used for donut.
                 */
                margin: 'number',
 
                /**
                 * @cfg {Number} [thickness=0]
                 * The thickness of the 3D pie part.
                 */
                thickness: 'number',
 
                /**
                 * @cfg {Number} [bevelWidth=5]
                 * The size of the 3D pie bevel.
                 */
                bevelWidth: 'number',
 
                /**
                 * @cfg {Number} [distortion=0]
                 * The distortion of the 3D pie part.
                 */
                distortion: 'number',
 
                /**
                 * @cfg {Object} [baseColor='white']
                 * The color of the 3D pie part before adding the 3D effect.
                 */
                baseColor: 'color',
 
                /**
                 * @cfg {Number} [colorSpread=1]
                 * An attribute used to control how flat the gradient of the sprite looks.
                 * A value of 0 essentially means no gradient (flat color).
                 */
                colorSpread: 'number',
 
                /**
                 * @cfg {Number} [baseRotation=0]
                 * The starting rotation of the polar series.
                 */
                baseRotation: 'number',
 
                /**
                 * @cfg {String} [part='top']
                 * The part of the 3D Pie represented by the sprite.
                 */
                part: 'enums(top,bottom,start,end,innerFront,innerBack,outerFront,outerBack)',
 
                /**
                 * @cfg {String} [label='']
                 * The label associated with the 'top' part of the sprite.
                 */
                label: 'string'
            },
            aliases: {
                rho: 'endRho'
            },
            triggers: {
                centerX: 'path,bbox',
                centerY: 'path,bbox',
                startAngle: 'path,partZIndex',
                endAngle: 'path,partZIndex',
                startRho: 'path',
                endRho: 'path,bbox',
                margin: 'path,bbox',
                thickness: 'path',
                distortion: 'path',
                baseRotation: 'path,partZIndex',
                baseColor: 'partZIndex,partColor',
                colorSpread: 'partColor',
                part: 'path,partZIndex',
                globalAlpha: 'canvas,alpha',
                fillOpacity: 'canvas,alpha'
            },
            defaults: {
                centerX: 0,
                centerY: 0,
                startAngle: Math.PI * 2,
                endAngle: Math.PI * 2,
                startRho: 0,
                endRho: 150,
                margin: 0,
                thickness: 35,
                distortion: 0.5,
                baseRotation: 0,
                baseColor: 'white',
                colorSpread: 1,
                miterLimit: 1,
                bevelWidth: 5,
                strokeOpacity: 0,
                part: 'top',
                label: ''
            },
            updaters: {
                alpha: 'alphaUpdater',
                partColor: 'partColorUpdater',
                partZIndex: 'partZIndexUpdater'
            }
        }
    },
 
    bevelParams: [],
 
    constructor: function (config) {
        this.callParent([config]);
 
        this.bevelGradient = new Ext.draw.gradient.Linear({
            stops: [{
                offset: 0,
                color: 'rgba(255,255,255,0)'
            }, {
                offset: 0.7,
                color: 'rgba(255,255,255,0.6)'
            }, {
                offset: 1,
                color: 'rgba(255,255,255,0)'
            }]
        });
    },
 
    alphaUpdater: function (attr) {
        var me = this,
            opacity = attr.globalAlpha,
            fillOpacity = attr.fillOpacity,
            oldOpacity = me.oldOpacity,
            oldFillOpacity = me.oldFillOpacity;
 
        // Update the path when the sprite becomes translucent or completely opaque. 
        if ((opacity !== oldOpacity && (opacity === 1 || oldOpacity === 1)) ||
            (fillOpacity !== oldFillOpacity && (fillOpacity === 1 || oldFillOpacity === 1))) {
            me.scheduleUpdater(attr, 'path', ['globalAlpha']);
            me.oldOpacity = opacity;
            me.oldFillOpacity = fillOpacity;
        }
    },
 
    partColorUpdater: function (attr) {
        var color = Ext.util.Color.fly(attr.baseColor),
            colorString = color.toString(),
            colorSpread = attr.colorSpread,
            fillStyle;
 
        switch (attr.part) {
            case 'top':
                fillStyle = new Ext.draw.gradient.Radial({
                    start: {
                        x: 0,
                        y: 0,
                        r: 0
                    },
                    end: {
                        x: 0,
                        y: 0,
                        r: 1
                    },
                    stops: [{
                        offset: 0,
                        color: color.createLighter(0.1 * colorSpread)
                    }, {
                        offset: 1,
                        color: color.createDarker(0.1 * colorSpread)
                    }]
                });
                break;
            case 'bottom':
                fillStyle =  new Ext.draw.gradient.Radial({
                    start: {
                        x: 0,
                        y: 0,
                        r: 0
                    },
                    end: {
                        x: 0,
                        y: 0,
                        r: 1
                    },
                    stops: [{
                        offset: 0,
                        color: color.createDarker(0.2 * colorSpread)
                    }, {
                        offset: 1,
                        color: color.toString()
                    }]
                });
                break;
            case 'outerFront':
            case 'outerBack':
                fillStyle =  new Ext.draw.gradient.Linear({
                    stops: [{
                        offset: 0,
                        color: color.createDarker(0.15 * colorSpread).toString()
                    }, {
                        offset: 0.3,
                        color: colorString
                    }, {
                        offset: 0.8,
                        color: color.createLighter(0.2 * colorSpread).toString()
                    }, {
                        offset: 1,
                        color: color.createDarker(0.25 * colorSpread).toString()
                    }]
                });
                break;
            case 'start':
                fillStyle = new Ext.draw.gradient.Linear({
                    stops: [{
                        offset: 0,
                        color: color.createDarker(0.1 * colorSpread).toString()
                    }, {
                        offset: 1,
                        color: color.createLighter(0.2 * colorSpread).toString()
                    }]
                });
                break;
            case 'end':
                fillStyle = new Ext.draw.gradient.Linear({
                    stops: [{
                        offset: 0,
                        color: color.createDarker(0.1 * colorSpread).toString()
                    }, {
                        offset: 1,
                        color: color.createLighter(0.2 * colorSpread).toString()
                    }]
                });
                break;
            case 'innerFront':
            case 'innerBack':
                fillStyle = new Ext.draw.gradient.Linear({
                    stops: [{
                        offset: 0,
                        color: color.createDarker(0.1 * colorSpread).toString()
                    }, {
                        offset: 0.2,
                        color: color.createLighter(0.2 * colorSpread).toString()
                    }, {
                        offset: 0.7,
                        color: colorString
                    }, {
                        offset: 1,
                        color: color.createDarker(0.1 * colorSpread).toString()
                    }]
                });
                break;
        }
 
        attr.fillStyle = fillStyle;
        attr.canvasAttributes.fillStyle = fillStyle;
    },
 
    partZIndexUpdater: function (attr) {
        var normalize = Ext.draw.sprite.AttributeParser.angle,
            rotation = attr.baseRotation,
            startAngle = attr.startAngle,
            endAngle = attr.endAngle,
            depth;
 
        switch (attr.part) {
            case 'top':
                attr.zIndex = 6;
                break;
            case 'outerFront':
                startAngle = normalize(startAngle + rotation);
                endAngle   = normalize(endAngle   + rotation);
                if (startAngle >= 0 && endAngle < 0) {
                    depth = Math.sin(startAngle);
                } else if (startAngle <= 0 && endAngle > 0) {
                    depth = Math.sin(endAngle);
                } else if (startAngle >= 0 && endAngle > 0) {
                    if (startAngle > endAngle) {
                        depth = 0;
                    } else {
                        depth = Math.max(Math.sin(startAngle), Math.sin(endAngle));
                    }
                } else {
                    depth = 1;
                }
                attr.zIndex = 4 + depth;
                break;
            case 'outerBack':
                attr.zIndex = 1;
                break;
            case 'start':
                attr.zIndex = 4 + Math.sin(normalize(startAngle + rotation));
                break;
            case 'end':
                attr.zIndex = 4 + Math.sin(normalize(endAngle + rotation));
                break;
            case 'innerFront':
                attr.zIndex = 2;
                break;
            case 'innerBack':
                attr.zIndex = 4 + Math.sin(normalize((startAngle + endAngle) / 2 + rotation));
                break;
            case 'bottom':
                attr.zIndex = 0;
                break;
        }
        attr.dirtyZIndex = true;
    },
 
    updatePlainBBox: function (plain) {
        var attr = this.attr,
            part = attr.part,
            baseRotation = attr.baseRotation,
            centerX = attr.centerX,
            centerY = attr.centerY,
            rho, angle, x, y, sin, cos;
 
        if (part === 'start') {
            angle = attr.startAngle + baseRotation;
        } else if (part === 'end') {
            angle = attr.endAngle + baseRotation;
        }
 
        if (Ext.isNumber(angle)) {
            sin = Math.sin(angle);
            cos = Math.cos(angle);
 
            x = Math.min(
                centerX + cos * attr.startRho,
                centerX + cos * attr.endRho
            );
            y = centerY + sin * attr.startRho * attr.distortion;
 
            plain.x = x;
            plain.y = y;
            plain.width = cos * (attr.endRho - attr.startRho);
            plain.height = attr.thickness + sin * (attr.endRho - attr.startRho) * 2;
 
            return;
        }
 
        if (part === 'innerFront' || part === 'innerBack') {
            rho = attr.startRho;
        } else {
            rho = attr.endRho;
        }
 
        plain.width = rho * 2;
        plain.height = rho * attr.distortion * 2 + attr.thickness;
        plain.x = attr.centerX - rho;
        plain.y = attr.centerY - rho * attr.distortion;
    },
 
    updateTransformedBBox: function (transform) {
        if (this.attr.part === 'start' || this.attr.part === 'end') {
            return this.callParent(arguments);
        }
        return this.updatePlainBBox(transform);
    },
 
    updatePath: function (path) {
        if (!this.attr.globalAlpha) {
            return;
        }
        if (this.attr.endAngle < this.attr.startAngle) {
            return;
        }
        this[this.attr.part + 'Renderer'](path);
    },
 
    render: function (surface, ctx) {
        var me = this,
            attr = me.attr;
 
        if (!attr.globalAlpha || Ext.Number.isEqual(attr.startAngle, attr.endAngle, 1e-8)) {
            return;
        }
        me.callParent([surface, ctx]);
        me.bevelRenderer(surface, ctx);
 
        // Only the top part will have the label attribute (set by the series). 
        if (attr.label && me.getMarker('labels')) {
            me.placeLabel();
        }
    },
 
    placeLabel: function () {
        var me = this,
            attr = me.attr,
            attributeId = attr.attributeId,
            margin = attr.margin,
            distortion = attr.distortion,
            centerX = attr.centerX,
            centerY = attr.centerY,
            baseRotation = attr.baseRotation,
            startAngle = attr.startAngle + baseRotation,
            endAngle = attr.endAngle + baseRotation,
            midAngle = (startAngle + endAngle) / 2,
            startRho = attr.startRho + margin,
            endRho = attr.endRho + margin,
            midRho = (startRho + endRho) / 2,
            sin = Math.sin(midAngle),
            cos = Math.cos(midAngle),
            surfaceMatrix = me.surfaceMatrix,
            label = me.getMarker('labels'),
            labelTpl = label.getTemplate(),
            calloutLine = labelTpl.getCalloutLine(),
            calloutLineLength = calloutLine && calloutLine.length || 40,
            labelCfg = {},
            x, y;
 
        surfaceMatrix.appendMatrix(attr.matrix);
 
        labelCfg.text = attr.label;
 
        x = centerX + cos * midRho;
        y = centerY + sin * midRho * distortion;
 
        labelCfg.x = surfaceMatrix.x(x, y);
        labelCfg.y = surfaceMatrix.y(x, y);
 
        x = centerX + cos * endRho;
        y = centerY + sin * endRho * distortion;
 
        labelCfg.calloutStartX = surfaceMatrix.x(x, y);
        labelCfg.calloutStartY = surfaceMatrix.y(x, y);
 
        x = centerX + cos * (endRho + calloutLineLength);
        y = centerY + sin * (endRho + calloutLineLength) * distortion;
        labelCfg.calloutPlaceX = surfaceMatrix.x(x, y);
        labelCfg.calloutPlaceY = surfaceMatrix.y(x, y);
 
        labelCfg.calloutWidth = 2;
 
        me.putMarker('labels', labelCfg, attributeId);
 
        me.putMarker('labels', {
            callout: 1
        }, attributeId);
    },
 
    bevelRenderer: function (surface, ctx) {
        var me = this,
            attr = me.attr,
            bevelWidth = attr.bevelWidth,
            params = me.bevelParams,
            i;
 
        for (= 0; i < params.length; i++) {
            ctx.beginPath();
            ctx.ellipse.apply(ctx, params[i]);
            ctx.save();
            ctx.lineWidth = bevelWidth;
            ctx.strokeOpacity = bevelWidth ? 1 : 0;
            ctx.strokeGradient = me.bevelGradient;
            ctx.stroke(attr);
            ctx.restore();
        }
    },
 
    lidRenderer: function (path, thickness) {
        var attr = this.attr,
            margin = attr.margin,
            distortion = attr.distortion,
            centerX = attr.centerX,
            centerY = attr.centerY,
            baseRotation = attr.baseRotation,
            startAngle = attr.startAngle + baseRotation,
            endAngle = attr.endAngle + baseRotation,
            midAngle = (startAngle + endAngle) / 2,
            startRho = attr.startRho,
            endRho = attr.endRho,
            sinEnd = Math.sin(endAngle),
            cosEnd = Math.cos(endAngle);
 
        centerX += Math.cos(midAngle) * margin;
        centerY += Math.sin(midAngle) * margin * distortion;
 
        path.ellipse(
            centerX, centerY + thickness,
            startRho, startRho * distortion,
            0, startAngle, endAngle, false
        );
        path.lineTo(
            centerX + cosEnd * endRho,
            centerY + thickness + sinEnd * endRho * distortion
        );
        path.ellipse(
            centerX, centerY + thickness,
            endRho, endRho * distortion,
            0, endAngle, startAngle, true
        );
        path.closePath();
    },
 
    topRenderer: function (path) {
        this.lidRenderer(path, 0);
    },
 
    bottomRenderer: function (path) {
        var attr = this.attr,
            none = Ext.util.Color.RGBA_NONE;
 
        if (attr.globalAlpha < 1 || attr.fillOpacity < 1 || attr.shadowColor !== none) {
            this.lidRenderer(path, attr.thickness);
        }
    },
 
    sideRenderer: function (path, position) {
        var attr = this.attr,
            margin = attr.margin,
            centerX = attr.centerX,
            centerY = attr.centerY,
            distortion = attr.distortion,
            baseRotation = attr.baseRotation,
            startAngle = attr.startAngle + baseRotation,
            endAngle = attr.endAngle + baseRotation,
            isFullPie = (!attr.startAngle && Ext.Number.isEqual(Math.PI * 2, attr.endAngle, 0.0000001)),
            thickness = attr.thickness,
            startRho = attr.startRho,
            endRho = attr.endRho,
            angle = (position === 'start' && startAngle) ||
                    (position === 'end' && endAngle),
            sin = Math.sin(angle),
            cos = Math.cos(angle),
            isTranslucent = attr.globalAlpha < 1,
            isVisible = position === 'start' && cos < 0 ||
                        position === 'end' && cos > 0 ||
                        isTranslucent,
            midAngle;
 
        if (isVisible && !isFullPie) {
            midAngle = (startAngle + endAngle) / 2;
            centerX += Math.cos(midAngle) * margin;
            centerY += Math.sin(midAngle) * margin * distortion;
            path.moveTo(
                centerX + cos * startRho,
                centerY + sin * startRho * distortion
            );
            path.lineTo(
                centerX + cos * endRho,
                centerY + sin * endRho * distortion
            );
            path.lineTo(
                centerX + cos * endRho,
                centerY + sin * endRho * distortion + thickness
            );
            path.lineTo(
                centerX + cos * startRho,
                centerY + sin * startRho * distortion + thickness
            );
            path.closePath();
        }
    },
 
    startRenderer: function (path) {
        this.sideRenderer(path, 'start');
    },
 
    endRenderer: function (path) {
        this.sideRenderer(path, 'end');
    },
 
    rimRenderer: function (path, radius, isDonut, isFront) {
        var me = this,
            attr = me.attr,
            margin = attr.margin,
            centerX = attr.centerX,
            centerY = attr.centerY,
            distortion = attr.distortion,
            baseRotation = attr.baseRotation,
            normalize = Ext.draw.sprite.AttributeParser.angle,
            startAngle = attr.startAngle + baseRotation,
            endAngle = attr.endAngle + baseRotation,
            // It's critical to use non-normalized start and end angles 
            // for middle angle calculation. Consider a situation where the 
            // start angle is +170 degrees and the end engle is -170 degrees 
            // after normalization (the middle angle is 0 then, but it should be 180 degrees). 
            midAngle = normalize((startAngle + endAngle) / 2),
            thickness = attr.thickness,
            isTranslucent = attr.globalAlpha < 1,
            isAllFront, isAllBack,
            params;
 
        me.bevelParams = [];
 
        startAngle = normalize(startAngle);
        endAngle = normalize(endAngle);
 
        centerX += Math.cos(midAngle) * margin;
        centerY += Math.sin(midAngle) * margin * distortion;
 
        isAllFront = startAngle >= 0 && endAngle >= 0;
        isAllBack = startAngle <= 0 && endAngle <= 0;
 
        function renderLeftFrontChunk() {
            path.ellipse(
                centerX, centerY + thickness,
                radius, radius * distortion,
                0, Math.PI, startAngle, true
            );
            path.lineTo(
                centerX + Math.cos(startAngle) * radius,
                centerY + Math.sin(startAngle) * radius * distortion
            );
            params = [
                centerX, centerY,
                radius, radius * distortion,
                0, startAngle, Math.PI, false
            ];
            if (!isDonut) {
                me.bevelParams.push(params);
            }
            path.ellipse.apply(path, params);
            path.closePath();
        }
 
        function renderRightFrontChunk() {
            path.ellipse(
                centerX, centerY + thickness,
                radius, radius * distortion,
                0, 0, endAngle, false
            );
            path.lineTo(
                centerX + Math.cos(endAngle) * radius,
                centerY + Math.sin(endAngle) * radius * distortion
            );
            params = [
                centerX, centerY,
                radius, radius * distortion,
                0, endAngle, 0, true
            ];
            if (!isDonut) {
                me.bevelParams.push(params);
            }
            path.ellipse.apply(path, params);
            path.closePath();
        }
 
        function renderLeftBackChunk() {
            path.ellipse(
                centerX, centerY + thickness,
                radius, radius * distortion,
                0, Math.PI, endAngle, false
            );
            path.lineTo(
                centerX + Math.cos(endAngle) * radius,
                centerY + Math.sin(endAngle) * radius * distortion
            );
            params = [
                centerX, centerY,
                radius, radius * distortion,
                0, endAngle, Math.PI, true
            ];
            if (isDonut) {
                me.bevelParams.push(params);
            }
            path.ellipse.apply(path, params);
            path.closePath();
        }
 
 
        function renderRightBackChunk() {
            path.ellipse(
                centerX, centerY + thickness,
                radius, radius * distortion,
                0, startAngle, 0, false
            );
            path.lineTo(
                centerX + radius,
                centerY
            );
            params = [
                centerX, centerY,
                radius, radius * distortion,
                0, 0, startAngle, true
            ];
            if (isDonut) {
                me.bevelParams.push(params);
            }
            path.ellipse.apply(path, params);
            path.closePath();
        }
 
        if (isFront) {
            if (!isDonut || isTranslucent) {
                if (startAngle >= 0 && endAngle < 0) {
                    renderLeftFrontChunk();
                } else if (startAngle <= 0 && endAngle > 0) {
                    renderRightFrontChunk();
                } else if (startAngle <= 0 && endAngle < 0) {
                    if (startAngle > endAngle) {
                        path.ellipse(
                            centerX, centerY + thickness,
                            radius, radius * distortion,
                            0, 0, Math.PI, false
                        );
                        path.lineTo(
                            centerX - radius,
                            centerY
                        );
                        params = [
                            centerX, centerY,
                            radius, radius * distortion,
                            0, Math.PI, 0, true
                        ];
                        if (!isDonut) {
                            me.bevelParams.push(params);
                        }
                        path.ellipse.apply(path, params);
                        path.closePath();
                    }
                } else { // startAngle >= 0 && endAngle > 0 
                    // obtuse horseshoe-like slice with the gap facing forward 
                    if (startAngle > endAngle) {
                        renderLeftFrontChunk();
                        renderRightFrontChunk();
                    } else { // acute slice facing forward 
                        params = [
                            centerX, centerY,
                            radius, radius * distortion,
                            0, startAngle, endAngle, false
                        ];
                        if (isAllFront && !isDonut || isAllBack && isDonut) {
                            me.bevelParams.push(params);
                        }
                        path.ellipse.apply(path, params);
                        path.lineTo(
                            centerX + Math.cos(endAngle) * radius,
                            centerY + Math.sin(endAngle) * radius * distortion + thickness
                        );
                        path.ellipse(
                            centerX, centerY + thickness,
                            radius, radius * distortion,
                            0, endAngle, startAngle, true
                        );
                        path.closePath();
                    }
                }
            }
        } else {
            if (isDonut || isTranslucent) {
                if (startAngle >= 0 && endAngle < 0) {
                    renderLeftBackChunk();
                } else if (startAngle <= 0 && endAngle > 0) {
                    renderRightBackChunk();
                } else if (startAngle <= 0 && endAngle < 0) {
                    if (startAngle > endAngle) {
                        renderLeftBackChunk();
                        renderRightBackChunk();
                    } else {
                        path.ellipse(
                            centerX, centerY + thickness,
                            radius, radius * distortion,
                            0, startAngle, endAngle, false
                        );
                        path.lineTo(
                            centerX + Math.cos(endAngle) * radius,
                            centerY + Math.sin(endAngle) * radius * distortion
                        );
                        params = [
                            centerX, centerY,
                            radius, radius * distortion,
                            0, endAngle, startAngle, true
                        ];
                        if (isDonut) {
                            me.bevelParams.push(params);
                        }
                        path.ellipse.apply(path, params);
                        path.closePath();
                    }
                } else { // startAngle >= 0 && endAngle > 0 
                    if (startAngle > endAngle) {
                        path.ellipse(
                            centerX, centerY + thickness,
                            radius, radius * distortion,
                            0, -Math.PI, 0, false
                        );
                        path.lineTo(
                            centerX + radius,
                            centerY
                        );
                        params = [
                            centerX, centerY,
                            radius, radius * distortion,
                            0, 0, -Math.PI, true
                        ];
                        if (isDonut) {
                            me.bevelParams.push(params);
                        }
                        path.ellipse.apply(path, params);
                        path.closePath();
                    }
                }
            }
        }
    },
 
    innerFrontRenderer: function (path) {
        this.rimRenderer(path, this.attr.startRho, true, true);
    },
 
    innerBackRenderer: function (path) {
        this.rimRenderer(path, this.attr.startRho, true, false);
    },
 
    outerFrontRenderer: function (path) {
        this.rimRenderer(path, this.attr.endRho, false, true);
    },
 
    outerBackRenderer: function (path) {
        this.rimRenderer(path, this.attr.endRho, false, false);
    }
});