/**

 * Highcharts Drilldown plugin

 * 

 * Author: Torstein Honsi

 * License: MIT License

 *

 * Demo: http://jsfiddle.net/highcharts/Vf3yT/

 */



/*global Highcharts,HighchartsAdapter*/

(function (H) {



	"use strict";



	var noop = function () {},

		defaultOptions = H.getOptions(),

		each = H.each,

		extend = H.extend,

		format = H.format,

		pick = H.pick,

		wrap = H.wrap,

		Chart = H.Chart,

		seriesTypes = H.seriesTypes,

		PieSeries = seriesTypes.pie,

		ColumnSeries = seriesTypes.column,

		fireEvent = HighchartsAdapter.fireEvent,

		inArray = HighchartsAdapter.inArray,

		dupes = [],

		ddSeriesId = 1;



	// Utilities

	/*

	 * Return an intermediate color between two colors, according to pos where 0

	 * is the from color and 1 is the to color. This method is copied from ColorAxis.js

	 * and should always be kept updated, until we get AMD support.

	 */

	function tweenColors(from, to, pos) {

		// Check for has alpha, because rgba colors perform worse due to lack of

		// support in WebKit.

		var hasAlpha,

			ret;



		// Unsupported color, return to-color (#3920)

		if (!to.rgba.length || !from.rgba.length) {

			Highcharts.error(23);

			ret = to.raw;



		// Interpolate

		} else {

			from = from.rgba;

			to = to.rgba;

			hasAlpha = (to[3] !== 1 || from[3] !== 1);

			ret = (hasAlpha ? 'rgba(' : 'rgb(') + 

				Math.round(to[0] + (from[0] - to[0]) * (1 - pos)) + ',' + 

				Math.round(to[1] + (from[1] - to[1]) * (1 - pos)) + ',' + 

				Math.round(to[2] + (from[2] - to[2]) * (1 - pos)) + 

				(hasAlpha ? (',' + (to[3] + (from[3] - to[3]) * (1 - pos))) : '') + ')';

		}

		return ret;

	}

	/**

	 * Handle animation of the color attributes directly

	 */

	each(['fill', 'stroke'], function (prop) {

		HighchartsAdapter.addAnimSetter(prop, function (fx) {

			fx.elem.attr(prop, tweenColors(H.Color(fx.start), H.Color(fx.end), fx.pos));

		});

	});



	// Add language

	extend(defaultOptions.lang, {

		drillUpText: '◁ Back to {series.name}'

	});

	defaultOptions.drilldown = {

		activeAxisLabelStyle: {

			cursor: 'pointer',

			color: '#0d233a',

			fontWeight: 'bold',

			textDecoration: 'underline'			

		},

		activeDataLabelStyle: {

			cursor: 'pointer',

			color: '#0d233a',

			fontWeight: 'bold',

			textDecoration: 'underline'			

		},

		animation: {

			duration: 500

		},

		drillUpButton: {

			position: { 

				align: 'right',

				x: -10,

				y: 10

			}

			// relativeTo: 'plotBox'

			// theme

		}

	};	



	/**

	 * A general fadeIn method

	 */

	H.SVGRenderer.prototype.Element.prototype.fadeIn = function (animation) {

		this

		.attr({

			opacity: 0.1,

			visibility: 'inherit'

		})

		.animate({

			opacity: pick(this.newOpacity, 1) // newOpacity used in maps

		}, animation || {

			duration: 250

		});

	};



	Chart.prototype.addSeriesAsDrilldown = function (point, ddOptions) {

		this.addSingleSeriesAsDrilldown(point, ddOptions);

		this.applyDrilldown();

	};

	Chart.prototype.addSingleSeriesAsDrilldown = function (point, ddOptions) {

		var oldSeries = point.series,

			xAxis = oldSeries.xAxis,

			yAxis = oldSeries.yAxis,

			newSeries,

			color = point.color || oldSeries.color,

			pointIndex,

			levelSeries = [],

			levelSeriesOptions = [],

			level,

			levelNumber;



		levelNumber = oldSeries.options._levelNumber || 0;

			

		ddOptions = extend({

			color: color,

			_ddSeriesId: ddSeriesId++

		}, ddOptions);

		pointIndex = inArray(point, oldSeries.points);



		// Record options for all current series

		each(oldSeries.chart.series, function (series) {

			if (series.xAxis === xAxis) {

				levelSeries.push(series);

				series.options._ddSeriesId = series.options._ddSeriesId || ddSeriesId++;

				series.options._colorIndex = series.userOptions._colorIndex;

				levelSeriesOptions.push(series.options);

				series.options._levelNumber = series.options._levelNumber || levelNumber; // #3182

			}

		});



		// Add a record of properties for each drilldown level

		level = {

			levelNumber: levelNumber,

			seriesOptions: oldSeries.options,

			levelSeriesOptions: levelSeriesOptions,

			levelSeries: levelSeries,

			shapeArgs: point.shapeArgs,

			bBox: point.graphic ? point.graphic.getBBox() : {}, // no graphic in line series with markers disabled

			color: color,

			lowerSeriesOptions: ddOptions,

			pointOptions: oldSeries.options.data[pointIndex],

			pointIndex: pointIndex,

			oldExtremes: {

				xMin: xAxis && xAxis.userMin,

				xMax: xAxis && xAxis.userMax,

				yMin: yAxis && yAxis.userMin,

				yMax: yAxis && yAxis.userMax

			}

		};



		// Generate and push it to a lookup array

		if (!this.drilldownLevels) {

			this.drilldownLevels = [];

		}

		this.drilldownLevels.push(level);



		newSeries = level.lowerSeries = this.addSeries(ddOptions, false);

		newSeries.options._levelNumber = levelNumber + 1;

		if (xAxis) {

			xAxis.oldPos = xAxis.pos;

			xAxis.userMin = xAxis.userMax = null;

			yAxis.userMin = yAxis.userMax = null;

		}



		// Run fancy cross-animation on supported and equal types

		if (oldSeries.type === newSeries.type) {

			newSeries.animate = newSeries.animateDrilldown || noop;

			newSeries.options.animation = true;

		}

	};



	Chart.prototype.applyDrilldown = function () {

		var drilldownLevels = this.drilldownLevels, 

			levelToRemove;

		

		if (drilldownLevels && drilldownLevels.length > 0) { // #3352, async loading

			levelToRemove = drilldownLevels[drilldownLevels.length - 1].levelNumber;

			each(this.drilldownLevels, function (level) {

				if (level.levelNumber === levelToRemove) {

					each(level.levelSeries, function (series) {

						if (series.options && series.options._levelNumber === levelToRemove) { // Not removed, not added as part of a multi-series drilldown

							series.remove(false);

						}

					});

				}

			});

		}

		

		this.redraw();

		this.showDrillUpButton();

	};



	Chart.prototype.getDrilldownBackText = function () {

		var drilldownLevels = this.drilldownLevels,

			lastLevel;

		if (drilldownLevels && drilldownLevels.length > 0) { // #3352, async loading

			lastLevel = drilldownLevels[drilldownLevels.length - 1];

			lastLevel.series = lastLevel.seriesOptions;

			return format(this.options.lang.drillUpText, lastLevel);

		}



	};



	Chart.prototype.showDrillUpButton = function () {

		var chart = this,

			backText = this.getDrilldownBackText(),

			buttonOptions = chart.options.drilldown.drillUpButton,

			attr,

			states;

			



		if (!this.drillUpButton) {

			attr = buttonOptions.theme;

			states = attr && attr.states;

						

			this.drillUpButton = this.renderer.button(

				backText,

				null,

				null,

				function () {

					chart.drillUp(); 

				},

				attr, 

				states && states.hover,

				states && states.select

			)

			.attr({

				align: buttonOptions.position.align,

				zIndex: 9

			})

			.add()

			.align(buttonOptions.position, false, buttonOptions.relativeTo || 'plotBox');

		} else {

			this.drillUpButton.attr({

				text: backText

			})

			.align();

		}

	};



	Chart.prototype.drillUp = function () {

		var chart = this,

			drilldownLevels = chart.drilldownLevels,

			levelNumber = drilldownLevels[drilldownLevels.length - 1].levelNumber,

			i = drilldownLevels.length,

			chartSeries = chart.series,

			seriesI,

			level,

			oldSeries,

			newSeries,

			oldExtremes,

			addSeries = function (seriesOptions) {

				var addedSeries;

				each(chartSeries, function (series) {

					if (series.options._ddSeriesId === seriesOptions._ddSeriesId) {

						addedSeries = series;

					}

				});



				addedSeries = addedSeries || chart.addSeries(seriesOptions, false);

				if (addedSeries.type === oldSeries.type && addedSeries.animateDrillupTo) {

					addedSeries.animate = addedSeries.animateDrillupTo;

				}

				if (seriesOptions === level.seriesOptions) {

					newSeries = addedSeries;

				}

			};



		while (i--) {



			level = drilldownLevels[i];

			if (level.levelNumber === levelNumber) {

				drilldownLevels.pop();

				

				// Get the lower series by reference or id

				oldSeries = level.lowerSeries;

				if (!oldSeries.chart) {  // #2786

					seriesI = chartSeries.length; // #2919

					while (seriesI--) {

						if (chartSeries[seriesI].options.id === level.lowerSeriesOptions.id) {

							oldSeries = chartSeries[seriesI];

							break;

						}

					}

				}

				oldSeries.xData = []; // Overcome problems with minRange (#2898)



				each(level.levelSeriesOptions, addSeries);

				

				fireEvent(chart, 'drillup', { seriesOptions: level.seriesOptions });



				if (newSeries.type === oldSeries.type) {

					newSeries.drilldownLevel = level;

					newSeries.options.animation = chart.options.drilldown.animation;



					if (oldSeries.animateDrillupFrom && oldSeries.chart) { // #2919

						oldSeries.animateDrillupFrom(level);

					}

				}

				newSeries.options._levelNumber = levelNumber;

				

				oldSeries.remove(false);



				// Reset the zoom level of the upper series

				if (newSeries.xAxis) {

					oldExtremes = level.oldExtremes;

					newSeries.xAxis.setExtremes(oldExtremes.xMin, oldExtremes.xMax, false);

					newSeries.yAxis.setExtremes(oldExtremes.yMin, oldExtremes.yMax, false);

				}

			}

		}



		this.redraw();



		if (this.drilldownLevels.length === 0) {

			this.drillUpButton = this.drillUpButton.destroy();

		} else {

			this.drillUpButton.attr({

				text: this.getDrilldownBackText()

			})

			.align();

		}



		dupes.length = []; // #3315

	};





	ColumnSeries.prototype.supportsDrilldown = true;

	

	/**

	 * When drilling up, keep the upper series invisible until the lower series has

	 * moved into place

	 */

	ColumnSeries.prototype.animateDrillupTo = function (init) {

		if (!init) {

			var newSeries = this,

				level = newSeries.drilldownLevel;



			each(this.points, function (point) {

				if (point.graphic) { // #3407

					point.graphic.hide();

				}

				if (point.dataLabel) {

					point.dataLabel.hide();

				}

				if (point.connector) {

					point.connector.hide();

				}

			});





			// Do dummy animation on first point to get to complete

			setTimeout(function () {

				if (newSeries.points) { // May be destroyed in the meantime, #3389

					each(newSeries.points, function (point, i) {  

						// Fade in other points			  

						var verb = i === (level && level.pointIndex) ? 'show' : 'fadeIn',

							inherit = verb === 'show' ? true : undefined;

						if (point.graphic) { // #3407

							point.graphic[verb](inherit);

						}

						if (point.dataLabel) {

							point.dataLabel[verb](inherit);

						}

						if (point.connector) {

							point.connector[verb](inherit);

						}

					});

				}

			}, Math.max(this.chart.options.drilldown.animation.duration - 50, 0));



			// Reset

			this.animate = noop;

		}



	};

	

	ColumnSeries.prototype.animateDrilldown = function (init) {

		var series = this,

			drilldownLevels = this.chart.drilldownLevels,

			animateFrom,

			animationOptions = this.chart.options.drilldown.animation,

			xAxis = this.xAxis;

			

		if (!init) {

			each(drilldownLevels, function (level) {

				if (series.options._ddSeriesId === level.lowerSeriesOptions._ddSeriesId) {

					animateFrom = level.shapeArgs;

					animateFrom.fill = level.color;

				}

			});



			animateFrom.x += (pick(xAxis.oldPos, xAxis.pos) - xAxis.pos);



			each(this.points, function (point) {

				if (point.graphic) {

					point.graphic

						.attr(animateFrom)

						.animate(

							extend(point.shapeArgs, { fill: point.color }), 

							animationOptions

						);

				}

				if (point.dataLabel) {

					point.dataLabel.fadeIn(animationOptions);

				}

			});

			this.animate = null;

		}

		

	};



	/**

	 * When drilling up, pull out the individual point graphics from the lower series

	 * and animate them into the origin point in the upper series.

	 */

	ColumnSeries.prototype.animateDrillupFrom = function (level) {

		var animationOptions = this.chart.options.drilldown.animation,

			group = this.group,

			series = this;



		// Cancel mouse events on the series group (#2787)

		each(series.trackerGroups, function (key) {

			if (series[key]) { // we don't always have dataLabelsGroup

				series[key].on('mouseover');

			}

		});

			



		delete this.group;

		each(this.points, function (point) {

			var graphic = point.graphic,

				complete = function () {

					graphic.destroy();

					if (group) {

						group = group.destroy();

					}

				};



			if (graphic) {

			

				delete point.graphic;



				if (animationOptions) {

					graphic.animate(

						extend(level.shapeArgs, { fill: level.color }),

						H.merge(animationOptions, { complete: complete })

					);

				} else {

					graphic.attr(level.shapeArgs);

					complete();

				}

			}

		});

	};



	if (PieSeries) {

		extend(PieSeries.prototype, {

			supportsDrilldown: true,

			animateDrillupTo: ColumnSeries.prototype.animateDrillupTo,

			animateDrillupFrom: ColumnSeries.prototype.animateDrillupFrom,



			animateDrilldown: function (init) {

				var level = this.chart.drilldownLevels[this.chart.drilldownLevels.length - 1],

					animationOptions = this.chart.options.drilldown.animation,

					animateFrom = level.shapeArgs,

					start = animateFrom.start,

					angle = animateFrom.end - start,

					startAngle = angle / this.points.length;



				if (!init) {

					each(this.points, function (point, i) {

						point.graphic

							.attr(H.merge(animateFrom, {

								start: start + i * startAngle,

								end: start + (i + 1) * startAngle,

								fill: level.color

							}))[animationOptions ? 'animate' : 'attr'](

								extend(point.shapeArgs, { fill: point.color }), 

								animationOptions

							);

					});

					this.animate = null;

				}

			}

		});

	}

	

	H.Point.prototype.doDrilldown = function (_holdRedraw, category) {

		var series = this.series,

			chart = series.chart,

			drilldown = chart.options.drilldown,

			i = (drilldown.series || []).length,

			seriesOptions;

		

		while (i-- && !seriesOptions) {

			if (drilldown.series[i].id === this.drilldown && inArray(this.drilldown, dupes) === -1) {

				seriesOptions = drilldown.series[i];

				dupes.push(this.drilldown);

			}

		}



		// Fire the event. If seriesOptions is undefined, the implementer can check for 

		// seriesOptions, and call addSeriesAsDrilldown async if necessary.

		fireEvent(chart, 'drilldown', { 

			point: this,

			seriesOptions: seriesOptions,

			category: category,

			points: category !== undefined && this.series.xAxis.ticks[category].label.ddPoints.slice(0)

		});

		

		if (seriesOptions) {

			if (_holdRedraw) {

				chart.addSingleSeriesAsDrilldown(this, seriesOptions);

			} else {

				chart.addSeriesAsDrilldown(this, seriesOptions);

			}

		}

	};



	/**

	 * Drill down to a given category. This is the same as clicking on an axis label.

	 */

	H.Axis.prototype.drilldownCategory = function (x) {

		each(this.ticks[x].label.ddPoints, function (point) {

			if (point.series && point.series.visible && point.doDrilldown) { // #3197

				point.doDrilldown(true, x);

			}

		});

		this.chart.applyDrilldown();

	};

	



	/**

	 * On initialization of each point, identify its label and make it clickable. Also, provide a

	 * list of points associated to that label.

	 */

	wrap(H.Point.prototype, 'init', function (proceed, series, options, x) {

		var point = proceed.call(this, series, options, x),

			chart = series.chart,

			tick = series.xAxis && series.xAxis.ticks[x],

			tickLabel = tick && tick.label;



		// Create a collection of points associated with the label. Reset it for each level.

		if (tickLabel) {

			if (!tickLabel.ddPoints) {

				tickLabel.ddPoints = [];

			}

			if (tickLabel.levelNumber !== series.options._levelNumber) {

				tickLabel.ddPoints.length = 0; // reset

			}

		}				

		

		if (point.drilldown) {

			

			// Add the click event to the point 

			H.addEvent(point, 'click', function () {

				point.doDrilldown();

			});

			/*wrap(point, 'importEvents', function (proceed) { // wrapping importEvents makes point.click event work

				if (!this.hasImportedEvents) {

					proceed.call(this);

					H.addEvent(this, 'click', function () {

						this.doDrilldown();

					});

				}

			});*/



			// Make axis labels clickable

			if (tickLabel) {

				if (!tickLabel.basicStyles) {

					tickLabel.basicStyles = H.merge(tickLabel.styles);

				}

				tickLabel

					.addClass('highcharts-drilldown-axis-label')

					.css(chart.options.drilldown.activeAxisLabelStyle)

					.on('click', function () {

						series.xAxis.drilldownCategory(x);

					});

				

				tickLabel.ddPoints.push(point);

				tickLabel.levelNumber = series.options._levelNumber;

					

			}

		} else if (tickLabel && tickLabel.basicStyles && tickLabel.levelNumber !== series.options._levelNumber) {

			tickLabel.styles = {}; // reset for full overwrite of styles

			tickLabel.css(tickLabel.basicStyles);

			tickLabel.on('click', null); // #3806			

		}



		return point;

	});



	wrap(H.Series.prototype, 'drawDataLabels', function (proceed) {

		var css = this.chart.options.drilldown.activeDataLabelStyle;



		proceed.call(this);



		each(this.points, function (point) {

			if (point.drilldown && point.dataLabel) {

				point.dataLabel

					.attr({

						'class': 'highcharts-drilldown-data-label'

					})

					.css(css)

					.on('click', function () {

						point.doDrilldown();

					});

			}

		});

	});



	// Mark the trackers with a pointer 

	var type, 

		drawTrackerWrapper = function (proceed) {

			proceed.call(this);

			each(this.points, function (point) {

				if (point.drilldown && point.graphic) {

					point.graphic

						.attr({

							'class': 'highcharts-drilldown-point'

						})

						.css({ cursor: 'pointer' });

				}

			});

		};

	for (type in seriesTypes) {

		if (seriesTypes[type].prototype.supportsDrilldown) {

			wrap(seriesTypes[type].prototype, 'drawTracker', drawTrackerWrapper);

		}

	}

		

}(Highcharts));

