// --------- labels.js -----------
var labels = {

	/**
	 * Adds the labels to the pie chart, but doesn't position them. There are two locations for the
	 * labels: inside (center) of the segments, or outside the segments on the edge.
	 * @param section "inner" or "outer"
	 * @param sectionDisplayType "percentage", "value", "label", "label-value1", etc.
	 * @param pie
	 */
	add: function(pie, section, sectionDisplayType) {
		var include = labels.getIncludes(sectionDisplayType);
		var settings = pie.options.labels;

		// group the label groups (label, percentage, value) into a single element for simpler positioning
		var outerLabel = pie.svg.insert("g", "." + pie.cssPrefix + "labels-" + section)
			.attr("class", pie.cssPrefix + "labels-" + section);

		var labelGroup = outerLabel.selectAll("." + pie.cssPrefix + "labelGroup-" + section)
			.data(pie.options.data.content)
			.enter()
			.append("g")
			.attr("id", function(d, i) { return pie.cssPrefix + "labelGroup" + i + "-" + section; })
			.attr("data-index", function(d, i) { return i; })
			.attr("class", pie.cssPrefix + "labelGroup-" + section)
			.style("opacity", 0);

		var formatterContext = { section: section, sectionDisplayType: sectionDisplayType };

		// 1. Add the main label
		if (include.mainLabel) {
			labelGroup.append("text")
				.attr("id", function(d, i) { return pie.cssPrefix + "segmentMainLabel" + i + "-" + section; })
				.attr("class", pie.cssPrefix + "segmentMainLabel-" + section)
				.text(function(d, i) {
					var str = d.label;

					// if a custom formatter has been defined, pass it the raw label string - it can do whatever it wants with it.
					// we only apply truncation if it's not defined
					if (settings.formatter) {
						formatterContext.index = i;
						formatterContext.part = 'mainLabel';
						formatterContext.value = d.value;
						formatterContext.label = str;
						str = settings.formatter(formatterContext);
					} else if (settings.truncation.enabled && d.label.length > settings.truncation.truncateLength) {
						str = d.label.substring(0, settings.truncation.truncateLength) + "...";
					}
					return str;
				})
				.style("font-size", settings.mainLabel.fontSize + "px")
				.style("font-family", settings.mainLabel.font)
				.style("fill", settings.mainLabel.color);
		}

		// 2. Add the percentage label
		if (include.percentage) {
			labelGroup.append("text")
				.attr("id", function(d, i) { return pie.cssPrefix + "segmentPercentage" + i + "-" + section; })
				.attr("class", pie.cssPrefix + "segmentPercentage-" + section)
				.text(function(d, i) {
					var percentage = d.percentage;
					if (settings.formatter) {
						formatterContext.index = i;
						formatterContext.part = "percentage";
						formatterContext.value = d.value;
						formatterContext.label = d.percentage;
						percentage = settings.formatter(formatterContext);
					} else {
						percentage += "%";
					}
					return percentage;
				})
				.style("font-size", settings.percentage.fontSize + "px")
				.style("font-family", settings.percentage.font)
				.style("fill", settings.percentage.color);
		}

		// 3. Add the value label
		if (include.value) {
			labelGroup.append("text")
				.attr("id", function(d, i) { return pie.cssPrefix +  "segmentValue" + i + "-" + section; })
				.attr("class", pie.cssPrefix + "segmentValue-" + section)
				.text(function(d, i) {
					formatterContext.index = i;
					formatterContext.part = "value";
					formatterContext.value = d.value;
					formatterContext.label = d.value;
					return settings.formatter ? settings.formatter(formatterContext, d.value) : d.value;
				})
				.style("font-size", settings.value.fontSize + "px")
				.style("font-family", settings.value.font)
				.style("fill", settings.value.color);
		}
	},

	/**
	 * @param section "inner" / "outer"
	 */
	positionLabelElements: function(pie, section, sectionDisplayType) {
		labels["dimensions-" + section] = [];

		// get the latest widths, heights
		var labelGroups = d3.selectAll("." + pie.cssPrefix + "labelGroup-" + section);
		labelGroups.each(function(d, i) {
			var mainLabel  = d3.select(this).selectAll("." + pie.cssPrefix + "segmentMainLabel-" + section);
			var percentage = d3.select(this).selectAll("." + pie.cssPrefix + "segmentPercentage-" + section);
			var value      = d3.select(this).selectAll("." + pie.cssPrefix + "segmentValue-" + section);

			labels["dimensions-" + section].push({
				mainLabel:  (mainLabel.node() !== null) ? mainLabel.node().getBBox() : null,
				percentage: (percentage.node() !== null) ? percentage.node().getBBox() : null,
				value:      (value.node() !== null) ? value.node().getBBox() : null
			});
		});

		var singleLinePad = 5;
		var dims = labels["dimensions-" + section];
		switch (sectionDisplayType) {
			case "label-value1":
				d3.selectAll("." + pie.cssPrefix + "segmentValue-" + section)
					.attr("dx", function(d, i) { return dims[i].mainLabel.width + singleLinePad; });
				break;
			case "label-value2":
				d3.selectAll("." + pie.cssPrefix + "segmentValue-" + section)
					.attr("dy", function(d, i) { return dims[i].mainLabel.height; });
				break;
			case "label-percentage1":
				d3.selectAll("." + pie.cssPrefix + "segmentPercentage-" + section)
					.attr("dx", function(d, i) { return dims[i].mainLabel.width + singleLinePad; });
				break;
			case "label-percentage2":
				d3.selectAll("." + pie.cssPrefix + "segmentPercentage-" + section)
					.attr("dx", function(d, i) { return (dims[i].mainLabel.width / 2) - (dims[i].percentage.width / 2); })
					.attr("dy", function(d, i) { return dims[i].mainLabel.height; });
				break;
	 	}
	},

	computeLabelLinePositions: function(pie) {
		pie.lineCoordGroups = [];
		d3.selectAll("." + pie.cssPrefix + "labelGroup-outer")
			.each(function(d, i) { return labels.computeLinePosition(pie, i); });
	},

	computeLinePosition: function(pie, i) {
		var angle = segments.getSegmentAngle(i, pie.options.data.content, pie.totalSize, { midpoint: true });
		var originCoords = math.rotate(pie.pieCenter.x, pie.pieCenter.y - pie.outerRadius, pie.pieCenter.x, pie.pieCenter.y, angle);
		var heightOffset = pie.outerLabelGroupData[i].h / 5; // TODO check
		var labelXMargin = 6; // the x-distance of the label from the end of the line [TODO configurable]

		var quarter = Math.floor(angle / 90);
		var midPoint = 4;
		var x2, y2, x3, y3;

		// this resolves an issue when the
		if (quarter === 2 && angle === 180) {
			quarter = 1;
		}

		switch (quarter) {
			case 0:
				x2 = pie.outerLabelGroupData[i].x - labelXMargin - ((pie.outerLabelGroupData[i].x - labelXMargin - originCoords.x) / 2);
				y2 = pie.outerLabelGroupData[i].y + ((originCoords.y - pie.outerLabelGroupData[i].y) / midPoint);
				x3 = pie.outerLabelGroupData[i].x - labelXMargin;
				y3 = pie.outerLabelGroupData[i].y - heightOffset;
				break;
			case 1:
				x2 = originCoords.x + (pie.outerLabelGroupData[i].x - originCoords.x) / midPoint;
				y2 = originCoords.y + (pie.outerLabelGroupData[i].y - originCoords.y) / midPoint;
				x3 = pie.outerLabelGroupData[i].x - labelXMargin;
				y3 = pie.outerLabelGroupData[i].y - heightOffset;
				break;
			case 2:
				var startOfLabelX = pie.outerLabelGroupData[i].x + pie.outerLabelGroupData[i].w + labelXMargin;
				x2 = originCoords.x - (originCoords.x - startOfLabelX) / midPoint;
				y2 = originCoords.y + (pie.outerLabelGroupData[i].y - originCoords.y) / midPoint;
				x3 = pie.outerLabelGroupData[i].x + pie.outerLabelGroupData[i].w + labelXMargin;
				y3 = pie.outerLabelGroupData[i].y - heightOffset;
				break;
			case 3:
				var startOfLabel = pie.outerLabelGroupData[i].x + pie.outerLabelGroupData[i].w + labelXMargin;
				x2 = startOfLabel + ((originCoords.x - startOfLabel) / midPoint);
				y2 = pie.outerLabelGroupData[i].y + (originCoords.y - pie.outerLabelGroupData[i].y) / midPoint;
				x3 = pie.outerLabelGroupData[i].x + pie.outerLabelGroupData[i].w + labelXMargin;
				y3 = pie.outerLabelGroupData[i].y - heightOffset;
				break;
		}

		/*
		 * x1 / y1: the x/y coords of the start of the line, at the mid point of the segments arc on the pie circumference
		 * x2 / y2: if "curved" line style is being used, this is the midpoint of the line. Other
		 * x3 / y3: the end of the line; closest point to the label
		 */
		if (pie.options.labels.lines.style === "straight") {
			pie.lineCoordGroups[i] = [
				{ x: originCoords.x, y: originCoords.y },
				{ x: x3, y: y3 }
			];
		} else {
			pie.lineCoordGroups[i] = [
				{ x: originCoords.x, y: originCoords.y },
				{ x: x2, y: y2 },
				{ x: x3, y: y3 }
			];
		}
	},

	addLabelLines: function(pie) {
		var lineGroups = pie.svg.insert("g", "." + pie.cssPrefix + "pieChart") // meaning, BEFORE .pieChart
			.attr("class", pie.cssPrefix + "lineGroups")
			.style("opacity", 0);

		var lineGroup = lineGroups.selectAll("." + pie.cssPrefix + "lineGroup")
			.data(pie.lineCoordGroups)
			.enter()
			.append("g")
			.attr("class", pie.cssPrefix + "lineGroup");

        var lineFunction = d3.line()
			.curve(d3.curveBasis)
			.x(function(d) { return d.x; })
			.y(function(d) { return d.y; });

		lineGroup.append("path")
			.attr("d", lineFunction)
			.attr("stroke", function(d, i) {
				return (pie.options.labels.lines.color === "segment") ? pie.options.colors[i] : pie.options.labels.lines.color;
			})
			.attr("stroke-width", 1)
			.attr("fill", "none")
			.style("opacity", function(d, i) {
				var percentage = pie.options.labels.outer.hideWhenLessThanPercentage;
				var isHidden = (percentage !== null && d.percentage < percentage) || pie.options.data.content[i].label === "";
				return isHidden ? 0 : 1;
			});
	},

	positionLabelGroups: function(pie, section) {
    if (pie.options.labels[section].format === "none") {
      return;
    }

		d3.selectAll("." + pie.cssPrefix + "labelGroup-" + section)
			.style("opacity", 0)
			.attr("transform", function(d, i) {
				var x, y;
				if (section === "outer") {
					x = pie.outerLabelGroupData[i].x;
					y = pie.outerLabelGroupData[i].y;
				} else {
					var pieCenterCopy = extend(true, {}, pie.pieCenter);

					// now recompute the "center" based on the current _innerRadius
					if (pie.innerRadius > 0) {
						var angle = segments.getSegmentAngle(i, pie.options.data.content, pie.totalSize, { midpoint: true });
						var newCoords = math.translate(pie.pieCenter.x, pie.pieCenter.y, pie.innerRadius, angle);
						pieCenterCopy.x = newCoords.x;
						pieCenterCopy.y = newCoords.y;
					}

					var dims = helpers.getDimensions(pie.cssPrefix + "labelGroup" + i + "-inner");
					var xOffset = dims.w / 2;
					var yOffset = dims.h / 4; // confusing! Why 4? should be 2, but it doesn't look right

					x = pieCenterCopy.x + (pie.lineCoordGroups[i][0].x - pieCenterCopy.x) / 1.8;
					y = pieCenterCopy.y + (pie.lineCoordGroups[i][0].y - pieCenterCopy.y) / 1.8;

					x = x - xOffset;
					y = y + yOffset;
				}

				return "translate(" + x + "," + y + ")";
			});
	},


	fadeInLabelsAndLines: function(pie) {

		// fade in the labels when the load effect is complete - or immediately if there's no load effect
		var loadSpeed = (pie.options.effects.load.effect === "default") ? pie.options.effects.load.speed : 1;
		setTimeout(function() {
			var labelFadeInTime = (pie.options.effects.load.effect === "default") ? 400 : 1; // 400 is hardcoded for the present

			d3.selectAll("." + pie.cssPrefix + "labelGroup-outer")
				.transition()
				.duration(labelFadeInTime)
				.style("opacity", function(d, i) {
					var percentage = pie.options.labels.outer.hideWhenLessThanPercentage;
					return (percentage !== null && d.percentage < percentage) ? 0 : 1;
				});

			d3.selectAll("." + pie.cssPrefix + "labelGroup-inner")
				.transition()
				.duration(labelFadeInTime)
				.style("opacity", function(d, i) {
					var percentage = pie.options.labels.inner.hideWhenLessThanPercentage;
					return (percentage !== null && d.percentage < percentage) ? 0 : 1;
				});

			d3.selectAll("g." + pie.cssPrefix + "lineGroups")
				.transition()
				.duration(labelFadeInTime)
				.style("opacity", 1);

			// once everything's done loading, trigger the onload callback if defined
			if (helpers.isFunction(pie.options.callbacks.onload)) {
				setTimeout(function() {
					try {
						pie.options.callbacks.onload();
					} catch (e) { }
				}, labelFadeInTime);
			}
		}, loadSpeed);
	},

	getIncludes: function(val) {
		var addMainLabel  = false;
		var addValue      = false;
		var addPercentage = false;

		switch (val) {
			case "label":
				addMainLabel = true;
				break;
			case "value":
				addValue = true;
				break;
			case "percentage":
				addPercentage = true;
				break;
			case "label-value1":
			case "label-value2":
				addMainLabel = true;
				addValue = true;
				break;
			case "label-percentage1":
			case "label-percentage2":
				addMainLabel = true;
				addPercentage = true;
				break;
		}
		return {
			mainLabel: addMainLabel,
			value: addValue,
			percentage: addPercentage
		};
	},


	/**
	 * This does the heavy-lifting to compute the actual coordinates for the outer label groups. It does two things:
	 * 1. Make a first pass and position them in the ideal positions, based on the pie sizes
	 * 2. Do some basic collision avoidance.
	 */
	computeOuterLabelCoords: function(pie) {

		// 1. figure out the ideal positions for the outer labels
		pie.svg.selectAll("." + pie.cssPrefix + "labelGroup-outer")
			.each(function(d, i) {
				return labels.getIdealOuterLabelPositions(pie, i);
			});

		// 2. now adjust those positions to try to accommodate conflicts
		labels.resolveOuterLabelCollisions(pie);
	},

	/**
	 * This attempts to resolve label positioning collisions.
	 */
	resolveOuterLabelCollisions: function(pie) {
    if (pie.options.labels.outer.format === "none") {
      return;
    }

		var size = pie.options.data.content.length;
		labels.checkConflict(pie, 0, "clockwise", size);
		labels.checkConflict(pie, size-1, "anticlockwise", size);
	},

	checkConflict: function(pie, currIndex, direction, size) {
    var i, curr;

		if (size <= 1) {
			return;
		}

		var currIndexHemisphere = pie.outerLabelGroupData[currIndex].hs;
		if (direction === "clockwise" && currIndexHemisphere !== "right") {
			return;
		}
		if (direction === "anticlockwise" && currIndexHemisphere !== "left") {
			return;
		}
		var nextIndex = (direction === "clockwise") ? currIndex+1 : currIndex-1;

		// this is the current label group being looked at. We KNOW it's positioned properly (the first item
		// is always correct)
		var currLabelGroup = pie.outerLabelGroupData[currIndex];

		// this one we don't know about. That's the one we're going to look at and move if necessary
		var examinedLabelGroup = pie.outerLabelGroupData[nextIndex];

		var info = {
			labelHeights: pie.outerLabelGroupData[0].h,
			center: pie.pieCenter,
			lineLength: (pie.outerRadius + pie.options.labels.outer.pieDistance),
			heightChange: pie.outerLabelGroupData[0].h + 1 // 1 = padding
		};

		// loop through *ALL* label groups examined so far to check for conflicts. This is because when they're
		// very tightly fitted, a later label group may still appear high up on the page
		if (direction === "clockwise") {
      i = 0;
			for (; i<=currIndex; i++) {
				curr = pie.outerLabelGroupData[i];

				// if there's a conflict with this label group, shift the label to be AFTER the last known
				// one that's been properly placed
				if (!labels.isLabelHidden(pie, i) && helpers.rectIntersect(curr, examinedLabelGroup)) {
					labels.adjustLabelPos(pie, nextIndex, currLabelGroup, info);
					break;
				}
			}
		} else {
      i = size - 1;
			for (; i >= currIndex; i--) {
				curr = pie.outerLabelGroupData[i];

				// if there's a conflict with this label group, shift the label to be AFTER the last known
				// one that's been properly placed
				if (!labels.isLabelHidden(pie, i) && helpers.rectIntersect(curr, examinedLabelGroup)) {
					labels.adjustLabelPos(pie, nextIndex, currLabelGroup, info);
					break;
				}
			}
		}
		labels.checkConflict(pie, nextIndex, direction, size);
	},

	isLabelHidden: function(pie, index) {
		var percentage = pie.options.labels.outer.hideWhenLessThanPercentage;
		return (percentage !== null && d.percentage < percentage) || pie.options.data.content[index].label === "";
	},

	// does a little math to shift a label into a new position based on the last properly placed one
	adjustLabelPos: function(pie, nextIndex, lastCorrectlyPositionedLabel, info) {
		var xDiff, yDiff, newXPos, newYPos;
		newYPos = lastCorrectlyPositionedLabel.y + info.heightChange;
		yDiff = info.center.y - newYPos;

		if (Math.abs(info.lineLength) > Math.abs(yDiff)) {
			xDiff = Math.sqrt((info.lineLength * info.lineLength) - (yDiff * yDiff));
		} else {
			xDiff = Math.sqrt((yDiff * yDiff) - (info.lineLength * info.lineLength));
		}

		if (lastCorrectlyPositionedLabel.hs === "right") {
			newXPos = info.center.x + xDiff;
		} else {
			newXPos = info.center.x - xDiff - pie.outerLabelGroupData[nextIndex].w;
		}

		pie.outerLabelGroupData[nextIndex].x = newXPos;
		pie.outerLabelGroupData[nextIndex].y = newYPos;
	},

	/**
	 * @param i 0-N where N is the dataset size - 1.
	 */
	getIdealOuterLabelPositions: function(pie, i) {
    var labelGroupNode = d3.select("#" + pie.cssPrefix + "labelGroup" + i + "-outer").node();
    if (!labelGroupNode) {
      return;
    }
    var labelGroupDims = labelGroupNode.getBBox();
		var angle = segments.getSegmentAngle(i, pie.options.data.content, pie.totalSize, { midpoint: true });

		var originalX = pie.pieCenter.x;
		var originalY = pie.pieCenter.y - (pie.outerRadius + pie.options.labels.outer.pieDistance);
		var newCoords = math.rotate(originalX, originalY, pie.pieCenter.x, pie.pieCenter.y, angle);

		// if the label is on the left half of the pie, adjust the values
		var hemisphere = "right"; // hemisphere
		if (angle > 180) {
			newCoords.x -= (labelGroupDims.width + 8);
			hemisphere = "left";
		} else {
			newCoords.x += 8;
		}

		pie.outerLabelGroupData[i] = {
			x: newCoords.x,
			y: newCoords.y,
			w: labelGroupDims.width,
			h: labelGroupDims.height,
			hs: hemisphere
		};
	}
};
