/**
 * $media.timeline (3.1.1)
 *
 * Require:
 * $media 2.x
 *
 * 2012. Created by Oscar Otero (http://oscarotero.com / http://anavallasuiza.com)
 *
 * $media.timeline is released under the GNU Affero GPL version 3.
 * More information at http://www.gnu.org/licenses/agpl-3.0.html
 */


(function ($) {
	'use strict';

	//Private function to sort the parameters of an object
	var sortObject = function (o) {
		var sorted = {}, key, keys = [];

		for (key in o) {
			if (o.hasOwnProperty(key)) {
				keys.push(key);
			}
		}

		keys.sort(function (a, b) {
			return a - b;
		});

		for (key = 0; key < keys.length; ++key) {
			sorted[keys[key]] = o[keys[key]];
		}

		return sorted;
	};


	//Point constructor
	window.$media.Point = function (settings) {
		settings = settings || {};
		this.data = settings.data || {};
		this.time = settings.time;

		if ($.isFunction(settings.fn)) {
			this.fn = settings.fn;
		} else {
			$.error('fn is not a valid function');
		}

		if (!$.isArray(this.time)) {
			this.time = [this.time, this.time];
		}

		if ($.isFunction(settings.fn_out)) {
			this.fn_out = settings.fn_out;
		} else if (settings.fn_out) {
			$.error('fn_out is not a valid function');
		} else if (this.time[1] === undefined) {
			this.time[1] = this.time[0];
		}

		this.relative = (this.time.join().indexOf('%') === -1) ? false : true;
	};

	window.$media.Point.prototype = {

		/**
		 * Sets the start and end of the point based in the media object
		 *
		 * @param media The $media instance
		 */
		updateTime: function (media) {
			if (!media) {
				return;
			}

			if (this.time[0] === this.time[1]) {
				this.start = this.end = media.time(this.time[0]).secondsTo('ms');
			} else {
				this.start = media.time(this.time[0]).secondsTo('ms');
				this.end = media.time(this.time[1]).secondsTo('ms');
			}

			this.data.time = [this.start/1000, this.end/1000];
		},


		/**
		 * Executes the "in" function of the point
		 *
		 * @param thisArg The value of the variable "this"
		 * @param array args The arguments passed to the function
		 *
		 * @return boolean True is the function has executed, false if not
		 */
		execute: function (thisArg, args) {
			if (this.waiting) {
				return false;
			}

			this.fn.apply(thisArg, (args || []));

			if (this.fn_out) {
				this.waiting = true;
			}

			return true;
		},

		
		/**
		 * Executes the "out" function of the point
		 *
		 * @param thisArg The value of the variable "this"
		 * @param array args The arguments passed to the function
		 *
		 * @return boolean True is the function has executed, false if not
		 */
		executeOut: function (thisArg, args) {
			if (!this.waiting || !this.fn_out) {
				this.waiting = false;

				return false;
			}

			this.fn_out.apply(thisArg, (args || []));

			this.waiting = false;
		}
	};


	//Timeline constructor
	window.$media.Timeline = function (data) {
		this.data = data || {};
		this.points = {};
		this.enabled = false;
	};


	window.$media.Timeline.prototype = {

		/**
		 * Saves the points into the timeline
		 *
		 * @param media The $media instance related with the timeline
		 * @param points Array with the points to save.
		 */
		savePoints: function (media, points) {
			if (!media || !points || !points.length) {
				return;
			}

			var percent = [], duration = media.duration(), i, length, point;

			for (i = 0, length = points.length; i < length; ++i) {
				point = points[i];

				if (!duration && (point.relative === true)) {
					percent.push(point);
					continue;
				}

				point.updateTime(media);

				if (this.points[point.start] === undefined) {
					this.points[point.start] = [point];
				} else {
					this.points[point.start].push(point);
				}
			}

			if (percent.length) {
				var that = this;

				media.readyState(1, function () {
					that.savePoints(this, percent);

					if (that.enabled) {
						this.refreshTimeline();
					}
				});
			}

			if (this.enabled) {
				media.refreshTimeline();
			}
		},


		/**
		 * Creates one or more points and saves in the timeline
		 *
		 * @param media The $media instance related with the timeline
		 * @param points Array with the settings for the points
		 *
		 * @return this
		 */
		addPoints: function (media, points) {
			var newPoints = [], i, length = points.length;

			for (i = 0; i < length; ++i) {
				newPoints.push(new window.$media.Point(points[i]));
			}

			this.savePoints(media, newPoints);

			return this;
		}
	};



	//Extends $media class
	window.$media.extend({

		/**
		 * Creates a new timeline
		 *
		 * @param string name The name for the timeline
		 * @param object options The settings for the timeline
		 *
		 * @return this
		 */
		createTimeline: function (name, options) {
			options = options || {};

			if (!this.timeline || !this.timelines) {
				this.timelines = {};
				this.timeline = {
					points: [],
					inPoints: [],
					outPoints: [],
					timeout: 0
				};

				this.on('play seeked', function () {
					this.startTimeline();
				}).on('seeking', function (event) {
					var length = this.timeline.outPoints.length;

					if (length) {
						var ms = this.time().secondsTo('ms'), k;

						for (k = 0; k < length; k++) {
							var point = this.timeline.outPoints[k];

							if (point && (ms < point.start || ms > point.end)) {
								point.executeOut(this, [point.data, this.timelines[point.timelineName].data]);
								this.timeline.outPoints.splice(k, 1);
							}
						}
					}
				});
			}

			this.timelines[name] = new window.$media.Timeline(options.data);

			if (options.points) {
				this.addTimelinePoints(name, options.points);
			}

			if (options.enabled) {
				this.enableTimeline(name);
			}

			return this;
		},


		/**
		 * Sets more
		 *
		 * Gets a timeline
		 */
		addTimelinePoints: function (name, points) {
			if (!this.timelineExists(name)) {
				return this;
			}

			if (!$.isArray(points)) {
				points = [points];
			}
			
			this.timelines[name].addPoints(this, points);

			return this;
		},


		/**
		 * function removeTimeline (name)
		 *
		 * Remove a timeline
		 */
		removeTimeline: function (name) {
			if (!this.timelineExists(name)) {
				return this;
			}
			
			if (this.timelines[name].enabled) {
				delete this.timelines[name];
				this.refreshTimeline();
			} else {
				delete this.timelines[name];
			}

			return this;
		},


		/**
		 * function enableDisableTimelines (object timelines)
		 *
		 * Enables and disables various timelines
		 */
		enableTimeline: function (name) {
			if (!this.timelineExists(name) || this.timelineIsEnabled(name)) {
				return this;
			}

			this.timelines[name].enabled = true;
			
			if (!$.isEmptyObject(this.timelines[name].points)) {
				this.refreshTimeline();
			}

			return this;
		},


		/**
		 * function enableDisableTimelines (object timelines)
		 *
		 * Enables and disables various timelines
		 */
		disableTimeline: function (name) {
			if (!this.timelineIsEnabled(name)) {
				return this;
			}

			this.timelines[name].enabled = false;

			if (!$.isEmptyObject(this.timelines[name].points)) {
				this.refreshTimeline();
			}

			return this;
		},



		/**
		 * function enableDisableTimelines (object timelines)
		 *
		 * Enables and disables various timelines
		 */
		enableDisableTimelines: function (timelines) {
			if (!timelines || !this.timelines) {
				return this;
			}

			var refresh = false, name;

			for (name in timelines) {
				if (timelines.hasOwnProperty(name)) {
					var timeline = this.timelines[name];

					if (timeline) {
						var enable = timelines[name] ? true : false;

						if (timeline.enabled !== enable) {
							timeline.enabled = enable;
							refresh = true;
						}
					}
				}
			}

			if (refresh) {
				this.refreshTimeline();
			}

			return this;
		},

		timelineExists: function (name) {
			if (!name || !this.timelines || !this.timelines[name]) {
				return false;
			}

			return true;
		},

		timelineIsEnabled: function (name) {
			if (this.timelineExists(name) && this.timelines[name].enabled) {
				return true;
			}

			return false;
		},


		/**
		 * function refreshTimeline ()
		 *
		 * Set the points array
		 */
		refreshTimeline: function () {
			var points = {}, name, timeline, timelinePoints, ms, k, point, length;

			this.timeline.points = [];

			for (name in this.timelines) {
				if (this.timelines.hasOwnProperty(name)) {
					timeline = this.timelines[name];

					if (!timeline.enabled) {
						continue;
					}

					timelinePoints = timeline.points;

					for (ms in timelinePoints) {
						if (timelinePoints.hasOwnProperty(ms)) {
							if (points[ms] === undefined) {
								points[ms] = [];
							}

							for (k = 0, length = timelinePoints[ms].length; k < length; k++) {
								point = timelinePoints[ms][k];
								point.timelineName = name;
								points[ms].push(point);
							}
						}
					}
				}
			}

			points = sortObject(points);

			for (ms in points) {
				if (points.hasOwnProperty(ms)) {
					for (k = 0, length = points[ms].length; k < length; k++) {
						this.timeline.points.push(points[ms][k]);
					}
				}
			}

			this.startTimeline();

			return this;
		},


		/**
		 * function getPoints (from)
		 *
		 * Get the timeline points from any time to the end
		 */
		getPoints: function (from) {
			if (from === undefined) {
				from = 0;
			}

			from = from.secondsTo('ms');

			var points = [], k, length = this.timeline.points.length;

			for (k = 0; k < length; k++) {
				if (from <= this.timeline.points[k].end) {
					points.push(this.timeline.points[k]);
				}
			}

			return points;
		},



		/**
		 * function startTimeline ()
		 *
		 * Execute the timeline functions
		 */
		startTimeline: function () {
			if (!this.timeline || (!this.timeline.points.length && !this.timeline.inPoints.length && !this.timeline.outPoints.length)) {
				return;
			}

			this.timeline.inPoints = this.getPoints(this.time());

			this.timelineTimeout();

			return this;
		},



		/**
		 * function timelineTimeout ()
		 *
		 * Function to execute on timeOut
		 */
		timelineTimeout: function () {
			if (!this.timeline || (!this.timeline.inPoints.length && !this.timeline.outPoints.length)) {
				return;
			}

			var ms = this.time().secondsTo('ms');
			var total_ms = this.duration().secondsTo('ms');
			var point, k, length;

			//Execute "out" functions
			length = this.timeline.outPoints.length;

			if (length) {
				for (k = 0; k < length; k++) {
					point = this.timeline.outPoints[k];

					if (point && (ms < point.start || ms > point.end || !this.timelines[point.timelineName].enabled)) {
						point.executeOut(this, [point.data, this.timelines[point.timelineName].data]);
						this.timeline.outPoints.splice(k, 1);
					}
				}
			}

			//Execute "in" functions
			while (this.timeline.inPoints && this.timeline.inPoints[0] && this.timeline.inPoints[0].start <= ms && this.timeline.inPoints[0].start < total_ms) {
				point = this.timeline.inPoints.shift();

				if (point.execute(this, [point.data, this.timelines[point.timelineName].data]) && point.waiting) {
					this.timeline.outPoints.push(point);
				}
			}

			//Create other timeout
			if (!this.playing() || this.element.seeking || (!this.timeline.inPoints.length && !this.timeline.outPoints.length)) {
				return;
			}

			var new_ms = 0;

			if (this.timeline.inPoints[0]) {
				new_ms = this.timeline.inPoints[0].start;
			}

			length = this.timeline.outPoints.length;

			if (length) {
				for (k = 0; k < length; k++) {
					if (!new_ms || this.timeline.outPoints[k].end < new_ms) {
						new_ms = this.timeline.outPoints[k].end;
					}
				}
			}
			
			new_ms = new_ms - ms;

			if (new_ms < 20) {
				new_ms = 20;
			}

			clearTimeout(this.timeline.timeout);
			this.timeline.timeout = setTimeout($.proxy(this.timelineTimeout, this), new_ms);
		}
	});
})(jQuery);