// Configuration options for JSHint
// Install node.js, run "sudo npm install jshint -g", then "jshint DLSummaryView.js".

/*jshint globalstrict:true, smarttabs:true, undef:true, unused:true, maxcomplexity: 5 */
/*global d3, filter, clearTimeout, setTimeout */
/*exported addSummary, addPipeline, allPipelinesAdded, updatePipeline, addHistogram, updateExistingHistogram, addTimeSeries, allTimeSeriesAdded, updateExistingTimeSeries, reset */

"use strict";

// Shared

// Format using SI suffixs to 3 precision
var siFormat = d3.format(".3s");

// Only format numbers big enough that they'll be cut off otherwise
function formatCount(c) {
	if (c < 10000) return c;
	return siFormat(c);
}

// Summary

function addSummary(label, value) {
	
	var summary = d3.select("#summary");
	
	summary.append("dt")
		.attr("class", "chart-title")
		.text(label);
	
	summary.append("dd")
		.text(value);
}

var histogramMetrics = {
	width: 300,
	height: 100,
	barHeight: 12,
	interBarSpacing: 3,
	labelMargin: 120,
	valueMargin: 36,
	barPadding: 2,
	barWidth: function () { return this.width - this.labelMargin - this.valueMargin - this.barPadding * 2; }
};

// Pipelines

var pipelinesToDraw = [],
	byPipelineCounts = [],
	pipelineUpdates = {},
	pipelineScale = null; // The shared scale used across the top rows of pipeline charts

function addPipeline(id, data) {
	
	// Stash by pipeline value for scale generation later
	byPipelineCounts.push(data[0].value);
	
	var pipeline;
	
	pipelinesToDraw.push( function(x) {
						 
		pipeline = d3.select("#pipelines").append("svg")
			.attr("class", "chart")
			.attr("width", histogramMetrics.width)
			.attr("height", data.length * (histogramMetrics.barHeight + histogramMetrics.interBarSpacing) + histogramMetrics.barHeight);

		drawHistogramBars(pipeline, x, data, "bar", false, true);
	});
	
	pipelineUpdates[id] = function(scale, newdata, subdata) {
		
		var secondScale = drawHistogramBars(pipeline, scale, newdata, "bar", false, true);
		
		if(subdata === null) return;
		drawHistogramBars(pipeline, scale, subdata, "subbar", false, false, secondScale);
	};
}

function allPipelinesAdded() {
	
	d3.select("#pipelines").append("h2")
		.attr("class", "chart-title")
		.text("Pipelines");
	
	// generate the pipeline scale
	pipelineScale = d3.scale.linear()
		.domain([0, d3.max(byPipelineCounts)])
		.rangeRound([0, histogramMetrics.barWidth()]);
	
	pipelinesToDraw.forEach( function(p) {
		p(pipelineScale);
	});
}

function updatePipeline(id, new_data, subdata) {
	if(filtered === false) return;
	
	pipelineUpdates[id](pipelineScale, new_data, subdata);
}

// Histograms

var histogramUpdates = {};

function addHistogram(id, label, originalData, collapse) {
	
	var needsCollapse = false;
	
	if (collapse !== undefined) {
		needsCollapse = collapse;
	} else {
		needsCollapse = (originalData.length > 6);		
	}
	
	var data = needsCollapse ? collapseHistogramData(originalData, 5) : originalData;
	
	var x = d3.scale.linear()
		.domain([0, d3.max(data, function(d) { return d.value; })])
		.rangeRound([0, histogramMetrics.barWidth()]);
		
	var histogram = d3.select("#" + id);
	if(histogram.empty()) {
		histogram = d3.select("#histograms").append("div")
			.attr("id", id)
			.attr("class", "histogram");
			
		histogram.append("h2")
			.attr("class", "chart-title")
			.text(label);
	}
	
	var chart = histogram.select("svg");
	if (chart.empty()) {
		chart = histogram.append("svg");
	}
	chart.attr("class", "chart")
		.attr("width", histogramMetrics.width)
		.attr("height", data.length * (histogramMetrics.barHeight + histogramMetrics.interBarSpacing) + histogramMetrics.barHeight);
	
	var toggleCollapse = function () {
		addHistogram(id, label, originalData, !needsCollapse);
	};
	chart.node().toggleCollapse = toggleCollapse;
	
	var collapseText = histogram.select("p.collapse");
	if (collapse === false) {
		if (collapseText.empty()) {
			collapseText = histogram.append("p")
				.attr("class", "collapse")
				.text(mcLocalizedCollapse);
		}
		collapseText.on("click", toggleCollapse);
	} else {
		collapseText.remove();
	}
	
	updateHistogram(chart, x, data, null, needsCollapse);
	
	histogramUpdates[id] = function (new_data, subdata) {

		var collapsedNewData = new_data;		
		if(needsCollapse && new_data !== null) {
			collapsedNewData = collapseHistogramData(new_data, 5);
		}
		
		var collapsedSubData = subdata;
		if(needsCollapse) {
			collapsedSubData = collapseHistogramData(subdata, 5);
		}
		
		updateHistogram(chart, x, collapsedNewData, collapsedSubData, needsCollapse);
	};
}

function updateHistogram(svg, x, data, subdata, needsCollapse) {
	
	if (data !== null) {
		drawHistogramBars(svg, x, data, "bar", needsCollapse);
	}
	
	if(subdata === null) return;
	drawHistogramBars(svg, x, subdata, "subbar", needsCollapse);
}

function updateExistingHistogram(id, data, subdata) {
	if(filtered === false) {
		return;
	}
	
	histogramUpdates[id](data, subdata);
}

// collpased - Whether to draw the data with the last bar visually seperated
// computeSecondScale - If truthy, compute and use a second scale for all bars but the first. It's domain is [0, max].
// actualSecondScale - If !null, used as the second scale for all bars but the first and computeSecondScale has no effect.
// Both scale arguments are used mostly by the pipeline graphs.
function drawHistogramBars(svg, x, data, className, collapsed, computeSecondScale, actualSecondScale) {
	
	var yTranslation = function(d, i) {
		var lastRowOffset = (collapsed && i === 5) ? 6 : 0;
		var y = (i * (histogramMetrics.barHeight + histogramMetrics.interBarSpacing) + lastRowOffset);
		return "translate(0," + y + ")";
	};
	
	var sx = x;
	if(computeSecondScale === true) {
		sx = d3.scale.linear()
				.domain([0, d3.max(data, function(d) { return d.value; })])
				.rangeRound(x.range());
	}
	
	if(actualSecondScale !== undefined && actualSecondScale !== null) {
		sx = actualSecondScale;
	}
	
	var width = function (d, i) { // 'combined' scale, at least 1 if nonzero
		var value = (i === 0) ? x(d.value) : sx(d.value);
		return (value === 0 && d.value > 0) ? 1 : value;
	};
	
	var formatCountDatum = function (d) {
		return formatCount(d.value);
	};
	
	var filterOrExpand = function(d,i) {
		if (collapsed && i === 5) {
			svg.node().toggleCollapse();
		} else {
			filterForDatum(d,i);
		}
	};
	
	// Drawing bars - Update
	var barUpdate = svg.selectAll("." + className)
	  .data(data)
		.attr("transform", yTranslation)
		.on("mouseover", previewFilterForDatum)
		.on("click", filterOrExpand);
	
	barUpdate.select("rect.filledBar")
		.attr("width", width);
	
	barUpdate.select(".values")
		.text(formatCountDatum);
	
	var updateLabel = barUpdate.select(".labels")
		.text(function(d) { return d.label; }); // Clears children
	updateLabel.call(truncateText, histogramMetrics.labelMargin);
	updateLabel.append("title").text(function(d) { return d.label; });
	
	// Drawing bars - Enter
	var barEnter = barUpdate.enter().append("g")
		.attr("class", className)
		.attr("transform", yTranslation)
		.on("mouseover", previewFilterForDatum)
		.on("mouseout", clearSubData)
		.on("click", filterOrExpand);
	
	var barLeft = histogramMetrics.labelMargin + histogramMetrics.barPadding;
	
	if (className != "subbar") {
		barEnter.append("rect")
			.attr("class", "emptybar")
			.attr("x", barLeft)
			.attr("width", histogramMetrics.barWidth())
			.attr("height", histogramMetrics.barHeight - 1);	
	}
	
	barEnter.append("rect")
		.attr("class", "filledBar")
		.attr("x", barLeft)
		.attr("width", width)
		.attr("height", histogramMetrics.barHeight - 1);
		
	barEnter.append("text")
		.attr("class", "values")
		.attr("x", histogramMetrics.width - histogramMetrics.valueMargin + histogramMetrics.barPadding)
		.attr("y", histogramMetrics.barHeight - 2)
		.text(formatCountDatum);
	
	var enterLabel = barEnter.append("text")
		.attr("class", "labels")
		.attr("x", histogramMetrics.labelMargin - 3)
		.attr("y", histogramMetrics.barHeight - 2)
		.attr("width", histogramMetrics.labelMargin)
		.text(function(d) { return d.label; });
	enterLabel.call(truncateText, histogramMetrics.labelMargin);
	enterLabel.append("title").text(function(d) { return d.label; });
	
	// Exit
	barUpdate.exit().remove();
	
	return sx; //return the secondary scale
}

function truncateText(text, width) {
	
	text.each(function() {
			
		var ellipse = '\u2026';
		
		var text = d3.select(this);
		
		// Use the text element to calculate the width of an ellipse
		var originalText = text.text();
		var ellipseWidth = text.text(ellipse).node().getComputedTextLength();
		text.text(originalText);
		
		var shortened = false;
		while(text.node().getComputedTextLength() > (width - ellipseWidth) - 2) {
			shortened = true;
			text.text(text.text().slice(0, -1).trim());
		}

		if(shortened) {
			text.text(text.text() + ellipse);
		}
	});
}

function collapseHistogramData(originalData, count) {
	
	var data = originalData.slice(0, count);
	
	var otherRows = originalData.slice(count);
	if(otherRows.length > 0) {
		data[count] = {
		label: otherRows.length + " " + mcLocalizedMore,
		value: d3.sum(otherRows.map( function (d) { return d.value; }))
		};
	}
	
	return data;
}

// Time Series

var timeSeriesToRender = []; // Since all time series share a x scale,

var timeSeriesUpdates = {}; // A map from id to svg element so graphs can be updated on demand

var maxDate = null;
var minDate = null;

var dataDateFormat = d3.time.format("%Y%m%d");
var parseDataDate = dataDateFormat.parse;

var timeMargin = {top: 20, right: 10, bottom: 30, left: 60},
timeWidth = 500 - timeMargin.left - timeMargin.right,
timeHeight = 150 - timeMargin.top - timeMargin.bottom;

// One of 2014, Feb, or 05
var dateTickFormat = d3.time.format.multi([
	["%d", function(d) { return d.getDate() != 1; }],
	["%b", function(d) { return d.getMonth(); }],
	["%Y", function() { return true; }]
]);

// b = short month, -d = no padding day of month. ex. Jan 1
var dateHoverFormat = d3.time.format("%b %-d");

// Time Range Selection State
var mousedownDate = null;
var lastMinDate = null;
var lastMaxDate = null;

function addTimeSeries(id, label, data, min, max, filter) {
	
	var localMin = parseDataDate(min);
	var localMax = parseDataDate(max);
	
	if(!minDate) minDate = localMin;
	if(!maxDate) maxDate = localMax;
	
	minDate = d3.min([localMin, minDate]);
	maxDate = d3.max([localMax, maxDate]);
	
	var render = function(x, barWidth, interval) {
		
		var yMax = d3.max(data, function(d) { return d.value; });
		yMax = Math.max(yMax, 10);
		
		var y = d3.scale.linear()
			.domain([0, yMax])
			.nice(4)
			.rangeRound([timeHeight, 0]);
		
		var xAxis = d3.svg.axis()
			.scale(x)
			.tickFormat(dateTickFormat)
			.orient("bottom");
		
		var yAxis = d3.svg.axis()
			.scale(y)
			.ticks(4)
			.tickFormat(formatCount)
			.orient("left");
		
		var chart = d3.select("#timeseries").append("div")
			.attr("class", "timeserieschart");
		
		chart.append("h2")
			.classed("chart-title", true)
			.text(label);
		
		var svg = chart.append("svg")
			.attr("id", id)
			.attr("width", timeWidth + timeMargin.left + timeMargin.right)
			.attr("height", timeHeight + timeMargin.top + timeMargin.bottom)
			.on("mouseout", clearTimebar)
		  .append("g")
			.attr("transform", "translate(" + timeMargin.left + "," + timeMargin.top + ")");
		
		svg.append("g")
			.attr("class", "x axis")
			.attr("transform", "translate(0," + timeHeight + ")")
			.call(xAxis);
				
		svg.append("g")
			.attr("class", "y axis")
			.call(yAxis);
			
		// Background to handle mouse events
		svg.append("rect")
			.attr("class", "background")
			.attr("width", timeWidth)
			.attr("height", timeHeight);
		
		drawTimeSeriesBars(svg, x, y, barWidth, interval, data, "bar", filter);
		
		// Grids
		svg.selectAll(".gridline")
			.data(y.ticks(5))
		  .enter().append("rect")
			.attr("class", "gridline")
			.attr("height", 1)
			.attr("width", timeWidth)
			.attr("y", y);
		
		svg.append("rect")
			.attr("class", "selectedTimeRange")
			.attr("height", timeHeight);
		
		// Now indicator
		svg.append("rect")
			.attr("class", "nowTick")
			.attr("width", 1)
			.attr("height", 106)
			.attr("y", 0)
			.attr("x", x(Date.now()));
		
		svg.append("circle")
			.attr("class", "nowTick")
			.attr("r", 2.5)
			.attr("cy", 0.5)
			.attr("cx", x(Date.now()) + 0.5);
		
		// Blue hover bar
		svg.append("rect")
			.attr("class", "hoverTimeBar")
			.attr("height", timeHeight)
			.attr("width", 1)
			.attr("y", 0)
			.attr("display", "none");
		
		svg.append("text")
			.attr("class", "hoverTimeLabel")
			.attr("display", "none");
		
		// This records how to update this time series with new data
		timeSeriesUpdates[id] = function(new_data, subdata) {
			updateTimeSeries(svg, x, y, barWidth, interval, new_data, subdata, filter);
		};
	};
	
	timeSeriesToRender.push(render); // Will be called in allTimeSeriesAdded
}

function updateTimeSeries(svg, x, y, barWidth, interval, data, subdata, filter) {
	
	// Sometimes data will be null because we don't need to redraw it.
	if(data !== null) {
		drawTimeSeriesBars(svg, x, y, barWidth, interval, data, "bar", filter);
	}
	
	// We aren't always hover filtering either, so subdata might be empty
	if(subdata === null) return;
	drawTimeSeriesBars(svg, x, y, barWidth, interval, subdata, "subbar", filter);
}

function drawTimeSeriesBars(svg, x, y, barWidth, interval, data, cssclass, filter) {
	
	var dates = data.map(function(d) { return parseDataDate(d.label); });
		
	// Time Selection
	var mousedownOnDate = function (date) {
		d3.selectAll(".selectedTimeRange")
			.style("pointer-events", "none")
			.attr("x", 0)
			.attr("width", 0);
		mousedownDate = date;
	};
	
	var mouseup = function () {
		d3.selectAll(".selectedTimeRange").style("pointer-events", "all");
		mousedownDate = null;
	};
	
	var mousemoveOnDate = function (currentDate) {
		
		var currentX = x(currentDate);
		
		highlightAllTimeSeries(currentX, currentDate);
		
		if(mousedownDate === null) {
			return;
		}
		
		var downX = x(mousedownDate);
		
		if(downX == currentX) {
			return;
		}
		
		lastMinDate = d3.min([mousedownDate, currentDate]);
		lastMaxDate = d3.max([mousedownDate, currentDate]);
		
		filtered = true;
		filter.previewFilterInclusiveStart_exclusiveEnd_(dataDateFormat(lastMinDate), dataDateFormat(lastMaxDate));
		
		var minX = d3.min([downX, currentX]);
		var maxX = d3.max([downX, currentX]);
		
		svg.select(".selectedTimeRange")
			.attr("x", minX)
			.attr("width", maxX - minX - 1);
	};
	
	var applyDateFromMouseTo = function (handler) {		
		return function () {
			var xPos = d3.mouse(svg.node())[0];
			var date = interval.round(x.invert(xPos));
			handler(date);
		};
	};
	
	svg.select(".background")
		.on("mousedown", applyDateFromMouseTo(mousedownOnDate))
		.on("mousemove", applyDateFromMouseTo(mousemoveOnDate))
		.on("mouseup", mouseup)
		.on("mouseout", mouseup);
	
	// Click on a selected time range to filter
	svg.select(".selectedTimeRange").on("click", function() {
		var start = dataDateFormat(lastMinDate);
		var end = dataDateFormat(lastMaxDate);
		filter.filterInclusiveStart_exclusiveEnd_(start, end);
	});
	
	// height needs to be at least 1, since we don't get data with 0 counts (d.value == 0)
	var height = function (d) {
		var h = timeHeight - y(d.value);
		return Math.max(h, 1);
	};
	
	// same adjustment as height above, otherwise the line appears below the axis
	var yCapped = function (d) {
		var yy = y(d.value);
		return Math.min(yy, timeHeight - 1);
	};
	
	// Drawing Bars - Update
	var updateBar = svg.selectAll("." + cssclass)
	  .data(data)
		.attr("transform", function(d, i) { return "translate(" + x(dates[i]) + ",0)"; });
	
	updateBar.select("rect")
		.attr("height", height)
		.attr("y", yCapped);
	
	// Drawing Bars - Enter
	var enterBar = updateBar.enter().append("g")
		.attr("class", cssclass)
		.attr("transform", function(d, i) { return "translate(" + x(dates[i]) + ",0)"; });
		
	enterBar.append("rect")
		.attr("height", height)
		.attr("y", yCapped)
		.attr("width", barWidth);
	
	// Drawing Bars - Exit
	updateBar.exit().remove();
}

// precision - the length of time each data point summarises
function allTimeSeriesAdded(precision) {
	
	var interval = d3.time.month;
	if(precision === 0) {
		interval = d3.time.day;
	} else if(precision == 1) {
		interval = d3.time.week;
	}
	
	var x = d3.time.scale()
		.domain([minDate, maxDate])
		.rangeRound([0, timeWidth])
		.nice(d3.time.month); // Extend the domain to a month boundry
	
	var barWidth = timeWidth / x.ticks(interval).length;
	var floorWidth = Math.floor(barWidth);
	var floorDelta = barWidth - floorWidth;
	
	// Subtract 1 if flooring shaved off less then 0.5
	barWidth = (floorDelta > 0.5) ? floorWidth : floorWidth - 1;
	
	if(barWidth < 1) barWidth = 1;
	
	timeSeriesToRender.forEach( function (a) { a(x, barWidth, interval); });
}

function highlightAllTimeSeries(x, date) {
	
	d3.selectAll(".hoverTimeBar")
		.attr("display", "inline")
		.attr("x", x);
	
	var labelOffset = 4;
	var labelFlipPoint = timeWidth - 30;

	var labelXPos = x + labelOffset;
	var labelAnchor = "start";
	if (x > labelFlipPoint) {
		labelXPos = x - labelOffset;
		labelAnchor = "end";
	}
	
	d3.selectAll(".hoverTimeLabel")
		.attr("display", "inline")
		.attr("x", labelXPos)
		.attr("width", 20)
		.style("text-anchor", labelAnchor)
		.text(dateHoverFormat(date));
}

function clearTimebar() {
	d3.selectAll(".hoverTimeBar")
		.attr("display", "none");
	d3.selectAll(".hoverTimeLabel")
		.attr("display", "none");
}

/* Update Exising Graphs */

function updateExistingTimeSeries(id, data, subdata) {
	if(filtered === false) {
		return;
	}
	
	timeSeriesUpdates[id](data, subdata);
}


/* Helpers */
var clearSubdataTimeout = null;
var filtered = false;

function clearSubData() {
	
	clearTimeout(clearSubdataTimeout);
	
	filter.cancelPreviewFilter();
	
	clearSubdataTimeout = setTimeout(function() {
		filtered = false;
		d3.selectAll(".subbar").remove();
		d3.selectAll(".selectedTimeRange").attr("width", 0);
	}, 100);
}

function previewFilterForDatum(d) {
	
	// clear the selected time range
	d3.selectAll(".selectedTimeRange").attr("width", 0);
	
	filtered = true;
	clearTimeout(clearSubdataTimeout);
	
	filter.previewFilterForDatum_(d);
}

function filterForDatum(d) {
	filter.filterForDatum_(d);
}

// Not called anymore
function reset() {
	
	// Clear the DOM
	d3.selectAll("svg").remove();
	d3.selectAll("dt").remove();
	d3.selectAll("dd").remove();
	d3.selectAll("h2").remove();
	
	// Clear any global state
	pipelinesToDraw = [];
	byPipelineCounts = [];
	pipelineUpdates = {};
	pipelineScale = null;
	
	histogramUpdates = {};
	
	timeSeriesToRender = [];
	timeSeriesUpdates = {};
	maxDate = null;
	minDate = null;
	
	clearSubdataTimeout = null;
	filtered = false;
}
