/**
 * @license Highcharts JS v2.2.5 (2012-06-08)
 * Prototype adapter
 *
 * @author Michael Nelson, Torstein Hønsi.
 *
 * Feel free to use and modify this script.
 * Highcharts license: www.highcharts.com/license.
 */

// JSLint options:
/*global Effect, Class, Event, Element, $, $$, $A */

// Adapter interface between prototype and the Highcharts charting library
var HighchartsAdapter = (function () {

var hasEffect = typeof Effect !== 'undefined';

return {

	/**
	 * Initialize the adapter. This is run once as Highcharts is first run.
	 * @param {Object} pathAnim The helper object to do animations across adapters.
	 */
	init: function (pathAnim) {
		if (hasEffect) {
			/**
			 * Animation for Highcharts SVG element wrappers only
			 * @param {Object} element
			 * @param {Object} attribute
			 * @param {Object} to
			 * @param {Object} options
			 */
			Effect.HighchartsTransition = Class.create(Effect.Base, {
				initialize: function (element, attr, to, options) {
					var from,
						opts;

					this.element = element;
					this.key = attr;
					from = element.attr ? element.attr(attr) : $(element).getStyle(attr);

					// special treatment for paths
					if (attr === 'd') {
						this.paths = pathAnim.init(
							element,
							element.d,
							to
						);
						this.toD = to;


						// fake values in order to read relative position as a float in update
						from = 0;
						to = 1;
					}

					opts = Object.extend((options || {}), {
						from: from,
						to: to,
						attribute: attr
					});
					this.start(opts);
				},
				setup: function () {
					HighchartsAdapter._extend(this.element);
					// If this is the first animation on this object, create the _highcharts_animation helper that
					// contain pointers to the animation objects.
					if (!this.element._highchart_animation) {
						this.element._highchart_animation = {};
					}

					// Store a reference to this animation instance.
					this.element._highchart_animation[this.key] = this;
				},
				update: function (position) {
					var paths = this.paths,
						element = this.element,
						obj;

					if (paths) {
						position = pathAnim.step(paths[0], paths[1], position, this.toD);
					}

					if (element.attr) { // SVGElement
						element.attr(this.options.attribute, position);
					
					} else { // HTML, #409
						obj = {};
						obj[this.options.attribute] = position;
						$(element).setStyle(obj);
					}
					
				},
				finish: function () {
					// Delete the property that holds this animation now that it is finished.
					// Both canceled animations and complete ones gets a 'finish' call.
					delete this.element._highchart_animation[this.key];
				}
			});
		}
	},
	
	/**
	 * Run a general method on the framework, following jQuery syntax
	 * @param {Object} el The HTML element
	 * @param {String} method Which method to run on the wrapped element
	 */
	adapterRun: function (el, method) {
		
		// This currently works for getting inner width and height. If adding
		// more methods later, we need a conditional implementation for each.
		return parseInt($(el).getStyle(method), 10);
		
	},

	/**
	 * Downloads a script and executes a callback when done.
	 * @param {String} scriptLocation
	 * @param {Function} callback
	 */
	getScript: function (scriptLocation, callback) {
		var head = $$('head')[0]; // Returns an array, so pick the first element.
		if (head) {
			// Append a new 'script' element, set its type and src attributes, add a 'load' handler that calls the callback
			head.appendChild(new Element('script', { type: 'text/javascript', src: scriptLocation}).observe('load', callback));
		}
	},

	/**
	 * Custom events in prototype needs to be namespaced. This method adds a namespace 'h:' in front of
	 * events that are not recognized as native.
	 */
	addNS: function (eventName) {
		var HTMLEvents = /^(?:load|unload|abort|error|select|change|submit|reset|focus|blur|resize|scroll)$/,
			MouseEvents = /^(?:click|mouse(?:down|up|over|move|out))$/;
		return (HTMLEvents.test(eventName) || MouseEvents.test(eventName)) ?
			eventName :
			'h:' + eventName;
	},

	// el needs an event to be attached. el is not necessarily a dom element
	addEvent: function (el, event, fn) {
		if (el.addEventListener || el.attachEvent) {
			Event.observe($(el), HighchartsAdapter.addNS(event), fn);

		} else {
			HighchartsAdapter._extend(el);
			el._highcharts_observe(event, fn);
		}
	},

	// motion makes things pretty. use it if effects is loaded, if not... still get to the end result.
	animate: function (el, params, options) {
		var key,
			fx;

		// default options
		options = options || {};
		options.delay = 0;
		options.duration = (options.duration || 500) / 1000;
		options.afterFinish = options.complete;

		// animate wrappers and DOM elements
		if (hasEffect) {
			for (key in params) {
				// The fx variable is seemingly thrown away here, but the Effect.setup will add itself to the _highcharts_animation object
				// on the element itself so its not really lost.
				fx = new Effect.HighchartsTransition($(el), key, params[key], options);
			}
		} else {
			if (el.attr) { // #409 without effects
				for (key in params) {
					el.attr(key, params[key]);
				}
			}
			if (options.complete) {
				options.complete();
			}
		}

		if (!el.attr) { // HTML element, #409
			$(el).setStyle(params);
		}
	},

	// this only occurs in higcharts 2.0+
	stop: function (el) {
		var key;
		if (el._highcharts_extended && el._highchart_animation) {
			for (key in el._highchart_animation) {
				// Cancel the animation
				// The 'finish' function in the Effect object will remove the reference
				el._highchart_animation[key].cancel();
			}
		}
	},

	// um.. each
	each: function (arr, fn) {
		$A(arr).each(fn);
	},

	/**
	 * Get the cumulative offset relative to the top left of the page. This method, unlike its
	 * jQuery and MooTools counterpart, still suffers from issue #208 regarding the position
	 * of a chart within a fixed container.
	 */
	offset: function (el) {
		return $(el).cumulativeOffset();
	},

	// fire an event based on an event name (event) and an object (el).
	// again, el may not be a dom element
	fireEvent: function (el, event, eventArguments, defaultFunction) {
		if (el.fire) {
			el.fire(HighchartsAdapter.addNS(event), eventArguments);
		} else if (el._highcharts_extended) {
			eventArguments = eventArguments || {};
			el._highcharts_fire(event, eventArguments);
		}

		if (eventArguments && eventArguments.defaultPrevented) {
			defaultFunction = null;
		}

		if (defaultFunction) {
			defaultFunction(eventArguments);
		}
	},

	removeEvent: function (el, event, handler) {
		if ($(el).stopObserving) {
			if (event) {
				event = HighchartsAdapter.addNS(event);
			}
			$(el).stopObserving(event, handler);
		} if (window === el) {
			Event.stopObserving(el, event, handler);
		} else {
			HighchartsAdapter._extend(el);
			el._highcharts_stop_observing(event, handler);
		}
	},
	
	washMouseEvent: function (e) {
		return e;
	},

	// um, grep
	grep: function (arr, fn) {
		return arr.findAll(fn);
	},

	// um, map
	map: function (arr, fn) {
		return arr.map(fn);
	},

	// deep merge. merge({a : 'a', b : {b1 : 'b1', b2 : 'b2'}}, {b : {b2 : 'b2_prime'}, c : 'c'}) => {a : 'a', b : {b1 : 'b1', b2 : 'b2_prime'}, c : 'c'}
	/*merge: function(){
		function doCopy(copy, original) {
			var value,
				key,
				undef,
				nil,
				same,
				obj,
				arr,
				node;

			for (key in original) {
				value = original[key];
				undef = typeof(value) === 'undefined';
				nil = value === null;
				same = original === copy[key];

				if (undef || nil || same) {
					continue;
				}

				obj = typeof(value) === 'object';
				arr = value && obj && value.constructor == Array;
				node = !!value.nodeType;

				if (obj && !arr && !node) {
					copy[key] = doCopy(typeof copy[key] == 'object' ? copy[key] : {}, value);
				}
				else {
					copy[key] = original[key];
				}
			}
			return copy;
		}

		var args = arguments, retVal = {};

		for (var i = 0; i < args.length; i++) {
			retVal = doCopy(retVal, args[i]);
		}

		return retVal;
	},*/
	merge: function () { // the built-in prototype merge function doesn't do deep copy
		function doCopy(copy, original) {
			var value, key;

			for (key in original) {
				value = original[key];
				if (value && typeof value === 'object' && value.constructor !== Array &&
						typeof value.nodeType !== 'number') {
					copy[key] = doCopy(copy[key] || {}, value); // copy

				} else {
					copy[key] = original[key];
				}
			}
			return copy;
		}

		function merge() {
			var args = arguments,
				i,
				retVal = {};

			for (i = 0; i < args.length; i++) {
				retVal = doCopy(retVal, args[i]);

			}
			return retVal;
		}

		return merge.apply(this, arguments);
	},

	// extend an object to handle highchart events (highchart objects, not svg elements).
	// this is a very simple way of handling events but whatever, it works (i think)
	_extend: function (object) {
		if (!object._highcharts_extended) {
			Object.extend(object, {
				_highchart_events: {},
				_highchart_animation: null,
				_highcharts_extended: true,
				_highcharts_observe: function (name, fn) {
					this._highchart_events[name] = [this._highchart_events[name], fn].compact().flatten();
				},
				_highcharts_stop_observing: function (name, fn) {
					if (name) {
						if (fn) {
							this._highchart_events[name] = [this._highchart_events[name]].compact().flatten().without(fn);
						} else {
							delete this._highchart_events[name];
						}
					} else {
						this._highchart_events = {};
					}
				},
				_highcharts_fire: function (name, args) {
					(this._highchart_events[name] || []).each(function (fn) {
						// args is never null here
						if (args.stopped) {
							return; // "throw $break" wasn't working. i think because of the scope of 'this'.
						}

						// Attach a simple preventDefault function to skip default handler if called
						args.preventDefault = function () {
							args.defaultPrevented = true;
						};

						// If the event handler return false, prevent the default handler from executing
						if (fn.bind(this)(args) === false) {
							args.preventDefault();
						}
					}
.bind(this));
				}
			});
		}
	}
};
}());
