import {add2D, mulScalar2D, subtract2D} from '../utils/linear';

const infinityPointScale = 5000000;
const epsilon = 0.000001;

export function approximately(a, b, precision = epsilon) {
	return Math.abs(a - b) <= precision;
}

// The following function should be useless, thanks to paper
export function lineLineIntersection(p1, p2, p3, p4) {
	const x1 = p1.x;
	const y1 = p1.y;
	const x2 = p2.x;
	const y2 = p2.y;
	const x3 = p3.x;
	const y3 = p3.y;
	const x4 = p4.x;
	const y4 = p4.y;
	const d = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4);

	if (d === 0) {
		return p1;
	}

	return {
		x: ((x1 * y2 - y1 * x2) * (x3 - x4) - (x1 - x2) * (x3 * y4 - y3 * x4)) / d,
		y: ((x1 * y2 - y1 * x2) * (y3 - y4) - (y1 - y2) * (x3 * y4 - y3 * x4)) / d,
	};
}

// Find the intersection of two rays.
// A ray is defined by a point and an angle.
export function rayRayIntersection(p1, a1, p2, a2) {
	// line equations
	const a = Math.tan(a1);
	const b = Math.tan(a2);
	const c = p1.y - a * p1.x;
	const d = p2.y - b * p2.x;
	let x;
	let y;

	// When searching for lines intersection,
	// angles can be normalized to 0 < a < PI
	// This will be helpful in detecting special cases below.
	/* eslint-disable no-param-reassign */
	a1 %= Math.PI;
	if (a1 < 0) {
		a1 += Math.PI;
	}
	a2 %= Math.PI;
	if (a2 < 0) {
		a2 += Math.PI;
	}

	// no intersection
	if (a1 === a2) {
		return {
			x: p1.x + infinityPointScale * Math.cos(a1),
			y: p1.y + infinityPointScale * Math.sin(a1),
		};
	}

	// We want to round a1, a2 and PI to avoid problems with approximation
	a1 = a1.toFixed(6);
	a2 = a2.toFixed(6);
	/* eslint-enable no-param-reassign */
	const piOver2 = (Math.PI / 2).toFixed(6);

	// Optimize frequent and easy special cases.
	// Without optimization, results would be incorrect when cos(a) === 0
	if (a1 === 0) {
		y = p1.y;
	}
	else if (a1 === piOver2) {
		x = p1.x;
	}
	if (a2 === 0) {
		y = p2.y;
	}
	else if (a2 === piOver2) {
		x = p2.x;
	}

	// easiest case
	if (x !== undefined && y !== undefined) {
		return {x, y};
	}

	// other cases that can be optimized
	if (a1 === 0) {
		return {x: (y - d) / b, y};
	}
	if (a1 === piOver2) {
		return {x, y: b * x + d};
	}
	if (a2 === 0) {
		return {x: (y - c) / a, y};
	}
	if (a2 === piOver2) {
		return {x, y: a * x + c};
	}

	// intersection from two line equations
	// algo: http://en.wikipedia.org/wiki/Line–line_intersection#Given_the_equations_of_the_lines
	const newX = (d - c) / (a - b);

	return {
		x: newX,
		// this should work equally well with ax+c or bx+d
		y: a * newX + c,
	};
}

// return the angle between two points
export function lineAngle(p0, p1) {
	return Math.atan2(p1.y - p0.y, p1.x - p0.x) % (2 * Math.PI);
}

export function onLine(params) {
	if (params.on[0].x === params.on[1].x && params.on[0].y === params.on[1].y) {
		return 'x' in params ? params.on[0].y : params.on[0].x;
	}

	const origin = params.on[0];
	const vector = [
		params.on[1].x - params.on[0].x,
		params.on[1].y - params.on[0].y,
	];

	return 'x' in params
		? ((params.x - origin.x) / vector[0]) * vector[1] + origin.y
		: ((params.y - origin.y) / vector[1]) * vector[0] + origin.x;
}

export function pointOnCurve(
	pointHandleOut,
	handleOut,
	pointHandleIn,
	handleIn,
	distanceFromOut,
	inverseOrientation,
	linePrecision = 3,
) {
	let length = 0;
	let previousPoint;
	let points;

	if (inverseOrientation) {
		points = [pointHandleIn, handleIn, handleOut, pointHandleOut];
	}
	else {
		points = [pointHandleOut, handleOut, handleIn, pointHandleIn];
	}

	for (let i = 0; i < linePrecision; i++) {
		const point = getPointOnCurve(points, i / (linePrecision - 1));

		if (previousPoint) {
			length += distance(previousPoint.x, previousPoint.y, point.x, point.y);
		}

		previousPoint = point;
	}

	let t = length === 0 ? 0 : distanceFromOut / length;

	t = Math.max(0.001, Math.min(1, t));

	return getPointOnCurve(points, t);
}

export function getPointOnCurve(points, t) {
	const inverseT = 1 - t;
	const a = inverseT * inverseT * inverseT;
	const b = inverseT * inverseT * t * 3;
	const c = inverseT * t * t * 3;
	const d = t * t * t;

	return {
		x: a * points[0].x + b * points[1].x + c * points[2].x + d * points[3].x,
		y: a * points[0].y + b * points[1].y + c * points[2].y + d * points[3].y,
		normal: lineAngle(
			{
				x: 0,
				y: 0,
			},
			{
				x:
					(points[1].x - points[0].x) * inverseT * inverseT
					+ 2 * (points[2].x - points[1].x) * t * inverseT
					+ (points[3].x - points[2].x) * t * t,
				y:
					(points[1].y - points[0].y) * inverseT * inverseT
					+ 2 * (points[2].y - points[1].y) * t * inverseT
					+ (points[3].y - points[2].y) * t * t,
			},
		),
	};
}

export function split(points, t = 1, base) {
	let result = points;
	let current = points;

	while (current.length > 1) {
		const newPoints = [];

		for (let i = 1; i < current.length; i++) {
			newPoints.push(
				add2D(mulScalar2D(1 - t, current[i - 1]), mulScalar2D(t, current[i])),
			);
		}

		result = result.concat(newPoints);
		current = newPoints;
	}

	if (t === 1) {
		return {
			left: [base[1], base[0]],
			right: [base[1], base[1]],
		};
	}

	const splitBezier = {
		left: [
			{
				x: result[0].x,
				y: result[0].y,
				handleOut: {
					x: result[4].x,
					y: result[4].y,
				},
			},
			{
				x: result[9].x,
				y: result[9].y,
				handleIn: {
					x: result[7].x,
					y: result[7].y,
				},
				handleOut: {
					x: result[8].x,
					y: result[8].y,
				},
			},
		],
		right: [
			{
				x: result[9].x,
				y: result[9].y,
				handleIn: {
					x: result[7].x,
					y: result[7].y,
				},
				handleOut: {
					x: result[8].x,
					y: result[8].y,
				},
			},
			{
				x: result[3].x,
				y: result[3].y,
				handleIn: {
					x: result[6].x,
					y: result[6].y,
				},
			},
		],
	};

	return splitBezier;
}

export function distance(x1, y1, x2, y2) {
	return Math.sqrt((x2 - x1) ** 2 + (y1 - y2) ** 2);
}

export function align(points, lineStart, lineEnd) {
	const tx = lineStart.x;
	const ty = lineStart.y;
	const a = -Math.atan2(lineEnd.y - ty, lineEnd.x - tx);

	return points.map(v => ({
		x: (v.x - tx) * Math.cos(a) - (v.y - ty) * Math.sin(a),
		y: (v.x - tx) * Math.sin(a) + (v.y - ty) * Math.cos(a),
	}));
}

function crt(v) {
	return v < 0 ? -((-v) ** (1 / 3)) : v ** (1 / 3);
}

export function getIntersectionTValue(
	pointHandleOut,
	handleOut,
	pointHandleIn,
	handleIn,
	lineStart = {x: 0, y: 0},
	lineEnd = {x: 1, y: 0},
	points = [pointHandleOut, handleOut, handleIn, pointHandleIn],
) {
	const p = align(points, lineStart, lineEnd);

	function reduce(t) {
		return t >= 0 && t <= 1;
	} // eslint-disable-line yoda

	// see http://www.trans4mind.com/personal_development/mathematics/polynomials/cubicAlgebra.htm
	const pa = p[0].y;
	const pb = p[1].y;
	const pc = p[2].y;
	const pd = p[3].y;
	const d = -pa + 3 * pb - 3 * pc + pd;
	let a = 3 * pa - 6 * pb + 3 * pc;
	let b = -3 * pa + 3 * pb;
	let c = pa;
	let u1;
	let v1;
	let x1;
	let x2;
	let x3;

	if (approximately(d, 0)) {
		// this is not a cubic curve.
		if (approximately(a, 0)) {
			// in fact, this is not a quadratic curve either.
			if (approximately(b, 0)) {
				// in fact in fact, there are no solutions.
				return [];
			}
			// linear solution:
			return [-c / b].filter(reduce);
		}
		// quadratic solution:
		const q = Math.sqrt(b * b - 4 * a * c);
		const a2 = 2 * a;

		return [(q - b) / a2, (-b - q) / a2].filter(reduce);
	}

	a /= d;
	b /= d;
	c /= d;

	const p3 = (3 * b - a * a) / 3 / 3;
	const q = (2 * a ** 3 - 9 * a * b + 27 * c) / 27;
	const q2 = q / 2;
	const discriminant = q2 ** 2 + p3 ** 3;

	let result;

	if (discriminant < 0) {
		const mp3 = -p3;
		const mp33 = mp3 * mp3 * mp3;
		const r = Math.sqrt(mp33);
		const t = -q / (2 * r);
		const cosphi
			= t < -1 // eslint-disable-line no-nested-ternary
				? -1
				: t > 1
					? 1
					: t;
		const phi = Math.acos(cosphi);
		const crtr = crt(r);
		const t1 = 2 * crtr;

		x1 = t1 * Math.cos(phi / 3) - a / 3;
		x2 = t1 * Math.cos((phi + Math.PI * 2) / 3) - a / 3;
		x3 = t1 * Math.cos((phi + 4 * Math.PI) / 3) - a / 3;
		result = [x1, x2, x3].filter(reduce);
	}
	else if (discriminant === 0) {
		u1 = q2 < 0 ? crt(-q2) : -crt(q2);
		x1 = 2 * u1 - a / 3;
		x2 = -u1 - a / 3;
		result = [x1, x2].filter(reduce);
	}
	else {
		const sd = Math.sqrt(discriminant);

		u1 = crt(-q2 + sd);
		v1 = crt(q2 + sd);
		result = [u1 - v1 - a / 3].filter(reduce);
	}

	return result;
}

// see https://github.com/Pomax/bezierjs/blob/gh-pages/lib/utils.js line 313
export function lineCurveIntersection(
	pointHandleOut,
	handleOut,
	pointHandleIn,
	handleIn,
	lineStart = {x: 0, y: 0},
	lineEnd = {x: 1, y: 0},
) {
	const points = [pointHandleOut, handleOut, handleIn, pointHandleIn];

	const result = getIntersectionTValue(
		pointHandleOut,
		handleOut,
		pointHandleIn,
		handleIn,
		lineStart,
		lineEnd,
		points,
	);

	return split(points, result[0] || 0, [pointHandleIn, pointHandleOut]);
}

export function log(...rest) {
	/* eslint-disable no-console */
	console.log(rest);
	/* eslint-enable no-console */
	return rest[0];
}

export function normalize(vector) {
	const x = vector.x;
	const y = vector.y;

	const norm = distance(0, 0, x, y);

	if (norm === 0) {
		return {
			x: 0,
			y: 0,
		};
	}

	return {
		x: x / norm,
		y: y / norm,
	};
}

export function vectorFromPoints(a, b) {
	return {
		x: b.x - a.x,
		y: b.y - a.y,
	};
}

export function parseInt(int) {
	return parseInt(int);
}

export function makeCurveInsideSerif(
	pAnchors,
	serifHeight,
	serifWidth,
	serifMedian,
	serifCurve,
	serifTerminal,
	thickness,
	midWidth,
	serifRotate,
) {
	const yDir = pAnchors.down ? -1 : 1;
	const xDir = pAnchors.left ? -1 : 1;

	const rotateRad
		= ((serifRotate * pAnchors.rotationAngle || 0) * Math.PI) / 180;
	const baseWidth = pAnchors.baseWidth;
	const baseDir = pAnchors.baseDir;
	const baseHeight = pAnchors.baseHeight;
	const stumpOpposite = pAnchors.opposite;
	const stumpBase = baseHeight;
	let stumpVector = {
		x: stumpOpposite.x - stumpBase.x,
		y: stumpOpposite.y - stumpBase.y,
	};

	if (baseHeight.x === stumpOpposite.x && baseHeight.y === stumpOpposite.y) {
		stumpVector = {
			x: -stumpOpposite.x + baseWidth.x,
			y: -stumpOpposite.y + baseWidth.y,
		};
	}
	const stumpNorm = distance(0, 0, stumpVector.x, stumpVector.y);
	const rotationCenter = pAnchors.rotationCenter;

	stumpVector = normalize(stumpVector);
	rotationCenter.typeIn = 'line';

	const topLeft = {
		x:
			rotationCenter.x
			+ (baseHeight.x - rotationCenter.x - serifHeight * xDir)
				* Math.cos(rotateRad)
			- (baseWidth.y - rotationCenter.y + serifWidth * yDir)
				* Math.sin(rotateRad),
		y:
			rotationCenter.y
			+ (baseWidth.y - rotationCenter.y + serifWidth * yDir)
				* Math.cos(rotateRad)
			+ (baseHeight.x - rotationCenter.x - serifHeight * xDir)
				* Math.sin(rotateRad),
	};
	const bottomLeft = {
		x:
			rotationCenter.x
			+ (baseHeight.x - rotationCenter.x - serifHeight * xDir)
				* Math.cos(rotateRad)
			- (baseHeight.y - rotationCenter.y) * Math.sin(rotateRad),
		y:
			rotationCenter.y
			+ (baseHeight.y - rotationCenter.y) * Math.cos(rotateRad)
			+ (baseHeight.x - rotationCenter.x - serifHeight * xDir)
				* Math.sin(rotateRad),
	};

	// We get the intersection with the left edge of the serif and the curve support
	// this operation is direction dependent
	let splitBase;

	if (pAnchors.inverseOrder) {
		splitBase = lineCurveIntersection(
			pAnchors.curveEnd,
			pAnchors.curveEnd.handleOut,
			pAnchors.baseWidth,
			pAnchors.baseWidth.handleIn,
			{x: topLeft.x, y: topLeft.y},
			{x: bottomLeft.x, y: bottomLeft.y},
		);
	}
	else {
		splitBase = lineCurveIntersection(
			pAnchors.baseWidth,
			pAnchors.baseWidth.handleOut,
			pAnchors.curveEnd,
			pAnchors.curveEnd.handleIn,
			{x: topLeft.x, y: topLeft.y},
			{x: bottomLeft.x, y: bottomLeft.y},
		);
	}

	// We chose a serifCenter depending on if the left edge intersect or not with
	// the curve support
	let serifCenter;
	let splitCurveEnd;

	if (!pAnchors.inverseOrder) {
		if (
			splitBase.right[0].x !== splitBase.right[1].x
			|| splitBase.right[0].y !== splitBase.right[1].y
		) {
			serifCenter = splitBase.right[0];
			splitCurveEnd = splitBase.right[1];
		}
		else {
			serifCenter = splitBase.left[0];
			splitCurveEnd = splitBase.left[1];
		}
	}
	else if (
		splitBase.left[0].x !== splitBase.left[1].x
		|| splitBase.left[0].y !== splitBase.left[1].y
	) {
		serifCenter = splitBase.left[1];
		splitCurveEnd = splitBase.left[0];
	}
	else {
		serifCenter = splitBase.right[1];
		splitCurveEnd = splitBase.right[0];
	}

	// The serif direction is the line from the serif center
	// to the serif left edge
	const serifDirection = vectorFromPoints(serifCenter, {
		x:
			rotationCenter.x
			+ (baseHeight.x - rotationCenter.x - serifHeight * xDir)
				* serifMedian
				* Math.cos(rotateRad)
			- (baseWidth.y - rotationCenter.y + serifWidth * yDir)
				* Math.sin(rotateRad),
		y:
			rotationCenter.y
			+ (baseWidth.y - rotationCenter.y + serifWidth * yDir)
				* Math.cos(rotateRad)
			+ (baseHeight.x - rotationCenter.x - serifHeight * xDir)
				* serifMedian
				* Math.sin(rotateRad),
	});

	const serifBasis = normalize(serifDirection);
	const serifRadDirection = Math.atan2(serifBasis.y, serifBasis.x);

	let pointOnCurveVar;
	let pointOnSerif;
	let pointWithCurve = {};
	let normalToCurve;

	if (pAnchors.inverseOrder) {
		pointWithCurve = pointOnCurve(
			splitCurveEnd,
			splitCurveEnd.handleOut,
			serifCenter,
			serifCenter.handleIn,
			serifCurve,
			true,
			200,
		);
	}
	else {
		pointWithCurve = pointOnCurve(
			serifCenter,
			serifCenter.handleOut,
			splitCurveEnd,
			splitCurveEnd.handleIn,
			serifCurve,
			false,
			200,
		);
	}

	if (serifCurve > 0) {
		normalToCurve = pointWithCurve.normal;
		pointOnCurveVar = {
			x: pointWithCurve.x,
			y: pointWithCurve.y,
			dirOut: pointWithCurve.normal,
			typeIn: 'line',
		};
		const curveRatio = Math.min(
			serifCurve / distance(0, 0, serifDirection.x, serifDirection.y),
			0.75,
		);

		pointOnSerif = {
			x: serifCenter.x + serifDirection.x * curveRatio,
			y: serifCenter.y + serifDirection.y * curveRatio,
			dirIn: serifRadDirection,
			dirOut: serifRadDirection,
		};
	}
	else {
		if (pAnchors.inverseOrder) {
			const relHandle = subtract2D(serifCenter, serifCenter.handleIn);

			normalToCurve = Math.atan2(relHandle.y, relHandle.x) + Math.PI;
		}
		else {
			const relHandle = subtract2D(serifCenter, serifCenter.handleOut);

			normalToCurve = Math.atan2(relHandle.y, relHandle.x) + Math.PI;
		}
		pointOnCurveVar = {
			x: serifCenter.x,
			y: serifCenter.y,
			typeIn: 'line',
		};
		pointOnSerif = {
			x: serifCenter.x,
			y: serifCenter.y,
			type: 'corner',
		};
	}
	const leftEdge = {
		x: serifCenter.x + serifDirection.x,
		y: serifCenter.y + serifDirection.y,
		dirIn: serifRadDirection,
		dirOut: rotateRad,
	};
	const rightEdge = {
		x:
			rotationCenter.x
			- (baseWidth.y - rotationCenter.y + serifWidth * midWidth * yDir)
				* Math.sin(rotateRad),
		y:
			rotationCenter.y
			+ (baseWidth.y - rotationCenter.y + serifWidth * midWidth * yDir)
				* Math.cos(rotateRad),
		dirIn: rotateRad,
		typeOut: 'line',
	};
	const serifRoot = {
		x: baseHeight.x,
		y: baseHeight.y,
		typeIn: 'line',
	};

	const rootVector = normalize(vectorFromPoints(serifRoot, rightEdge));
	const medianVector = normalize(vectorFromPoints(pointOnSerif, leftEdge));

	const terminalVector = normalize({
		x: rootVector.x + medianVector.x,
		y: rootVector.y + medianVector.y,
	});

	const midPoint = {
		x:
			(leftEdge.x + rightEdge.x) / 2
			+ serifTerminal * serifHeight * terminalVector.x,
		y:
			(leftEdge.y + rightEdge.y) / 2
			+ serifTerminal * serifHeight * terminalVector.y,
		dirIn: rotateRad,
		dirOut: rotateRad,
	};

	if (serifTerminal !== 0) {
		leftEdge.dirOut = Math.atan2(medianVector.y, medianVector.x);
		rightEdge.dirIn = Math.atan2(rootVector.y, rootVector.x);
	}
	else if (midWidth !== 1) {
		const dirOut = Math.atan2(
			leftEdge.y - rightEdge.y,
			leftEdge.x - rightEdge.x,
		);

		leftEdge.dirOut = dirOut;
		rightEdge.dirIn = dirOut;
		midPoint.dirIn = dirOut;
		midPoint.dirOut = dirOut;
	}

	const midStump = {
		x: serifRoot.x + (stumpNorm / 2) * stumpVector.x,
		y: serifRoot.y + (stumpNorm / 2) * stumpVector.y,
		dirOut: baseDir,
		typeIn: 'line',
	};

	const lastPoint = {
		x:
			pointOnCurveVar.x
			- (stumpNorm / 2) * Math.sin(normalToCurve) * yDir * xDir,
		y:
			pointOnCurveVar.y
			+ (stumpNorm / 2) * Math.cos(normalToCurve) * yDir * xDir,
		dirIn: normalToCurve,
		typeOut: 'line',
		type: 'corner',
	};

	if (serifCurve + serifHeight < 70) {
		midStump.tensionOut = 0;
		lastPoint.tensionIn = 0;
	}
	else {
		midStump.tensionOut = 1;
		lastPoint.tensionIn = 1;
	}

	return [
		pointOnCurveVar,
		pointOnSerif,
		leftEdge,
		midPoint,
		rightEdge,
		rotationCenter,
		serifRoot,
		midStump,
		lastPoint,
	];
}
