
Utils = {
	getUnixTimestamp : function(d) { return Math.round(d.getTime() / 1000); },
	getDateFromUnix : function(t) { return new Date(t*1000); }
}


function XAxis(canvas, context) {
	
	this._cv = canvas;
	this._cx = context;
	this._w = this._cv.width;
}

XAxis.prototype._x = 0;
XAxis.prototype._w = 0;
XAxis.prototype._end = null;
XAxis.prototype._start = null;

XAxis.prototype.getX = function() { return this._x; }
XAxis.prototype.getWidth = function() { return this._w; }
XAxis.prototype.setPosition = function(x) { this._x = x; }
XAxis.prototype.setWidth = function(w) { this._w = w; }
XAxis.prototype.getCoordinate = function(d) { return (((d - this._start)/(this._end - this._start)) * this._w) + this._x; }
XAxis.prototype.getRange = function() { return { start : this._start, end : this._end }; }

XAxis.prototype.addDataSet = function(ds) {
	var ds = ds || [];
		ds = (ds instanceof Array) ? ds : [ds];
	
	var thisDomain;
	for(var i=0; i<ds.length; i++) {
		thisDomain = ds[i].getDomain();
		
		this._start = (this._start == null) ? thisDomain[0] : Math.min(thisDomain[0], this._start);
		this._end = (this._end == null) ? thisDomain[1] : Math.max(thisDomain[1], this._end);
	}
}


function YAxis(canvas, context) {
	
	this._cv = canvas;
	this._cx = context;
	this._h = this._cv.height;
}

YAxis.prototype._y = 0;
YAxis.prototype._h = 0;
YAxis.prototype._end = null;
YAxis.prototype._start = null;

YAxis.prototype.getY = function() { return this._y; }
YAxis.prototype.getHeight = function() { return this._h; }
YAxis.prototype.setPosition = function(y) { this._y = y; }
YAxis.prototype.setHeight = function(h) { this._h = h; }
YAxis.prototype.getCoordinate = function(v) { return this._h - (((v - this._start)/(this._end - this._start)) * this._h) + this._y; }
YAxis.prototype.getRange = function() { return { start : this._start, end : this._end }; }

YAxis.prototype.addDataSet = function(ds) {
	var ds = ds || [];
		ds = (ds instanceof Array) ? ds : [ds];
	
	var thisRange;
	for(var i=0; i<ds.length; i++) {
		thisRange = ds[i].getRange();
		
		this._start = (this._start == null) ? thisRange[0] : Math.min(thisRange[0], this._start);
		this._end = (this._end == null) ? thisRange[1] : Math.max(thisRange[1], this._end);
	}
}

function DataSet() {
	this._points = [];
}

DataSet.prototype.get = function() { return this._points; }

DataSet.prototype.addDataPoint = function(d,v) {
	
	var datapoints = (arguments.length == 1 && (arguments[0] instanceof Array))
		? arguments[0]
		: [{d:d,v:v}];
	
	this._points = this._points.concat(datapoints);
	this._points.sort(this._sortPoints);
}

DataSet.prototype._sortPoints = function(a,b) {
	var valA = a.d,
		valB = b.d;
	
	return (valA - valB);
}

DataSet.prototype.getRange = function() {
	
	var points = this._points;
	var min = null, max = null;
	for(var i=0; i<points.length; i++) {
		min = (min == null) ? points[i].v : Math.min(min, points[i].v);
		max = (max == null) ? points[i].v : Math.max(max, points[i].v);
	}
	
	return [min,max];
}

DataSet.prototype.getDomain = function() {
	var points = this._points;
	var min = null, max = null;
	for(var i=0; i<points.length; i++) {
		min = (min == null) ? points[i].t : Math.min(min, points[i].t);
		max = (max == null) ? points[i].t : Math.max(max, points[i].t);
	}
	
	return [min,max];
}


/***********************************************
 * 
 * Experience Manager
 * 
 */


function ExperienceManager(data) {
	
	this.__construct = function(){
		this._data = data;
		this._experienceSets = this.getWorkDataSets(data.work);
		this._experienceSets = this._experienceSets.concat(this.getEducationDataSets(data.education));
	}
	
	this.getExperienceSets = function() { return this._experienceSets; }
	
	this.getWorkDataSets = function(work) {
	
		var experienceSets = [], 
			thisWork, tags, thisES;
		
		for(var name in work) {
			
			thisWork = work[name];
			thisWork = (thisWork instanceof Array) ? thisWork : [thisWork];
			
			for (var i = 0; i < thisWork.length; i++) {
				
				thisES = new ExperienceSet(name);
				
				tags = thisWork[i].tags;
				for (var keyword in tags) {
					
					thisES.addTimeSeries(keyword, new TimeSeries(
						thisWork[i].date.start,
						thisWork[i].date.end || new Date(),
						tags[keyword]
					));
					
				}
				
				experienceSets.push(thisES);
			}
		}
		
		return experienceSets;
	}
	
	this.getEducationDataSets = function(education) {
	
		var experienceSets = [], 
			thisEducation, tags, thisES, courses, thisCourse, thisDate;
		
		for(var name in education) {
			
			thisEducation = education[name];
			courses = thisEducation.courses;
			
			for (var i = 0; i < courses.length; i++) {
				thisCourse = courses[i];
				thisDate = thisCourse.date;
				
				thisDate = (thisDate instanceof Array) ? thisDate : [thisDate];
				
				for (var j = 0; j < thisDate.length; j++) {
				
					thisES = new ExperienceSet(name);
					
					tags = thisCourse.tags;
					for (var keyword in tags) {
					
						thisES.addTimeSeries(keyword, new TimeSeries(
							thisDate[j].start, 
							thisDate[j].end || new Date(), 
							tags[keyword])
						);
						
					}
					
					experienceSets.push(thisES);
				}
			}
		}
		
		return experienceSets;
	}
	
	this.getDataSets = function() {
		
		var datasets = [], thisDS;
		var thisExperience, timeSeries;
		for(var i=0; i<this._experienceSets.length; i++) {
			
			thisExperience = this._experienceSets[i];
			timeSeries = thisExperience.getTimeSeries();
			
			for(var label in timeSeries) {
				thisDS = new DataSet();
				thisDS.addDataPoint(timeSeries[label].points);
				
				datasets.push(thisDS);
			}
			
		}
		return datasets;
		
	}
	
	this.__construct();
}

function ExperienceSet(name) {
	this._name = name;
	this._timeseries = {};
	
	this.addTimeSeries = function(label, ts) { 
		this._timeseries[label] = ts; 
	}
	
	this.getSlopeAtX = function(label, t) {
		var ts = this._timeseries[label];
		if(ts) {
			return this._timeseries[label].getSlopeAtX(t);
		}
		return 0;
	}
	
	this.getTimeSeries = function() {
		return this._timeseries;
	}
}

function TimeSeries(start, end, data) {
	
	var start = (start instanceof Date) ? start : new Date(start);
	var end = (end instanceof Date) ? end : new Date(end);
	
	this._regions = [];
	this.start = start;
	this.end = end;
	this.points = this._distributeTimeSeriesData(data);
}
TimeSeries.prototype.getStartTimestamp = function() { return Utils.getUnixTimestamp(this.start); }
TimeSeries.prototype.getEndTimestamp = function() { return Utils.getUnixTimestamp(this.end); }
TimeSeries.prototype._distributeTimeSeriesData = function(data) {
	var data = data || [];
		data = (data instanceof Array) ? data : [data, data];
	
	var unixEnd = Utils.getUnixTimestamp(this.end);
	var unixStart = Utils.getUnixTimestamp(this.start);
	
	var timeDelta = (unixEnd - unixStart)/(data.length-1);
	var ts = [];
	for(var i=0, t=unixStart; i<data.length; i++, t += timeDelta) {
		
		if(i< data.length-1) {
			this._regions.push({ 
				s : unixStart, 
				e : unixStart + timeDelta, 
				v : data[i] 
			});
		}
		
		ts.push({t:t, v:data[i]});
	}
	
	return ts;
}
TimeSeries.prototype.getSlopeAtX = function(t) {
	
	var thisRegion;
	for(var i=0; i<this._regions.length; i++) {
		thisRegion = this._regions[i];
		if(t>=thisRegion.s && t<thisRegion.e) {
			/*
			 * v is the percentage of time taken up, so divide by 100 to get the slope. 
			 * 100/100 would be a slope of 1 (for every 1 year in the job, gaining a year of experience for that tag)
			 */
			return (thisRegion.v/100); // 
		}
	}
	return 0;
}


function TimelineManager() { 

	this._timelines = [];
	this._ranges = []; 
	
	function Timeline() {
		this.End = 0;
		this.Ranges = [];
		
		this.addDateRange = function(range) {
			
			var newRange = {
				start : range.start || new Date(),
				end : range.end || new Date(),
				data : range.data
			};
			
			this.End = newRange.end;
			this.Ranges.push(newRange);
		}
	}
	
	
	this.addDateRange = function(start, end, data) {
		if(start && !(start instanceof Date)) { start = new Date(start); }
		if(end && !(end instanceof Date)) { end = new Date(end); }
		
		this._ranges.push({ start : start, end : end, data : data });
	}
	
	this.distribute = function() {
	
		var ranges = this._ranges;
		ranges.sort(function(a,b) {
			var aStart = a.start,
				bStart = b.start;
			
			return (aStart > bStart) ? 1 : (bStart > aStart) ? -1 : 0;
		})
		
		var timelines = this._timelines;
		var thisRange, thisTimeline;
	
		rangeLoop: for(var i=0; i<ranges.length; i++) {
			
			thisRange = ranges[i];
			timelineLoop : for(var j=0; j<timelines.length; j++) {
				
				thisTimeline = timelines[j];
				if(thisTimeline.End < thisRange.start) { //it can fit on this line;
					thisTimeline.addDateRange(thisRange);
					continue rangeLoop;
				}
			}
			
			//have to add another timeline
			thisTimeline = new Timeline();
			thisTimeline.addDateRange(thisRange);
			timelines.push(thisTimeline);
		}
	}
	
	this.getTimelineRanges = function() {
		
		var ranges = []; 
		var timelines = this._timelines;
		for(var i=0; i<timelines.length; i++) {
			ranges.push(timelines[i].Ranges);
		}
		
		return ranges;
	}
}



function Chart(data) { this._data = data; }

Chart.prototype._Height = 90;
Chart.prototype._TopPadding = 5;
Chart.prototype.init = function() {
	var headerWidth = $("#header").width();
	var timeline = $("#timeline");
		timeline.css({ 'margin-left' : headerWidth });
	var canvasWidth = timeline.width();
	
	var wrapper, canvas;
	wrapper = $.create("div", {id:"canvas-wrapper"},[
		canvas = $.create("canvas",{ width : canvasWidth, height : this._Height })
	]).css({ width : canvasWidth, height : this._Height });
	
	$("#timeline").append(wrapper);
	
	this._canvas = canvas.get(0);
	this._context = this._canvas.getContext("2d");
	
	var self = this;
	$(window).resize(function() { 
		wrapper.css({ 'width' : 'auto' });
		canvas.css({ 'width' : 'auto' });
		
		var newWidth = timeline.width();
		wrapper.css({ 'width' : newWidth});
		canvas.attr('width', newWidth);
		
		self.draw();
	});
	
	this.draw();
}

Chart.prototype._keywords = ['java'];
Chart.prototype.draw = function(keywords) {
	
	if(keywords) { this._keywords = keywords; }
	
	this._context.clearRect(0, 0, this._canvas.width, this._canvas.height);
	
	this._xaxis = new XAxis(this._canvas, this._context);
	this._yaxis = new YAxis(this._canvas, this._context);
	
	
	this._expMgr = new ExperienceManager(this._data);
	this._xaxis.addDataSet(this._expMgr.getDataSets());
	
	var barHeight = this.drawTimelines();
	
	this._yaxis.setPosition(this._TopPadding);
	this._yaxis.setHeight(this._Height - barHeight - this._TopPadding);
	
	this.drawGraphs(this._keywords);
	
	this.drawXAxis();
	
}

Chart.prototype.drawXAxis = function() {
	
	var cx = this._context,
		cv = this._canvas;
	
	var xaxis = this._xaxis,
		yaxis = this._yaxis;
	
	var yPos = yaxis.getY() + yaxis.getHeight();
	
	cx.strokeStyle = "#000000";
	cx.beginPath();
	cx.moveTo(xaxis.getX(), yPos);
	cx.lineTo(xaxis.getX() + xaxis.getWidth(), yPos);
	cx.stroke();
	
	var xRange = xaxis.getRange();
	var now = new Date();
	var currDate = new Date(now.getFullYear(),0,1,0,0,0);
	
	
	
	var xCoord = xaxis.getCoordinate(Utils.getUnixTimestamp(currDate));
	while(xCoord > 0) {
		
		cx.moveTo(xCoord, yPos-2);
		cx.lineTo(xCoord, yPos+2);
		cx.stroke();
		
		currDate.setYear(currDate.getFullYear() - 1);
		xCoord = xaxis.getCoordinate(Utils.getUnixTimestamp(currDate));
	}
	
}


Chart.prototype.drawTimelines = function() {
	var work = this._data.work,
		education = this._data.education;
	
	
	var mgr = new TimelineManager();
	
	var thisWork, thisDate;
	for(var name in work) {
		
		thisWork = work[name];
		thisWork = (thisWork instanceof Array) ? thisWork : [thisWork];
		for(var i=0; i<thisWork.length; i++) {
			
			thisDate = thisWork[i].date;
			mgr.addDateRange(thisDate.start, thisDate.end, thisWork[i]);
		}
	}
	
	var thisEducation, thisDate, courses, thisCourse;
	for(var name in education) {
		
		thisEducation = education[name];
		courses = thisEducation.courses;
		
		for (var i = 0; i < courses.length; i++) {
			thisCourse = courses[i];
			thisDate = thisCourse.date;
			
			thisDate = (thisDate instanceof Array) ? thisDate : [thisDate];
			for (var j = 0; j < thisDate.length; j++) {
				mgr.addDateRange(thisDate[j].start, thisDate[j].end, thisCourse);
			}
		}
	}
	
	mgr.distribute();
	
	var barDistance = 5;
	var ranges = mgr.getTimelineRanges();
	var thisRangeCollection;
	for(var i=0; i<ranges.length; i++) {
		this.drawRangeCollection(ranges[i], this._Height - (barDistance*(ranges.length-i)));
	}
	
	return barDistance*(ranges.length+1);
	
}

Chart.prototype._colorIterator = 0;
Chart.prototype._colors = ['#06799F','#216278','#024E68','#3AAACF','#61B4CF'];
Chart.prototype.drawRangeCollection = function(ranges,y) {
	
	var cx = this._context;
	
	var startX, endX, thisRange;
	var xaxis = this._xaxis;
	for(var i=0; i<ranges.length; i++) {
		
		thisRange = ranges[i];
		
		startX = xaxis.getCoordinate(Utils.getUnixTimestamp(thisRange.start));
		endX = xaxis.getCoordinate(Utils.getUnixTimestamp(thisRange.end));
		
		cx.strokeStyle = thisRange.data.color;
		cx.beginPath();
		cx.moveTo(startX,y);
		cx.lineTo(endX,y);
		cx.stroke();
	}
	
}

Chart.prototype.drawGraphs = function(keyword) {
	
	var range = this._xaxis.getRange(),
		start = range.start,
		end = range.end;
	
	var secondsPerDay = 60*60*24;
	var secondsPerMonth = secondsPerDay*30;
	
	var datasets = [];
	
	var keyword = (keyword instanceof Array) ? keyword : [keyword];
	var points, rollingVal, xs, ds, totalSlope;
	for (var k = 0; k < keyword.length; k++) {
	
		points = [];
		
		rollingVal = 0; //this will be the combined experienced in seconds
		xs = this._expMgr.getExperienceSets();
		for (var t = start; t <= end; t += secondsPerDay) {
		
			totalSlope = 0;
			for (var i = 0; i < xs.length; i++) {
				totalSlope += xs[i].getSlopeAtX(keyword[k], t);
			}
			
			rollingVal = (totalSlope * secondsPerDay) + rollingVal;
			points.push({
				t: t,
				v: rollingVal
			});
		}
		
		var ds = new DataSet();
			ds.addDataPoint(points);
		
		datasets.push(ds);
		this._yaxis.addDataSet(ds);
	}
	
	
	
	var fill = new LineFillGraph(this._canvas, this._context);
		fill.XAxis = this._xaxis;
		fill.YAxis = this._yaxis;
		
	for (var i = 0; i < datasets.length; i++) {
		fill.AddDataSet(datasets[i]);
	}
	fill.draw();
}



function LineFillGraph(canvas, context) {
	this._cv = canvas;
	this._cx = context;
}
LineFillGraph.prototype.XAxis = null;
LineFillGraph.prototype.YAxis = null;
LineFillGraph.prototype._Datasets = null;
LineFillGraph.prototype.AddDataSet = function(ds) {
	if(!this._Datasets) { this._Datasets = []; }
	this._Datasets.push(ds);
};
LineFillGraph.prototype.draw = function() {
	if(!this.XAxis) { throw new Error("XAxis is not defined. "); }
	if(!this.YAxis) { throw new Error("YAxis is not defined. "); }
	if(!this._Datasets) { throw new Error("Dataset is not defined. "); }
	
	var xaxis = this.XAxis,
		yaxis = this.YAxis,
		cx = this._cx;
	
	var lineColor = "rgb(0,0,0)",
		fillColor = "rgba(0, 91, 142, .3)";
	
	
	var thisDS, points, thisPoint, action;
	for(var i=0; i<this._Datasets.length; i++) {
		thisDS = this._Datasets[i];
		points = thisDS.get();
		
		if(!points.length) { continue; }
		
		/////////////////////////// Draw the fill
		cx.fillStyle = fillColor;
		cx.beginPath();
		cx.moveTo(xaxis.getX(), yaxis.getY() + yaxis.getHeight());
		for(var j=0; j<points.length; j++) {
			
			thisPoint = points[j];
			cx.lineTo(xaxis.getCoordinate(thisPoint.t), yaxis.getCoordinate(thisPoint.v))
		}
		cx.lineTo(xaxis.getX() + xaxis.getWidth(), yaxis.getY() + yaxis.getHeight())
		cx.fill();

		/////////////////////////// Draw the line
		cx.strokeStyle = lineColor;
		cx.beginPath();
		for(var j=0; j<points.length; j++) {
			
			thisPoint = points[j];
			(j == 0) 
				? cx.moveTo(xaxis.getCoordinate(thisPoint.t), yaxis.getCoordinate(thisPoint.v))
				: cx.lineTo(xaxis.getCoordinate(thisPoint.t), yaxis.getCoordinate(thisPoint.v));
		}
		
		if (points[0]) {
			cx.moveTo(xaxis.getCoordinate(points[0].t), yaxis.getCoordinate(points[0].v));
		}
		
		cx.stroke();

	}
}



