/** 
 * @author   mbostock, extended and ported to TypeScript by Ikaros Kappler
 * @date     2020-05-19
 * @modified 2021-02-08 Fixed a lot of es2015 compatibility issues.
 * @version  1.0.2
 * @file     contextPolygonIncircle
 * @public
 */

import { Circle } from "../../Circle";
import { geomutils } from "../../geomutils";
import { Line } from "../../Line";
import { Polygon } from "../../Polygon";
import { Triangle } from "../../Triangle";
import { Vertex } from "../../Vertex";


/**
 * For circle-polygon-intersection-count detection we need an epsilon to
 * eliminate smaller precision errors.
 */
const EPS : number = 0.000001;

export interface PolygonIncircle {
    circle : Circle;
    triangle : Triangle;
};

/**
 * Compute the max sized inlying circle in the given convex (!) polygon - also called the
 * convex-polygon incircle.
 *
 * The function will return an object with either: the circle, and the triangle that defines
 * the three tangent points where the circle touches the polygon.
 *
 * Inspired by
 *  https://observablehq.com/@mbostock/convex-polygon-incircle
 *  https://observablehq.com/@mbostock/circle-tangent-to-three-lines
 *
 *
 * @requires Circle
 * @requires Line
 * @requires Vertex
 * @requires Triangle
 * @requires nsectAngle
 * @requires geomutils
 *
 * @global
 * @name convexPolygonIncircle
 * @param {Polygon} convexHull - The actual convex polygon.
 * @return {PolygonIncircle} A pair of a circle (the incircle) and a triangle (the three points where the circle is touching the polygon).
 */
export const convexPolygonIncircle = ( convexHull:Polygon ) : PolygonIncircle => {
    var n : number = convexHull.vertices.length;
    var bestCircle : Circle|undefined = undefined;
    var bestTriangle : Triangle|undefined = undefined;
    for( var a = 0; a < n; a++ ) {
	for( var b = a+1; b < n; b++ ) {
	    for( var c = b+1; c < n; c++ ) {
		// As these lines are part of the convex hull, we know that
		//  * line a preceeds line b and
		//  * line b preceeds line c :)
		let lineA : Line = new Line( convexHull.vertices[a], convexHull.vertices[(a+1)%n] );
		let lineB : Line = new Line( convexHull.vertices[b], convexHull.vertices[(b+1)%n] );
		let lineC : Line = new Line( convexHull.vertices[c], convexHull.vertices[(c+1)%n] );

		// Find intersections by expanding the lines
		let vertB : Vertex = lineA.intersection(lineB);
		let vertC : Vertex = lineB.intersection(lineC);

		// An object: { center: Vertex, radius: number }
		let triangle : Triangle = getTangentTriangle4( lineA.a, vertB, vertC, lineC.b );
		// Workaround. There will be a future version where the 'getCircumCircle()' functions
		// returns a real Circle instance.
		let _circle : Circle = triangle.getCircumcircle();
		let circle : Circle = new Circle( _circle.center, _circle.radius );

		// Count the number of intersections with the convex hull:
		// If there are exactly three, we have found an in-lying circle.
		//  * Check if this one is better (bigger) than the old one.
		//  * Also check if the circle is located inside the polygon;
		//    The construction can, in some cases, produce an out-lying circle.
		if( !convexHull.containsVert(circle.center) ) {
		    continue;
		}
		var circleIntersections : Array<number> = findCircleIntersections( convexHull, circle );
		if( circleIntersections.length == 3 && (bestCircle == undefined || bestCircle.radius < circle.radius) ) {
		    bestCircle = circle;
		    bestTriangle = triangle;
		}
	    }
	}
    }
    return { circle : bestCircle,
	     triangle : bestTriangle };
};


/**
 * This function computes the three points for the inner maximum circle 
 * lying tangential to the three subsequential lines (given by four points).
 *
 * Compute the circle from that triangle by using Triangle.getCircumcircle().
 *
 * Not all three lines should be parallel, otherwise the circle might have infinite radius.
 *
 * LineA := [vertA, vertB]
 * LineB := [vertB, vertC]
 * LineC := [vertC, vertD]
 *
 * @param {Vertex} vertA - The first point of the three connected lines.
 * @param {Vertex} vertB - The second point of the three connected lines.
 * @param {Vertex} vertC - The third point of the three connected lines.
 * @param {Vertex} vertD - The fourth point of the three connected lines.
 * @return {Triangle} The triangle of the circular tangential points with the given lines (3 or 4 of them).
 */
const getTangentTriangle4 = ( vertA : Vertex,
			      vertB : Vertex,
			      vertC : Vertex,
			      vertD : Vertex ) : Triangle => {
    const lineA : Line = new Line(vertA,vertB);
    const lineB : Line = new Line(vertB,vertC);
    const lineC : Line = new Line(vertC,vertD);

    const bisector1 : Line = geomutils.nsectAngle( vertB, vertA, vertC, 2 )[0]; // bisector of first triangle
    const bisector2 : Line  = geomutils.nsectAngle( vertC, vertB, vertD, 2 )[0]; // bisector of second triangle
    const intersection : Vertex = bisector1.intersection( bisector2 );

    // Find the closest points on one of the polygon lines (all have same distance by construction)
    const circleIntersA : Vertex = lineA.getClosestPoint( intersection );
    const circleIntersB : Vertex = lineB.getClosestPoint( intersection );
    const circleIntersC : Vertex = lineC.getClosestPoint( intersection );

    return new Triangle(circleIntersA, circleIntersB, circleIntersC);
};


/**
 * Find all intersecting lines (indices) on the polyon that intersect the circle.
 *
 * @param {Polygon} convexHull - The polygon to detect the intersections on (here this is the convex hull given).
 * @param {Circle} circle - The circle to detect the intersections with.
 * @return {Array<number>} The indices of those lines that intersect (or touch) the circle.
 **/
const findCircleIntersections = ( convexHull:Polygon, circle:Circle ) : Array<number> => {
    var result = [];
    for( var i = 0; i < convexHull.vertices.length; i++ ) {
	var line : Line = new Line( convexHull.vertices[i], convexHull.vertices[(i+1)%convexHull.vertices.length] );
	// Use an epsilon here because circle coordinates can be kind of unprecise in the detail.
	if( circle.lineDistance(line) < EPS ) {
	    result.push( i );
	}
    }
    return result;
};


