function Desc(args){
	this.cfg = $.extend({
		parent: null,
		time: 0,
		startLeft: true, // line starts at left side
		leftSide: false, // description is on right or left side
		text: '',
		textBaseline: 'top' // top | bottom | middle
	}, args);
	
	this.container;
	this.hasChanged = true;
	
	// ctor
	{
	}
	
	this.removeContainer = function(){
		if(!this.container)
			return;
		
		this.container.remove();
		delete this.context;
		delete this.container;
	}
	
	this.update = function(){
		if(this.hasChanged == false)
			return;
		
		var c = this.context;
		var pLeft = this.cfg.parent.cfg.left, pRight = this.cfg.parent.cfg.right;
		var rightDesc = pRight+40;
		var padding = 3;
		
		var css = {
			top: this.cfg.time+'px',
			width: this.cfg.parent.cfg.width+'px'
		};
		
		with(this.cfg){
			if(leftSide){ // description on left side of traces
				css.left = padding+'px';
				css['text-align'] = 'left';
				
				if(startLeft)
					css.width = pLeft-padding+'px';
				else
					css.width = pRight+'px';
			}
			else{ // desc on right side
				css.right = padding+'px';
				css['text-align'] = 'right';
				
				if(startLeft)
					css.width = (parent.cfg.width-pLeft-padding)+'px';
				else
					css.width = (parent.cfg.width-pRight-padding)+'px';
			}
		}
		
		this.container = $('<div>'+this.cfg.text+'</div>')
			.addClass('desc')
			.css(css)
			.appendTo(this.cfg.parent.scrollContainer);
		
		this.hasChanged = false;
		return this;
	}
}

function Trace(args){
	this.cfg = $.extend({
		parent: null,
		start: 0, // absolute
		end: 100, // relative
		length: 10, // time the pkg took to transfer
		startLeft: true,
		
		group: '',
		type: '',
		desc: {
			start: '', 
			end: '',
			top: ''
		}
	}, args);
	this.container;
	this.context;
	this.hasChanged = true;
	
	// ctor
	{
		this.container = $('<canvas width="'+this.cfg.parent.cfg.width+'" height="'+this.cfg.end+'"></canvas>')
			.css({top: this.cfg.start+'px'})
			.appendTo(this.cfg.parent.scrollContainer);
		this.context = this.container[0].getContext('2d');
	};
	
	
	
	
	this.removeContainer = function(){
		if(!this.container)
			return;
		
		this.container.remove();
		delete this.context;
		delete this.container;
	}
	
	this.setChanged = function(){
		this.hasChanged = true;
	}
	this.update = function(c){
		if(this.hasChanged == false || !(this.container))
			return;
		
		var c = this.context;
		var left = this.cfg.parent.cfg.left, right = this.cfg.parent.cfg.right;
		var rightDesc = right+40;
		
		c.save();
	
		c.fillStyle = this.cfg.parent.cfg.style[this.cfg.type].fillStyle;
		
		c.beginPath();
		if(this.cfg.startLeft){
			c.moveTo(left, 0);
			c.lineTo(left, this.cfg.length);
			c.lineTo(right, this.cfg.end);
			c.lineTo(right, this.cfg.end-this.cfg.length);
		}
		else{
			c.moveTo(left, this.cfg.end);
			c.lineTo(left, this.cfg.end-this.cfg.length);
			c.lineTo(right, 0);
			c.lineTo(right, this.cfg.length);
		}
		c.closePath();
		c.fill();
		
		
		
		
		// descriptions
		c.lineWidth = 0.5;
		c.lineCap = 'butt';
		c.strokeStyle = '#000';
		c.fillStyle = '#000';
		c.font = '12px sans-serif';
		
		
		for(var i in this.cfg.desc){
			if(this.cfg.desc[i].length == 0)
				return;
			
			switch(i){
				case 'start':
				case 'end':
					var y = (i == 'start') ? 0.5 : this.cfg.end+0.5;
					
					c.beginPath();
					if(i == 'start')
						if(this.cfg.startLeft)
							c.moveTo(left, y);
						else
							c.moveTo(right, y);
					else // end
						if(this.cfg.startLeft)
							c.moveTo(right, y);
						else
							c.moveTo(left, y);
						
					c.lineTo(rightDesc, y);
					c.stroke();
					
					c.textAlign = 'left';
					c.textBaseline = (i == 'start') ? 'top': 'bottom';
					c.fillText(this.cfg.desc[i], rightDesc, y);
					break;
				case 'top':
					c.save();
					c.textAlign = 'center';
					
					var rotate = Math.atan( (this.cfg.end-this.cfg.length) / (right-left) );
					if(this.cfg.startLeft){
						c.translate(
							Math.ceil(left + (right-left)/2), 
							Math.ceil((this.cfg.end-this.cfg.length)/2)
						);
					}
					else{
						c.translate(
							Math.ceil(left + (right-left)/2), 
							Math.ceil((this.cfg.end-this.cfg.length)/2)
						);
						rotate = rotate*-1;
					}
					//console.log(rotate * 180 / Math.PI);
					
					c.rotate(rotate);
					
					c.fillText(this.cfg.desc[i], 0, 0);
					
					c.restore();
					break;
			}
			
			
			
		}
		
		c.restore();
		this.hasChanged = false;
		return this;
	}
}

function Event(args){
	this.cfg = $.extend({
		parent: null,
		time: 0,
		callback: function(){},
	}, args);
	this.timeout = false;
	
	// ctor
	{
		
	}
	
	this.run = function(){
		with(this.cfg){
			if (callback && typeof(callback) === "function") {
				callback.call(this);
			}
		}
	}
	this.startDelay = function(delayMs){
		if(!this.timeout === false)
			this.stopDelay(); // remove old delay
		
		var self = this;
		this.timeout = window.setTimeout(function(){
			self.run();
		}, delayMs);
	}
	this.stopDelay = function(){
		if(this.timeout === false)
			return;
		window.clearTimeout(this.timeout);
		this.timeout = false;
	}
	
	//http://onemarco.com/2008/11/12/callbacks-and-binding-and-callback-arguments-and-references/
	/**
	* @param {Function} func the callback function
	* @param {Object} opts an object literal with the following
	* properties (all optional):
	* scope: the object to bind the function to (what the "this" keyword will refer to)
	* args: an array of arguments to pass to the function when it is called, these will be
	* appended after any arguments passed by the caller
	* suppressArgs: boolean, whether to supress the arguments passed
	* by the caller.  This default is false.
	*/
	function callback(func,opts){   
		var cb = function(){
			var args = opts.args ? opts.args : [];
			var scope = opts.scope ? opts.scope : this;
			var fargs = opts.supressArgs === true ?
				[] : toArray(arguments);
			func.apply(scope,fargs.concat(args));
		}
		return cb;
	}
	
	/* A utility function for callback() */
	function toArray(arrayLike){
		var arr = [];
		for(var i = 0; i < arrayLike.length; i++){
			arr.push(arrayLike[i]);
		}
		return arr;
	}
}

//http://onemarco.com/2008/11/12/callbacks-and-binding-and-callback-arguments-and-references/
/**
* @param {Function} func the callback function
* @param {Object} opts an object literal with the following
* properties (all optional):
* scope: the object to bind the function to (what the "this" keyword will refer to)
* args: an array of arguments to pass to the function when it is called, these will be
* appended after any arguments passed by the caller
* suppressArgs: boolean, whether to supress the arguments passed
* by the caller.  This default is false.
*/
Event.createCallback = function(func,opts){   
	var cb = function(){
		var args = opts.args ? opts.args : [];
		var scope = opts.scope ? opts.scope : this;
		var fargs = opts.supressArgs === true ?
			[] : Event.toArray(arguments);
		func.apply(scope,fargs.concat(args));
	}
	return cb;
}

/* A utility function for callback() */
Event.toArray = function(arrayLike){
	var arr = [];
	for(var i = 0; i < arrayLike.length; i++){
		arr.push(arrayLike[i]);
	}
	return arr;
}



function Tracer(args){
	this.cfg = $.extend({
		container: null,
		context: null,
		width: 500,
		height: 500,
		
		left: 20,
		right: 300,
		
		pxPerSec: 10,
		style: {
			ack: {
				fillStyle: '#006A00'
			},
			pkg: {
				fillStyle: '#00adef'
			}
		}
	}, args);
	this.traces;
	this.events;
	this.eventTimers;
	this.descs;
	
	this.scrollContainer;
	this.visibleContainer;
	this.gcInterval;
	this.hasChanged;
	this.endTime;
	
	this.reset = function(){
		this.hasChanged = true;
		this.traces = new Array();
		this.events = new Array();
		this.eventTimers = new Array();
		
		this.descs = new Array();
		this.endTime = 0; // the last event/desc/trace
		
		var $container = $(this.cfg.container);
		$container.children().remove();
		var t = $('<canvas width="'+this.cfg.width+'" height="'+this.cfg.height+'"></canvas>')
			.attr('class', 'background')
			.appendTo($container);
		this.cfg.context = t[0].getContext('2d');
		
		$container.css({
			width: this.cfg.width+'px',
			height: this.cfg.height+'px'
		});
		
		// generate scroll container
		this.visibleContainer = $('<div></div>')
			.attr('class', 'visible')
			.css({
				'height': this.cfg.height-100+'px',
				width: this.cfg.width+'px'
			})
			.appendTo($container);
		
		this.scrollContainer = $('<div></div>')
			.attr('class', 'scroll')
			.css({
				//'top': this.cfg.height-100+'px',
				'margin-top': this.cfg.height-100+'px',
				width: this.cfg.width+'px'
			})
			.appendTo(this.visibleContainer);
	};
	
	this.addTrace = function(args){
		var t = new Trace($.extend(args, {parent: this}));
		
		this.traces.push(t);
		
		this.setChanged();
		
		this.endTime = Math.max(this.endTime, t.cfg.start+t.cfg.end);
		return this;
	}
	
	this.addDesc = function(args){
		var t = $.extend(args, {parent: this});
		
		this.descs.push(new Desc(t));
		this.setChanged();
		
		this.endTime = Math.max(this.endTime, args.time);
		return this;
	}
	
	this.addEvent = function(args){
		var t = $.extend(args, {parent: this});
		
		this.events.push(new Event(t));
		this.setChanged();
		this.endTime = Math.max(this.endTime, args.time);
		return this;
	}
	
	this.setChanged = function(){
		this.hasChanged = true;
	}
	
	this.update = function(){
		if(this.hasChanged == false)
			return;
		
		var c = this.cfg.context;
		var width = 2, padding = 4;
		
		c.save();
		
		c.lineWidth = width;
		c.lineCap = 'butt';
		c.strokeStyle = '#000000';
		
		c.beginPath();
		c.moveTo(this.cfg.left-padding, 0);
		c.lineTo(this.cfg.left-padding, this.cfg.height);
		c.stroke();
		
		c.beginPath();
		c.moveTo(this.cfg.right+padding, 0);
		c.lineTo(this.cfg.right+padding, this.cfg.height);
		c.stroke();
		
		c.restore();
		
		// draw traces
		for(var i in this.traces){
			this.traces[i].update();
		}
		
		// draw descs
		for(var i in this.descs){
			this.descs[i].update();
		}
		
		
		this.hasChanged = false;
		return this;
	}
	
	this.startScrolling = function(untilTime){
		var currentTime = this.getCurrentTime(); // px
		var duration; // sec
		var dest; // px
		
		if(!untilTime){
			duration = 60*60*24;
			dest = currentTime + (pxPerSec * duration);
		}
		else{
			dest = untilTime;
			
			if(dest <= currentTime)
				return; // only scroll forward
			
			duration = (dest-currentTime)/pxPerSec;
		}
		
		with(this.cfg){
			var self = this;
			this.scrollContainer.animate({
				'top': (dest*-1)+'px',
			}, {
				duration: duration*1000,
				easing: 'linear',
				callback: function(){
					self.stopScrolling();
				}
			});
			
			// plan events
			for(var i in this.events){
				var e = this.events[i];
				if(e.cfg.time < currentTime)
					continue;
				
				e.startDelay(1000/pxPerSec*(e.cfg.time-currentTime));
			}
			
			// plan garbage collecting if the scroll area has scrolled the full height of the outer container
			var self = this;
			this.gcInterval = window.setInterval(function(){
				console.log('gc');
				self.gc();
			}, height/pxPerSec*1000);
			console.log(height/pxPerSec*1000);
		}
	}
	this.stopScrolling = function(){
		this.scrollContainer.stop(true, false);
		
		// stop events
		for(var i in this.events){
			var e = this.events[i];
			e.stopDelay();
		}
		
		// stop gc
		if(this.gcInterval){
			window.clearInterval(this.gcInterval);
			delete this.gcInterval;
		}
	}
	
	this.getCurrentTime = function(){
		return parseInt(this.scrollContainer.css('top').trimRight(2))*-1;
	}
	
	this.getEndTime = function(){
		return this.endTime;
	}
	
	this.callEvents = function(time){
		if(!this.events[time])
			return;
		
		for(var i in this.events[time]){
			this.events[time][i].push(new Event(t));
		}
	}
	
	this.gc = function(){
		// remove all traces that are out of sight
		var lastTime = this.getCurrentTime() - this.cfg.height; // y of top time; all traces that end before can be removed
		
		var t;
		for(var i in this.traces){
			t = this.traces[i];
			
			if(t.cfg.start + t.cfg.end < lastTime)
				t.removeContainer();
		}
	}
	
	
	// ctor
	{
		this.reset();
	}
	
	return this;
}