/**
 * @classdesc A HobbyCurve/HobbyPath calculation class: compute a set of optimal
 *            cubic Bézier curves from a sequence of vertices.
 *
 * This Hobby curve (path) implementation was strongly inspired by
 * the one by Prof. Dr. Edmund Weitz:
 * Here's the website:
 *  http://weitz.de/hobby/
 *
 * @date     2020-04-07
 * @author   Transformed to a JS-class by Ikaros Kappler
 * @modified 2020-08-19 Ported from vanilla JS to TypeScript.
 * @version 1.0.1
 *
 * @file HobbyPath
 * @public
 **/

import { CubicBezierCurve } from "../../CubicBezierCurve";
import { Vertex } from "../../Vertex";

interface IControlPoints {
    startControlPoints : Array<Vertex>;
    endControlPoints : Array<Vertex>;
};


/**
 * @classdesc A HobbyCurve/HobbyPath calculation class: compute a set of optimal
 *            cubic Bézier curves from a sequence of vertices.
 *
 * @requires CubicBezierCurve
 * @requires Vertex
 */
export class HobbyPath {

    /**
     * @member {Array<Vertex>} vertices
     * @memberof HobbyPath
     * @type {Array<Vertex>}
     * @instance
     **/
    private vertices : Array<Vertex>;

    /**
     * @constructor
     * @name HobbyPath
     * @param {Array<Vertex>=} vertices? - An optional array of vertices to initialize the path with.
     **/
    constructor( vertices?:Array<Vertex> ) {
	this.vertices = vertices ? vertices : [];
    };


    /**
     * Add a new point to the end of the vertex sequence.
     *
     * @name addPoint
     * @memberof HobbyPath
     * @instance
     * @param {Vertex} p - The vertex (point) to add.
     **/
    addPoint(p: Vertex) : void {
	this.vertices.push( p );
    };


    /**
     * Generate a sequence of cubic Bézier curves from the point set.
     *
     * @name generateCurve
     * @memberof HobbyPath
     * @instance
     * @param {boolean=} circular - Specify if the path should be closed.
     * @param {number=0} omega - (default=0) An optional tension parameter.
     * @return Array<CubicBezierCurve>
     **/
    generateCurve( circular?:boolean, omega?:number ) : Array<CubicBezierCurve> {
	let n : number = this.vertices.length;	
	if (n > 1) {
	    if (n == 2) {
		// for two points, just draw a straight line
		return [ new CubicBezierCurve(
		    this.vertices[0],
		    this.vertices[1],
		    this.vertices[0],
		    this.vertices[1] 
		) ];
	    } else {
		const curves : Array<CubicBezierCurve> = [];
		let controlPoints = this.hobbyControls(circular,omega);
		for (let i = 0; i < n - (circular?0:1); i++) {
		    // if i is n-1, the "next" point is the first one
		    let j : number = (i+1) % n; // Use a succ function here?
		    curves.push( new CubicBezierCurve(
			this.vertices[i],
			this.vertices[j],
			controlPoints.startControlPoints[i], 
			controlPoints.endControlPoints[i] 
		    ) );
		}
		return curves;
	    }
	} else {
	    return [];
	}	
    };
    

    /**
     * Computes the control point coordinates for a Hobby curve through
     * the points given.
     *
     * @name hobbyControls
     * @memberof HobbyPath
     * @instance
     * @param {boolean}  circular - If true, then the path will be closed.
     * @param {number=0} omega    - The 'curl' or the path.
     * @return {IControlPoints} An object with two members: startControlPoints and endControlPoints (Array<Vertex>).
     **/
    private hobbyControls(circular?:boolean, omega?:number) : IControlPoints {
	// This is a version that works for both, closed and non-closed paths.

	if( typeof omega === 'undefined' )
	    omega = 0;
	
	let n : number = this.vertices.length - (circular ? 0 : 1);
	let D : Array<number> = new Array<number>(n);

	let ds : Array<Vertex> = new Array<Vertex>(n);
	var succ = (i:number) : number => { return circular ? ((i+1)%n) : (i+1) };
	var pred = (i:number) : number => { return circular ? ((i + n - 1) % n) : (i-1) };
				 	
	for (let i = 0; i < n; i++) {
	    // the "next" point in a modular way
	    let j : number = succ(i);
	    ds[i] = this.vertices[i].difference( this.vertices[j] );
	    D[i] = Math.sqrt(ds[i].x*ds[i].x+ds[i].y*ds[i].y);
	}
	let gamma : Array<number> = new Array(n + (circular?0:1));
	for (let i = (circular?0:1); i < n; i++) {
	    // the "previous" point in a modular way
	    let k : number = pred(i);
	    let sin = ds[k].y / D[k];
	    let cos = ds[k].x / D[k];
	    let vec = HobbyPath.utils.rotate(ds[i], -sin, cos);
	    gamma[i] = Math.atan2(vec.y, vec.x);
	}
	if( !circular )
	    gamma[n] = 0;
	let a : Array<number> = new Array(n + (circular?0:1));
	let b : Array<number> = new Array(n + (circular?0:1));
	let c : Array<number> = new Array(n + (circular?0:1));
	let d : Array<number> = new Array(n + (circular?0:1));
	for (let i = (circular?0:1); i < n; i++) {
	    // j is the "next" point, k the "previous" one
	    let j : number = succ(i);
	    let k : number = pred(i);
	    // see video for the equations
	    a[i] = 1 / D[k];
	    b[i] = (2*D[k]+2*D[i])/(D[k]*D[i]);
	    c[i] = 1 / D[i];
	    d[i] = -(2*gamma[i]*D[i]+gamma[j]*D[k])/(D[k]*D[i]);
	} 
	// make matrix tridiagonal in preparation for the "sherman" function
	var alpha : Array<number>;
	var beta : Array<number>;
	if( circular ) {
	    let s : number = a[0]*omega; // Use omega here?
	    a[0] = 0;
	    let t : number = c[n-1]*omega; // Use omega here?
	    c[n-1] = 0;
	    alpha = HobbyPath.utils.sherman(a, b, c, d, s, t);
	    beta = new Array<number>(n);
	    for (let i = 0; i < n - (circular?0:1); i++) {
		// "next" point
		let j : number = succ(i);
		beta[i] = -gamma[j]-alpha[j];
	    }
	} else {
	    // see the Jackowski article for the following values; the result
	    // will be that the curvature at the first point is identical to the
	    // curvature at the second point (and likewise for the last and
	    // second-to-last)
	    b[0] = 2 + omega;
	    c[0] = 2 * omega + 1;
	    d[0] = -c[0] * gamma[1];
	    a[n] = 2 * omega + 1;
	    b[n] = 2 + omega;
	    d[n] = 0;
	    // solve system for the angles called "alpha" in the video
	    alpha = HobbyPath.utils.thomas(a, b, c, d);
	    // compute "beta" angles from "alpha" angles
	    beta = new Array<number>(n);
	    for (let i = 0; i < n - 1; i++)
		beta[i] = -gamma[i+1]-alpha[i+1];
	    // again, see Jackowski article
	    beta[n-1] = -alpha[n];
	}
	let startControlPoints = new Array<Vertex>(n);
	let endControlPoints = new Array<Vertex>(n);
	for (let i = 0; i < n; i++) {
	    let j : number = succ(i);
	    let a : number = HobbyPath.utils.rho(alpha[i], beta[i]) * D[i] / 3;
	    let b : number = HobbyPath.utils.rho(beta[i], alpha[i]) * D[i] / 3;
	    let v : Vertex = HobbyPath.utils.normalize(HobbyPath.utils.rotateAngle(ds[i], alpha[i]));
	    startControlPoints[i] = new Vertex( this.vertices[i].x + a * v.x, this.vertices[i].y + a * v.y );
	    v = HobbyPath.utils.normalize(HobbyPath.utils.rotateAngle(ds[i], -beta[i]));
	    endControlPoints[i] = new Vertex( this.vertices[j].x - b * v.x, this.vertices[j].y - b * v.y );
	}
	return { startControlPoints : startControlPoints,
		 endControlPoints: endControlPoints
	       };
    }

    static utils = {
	// rotates a vector [x, y] about an angle; the angle is implicitly
	// determined by its sine and cosine
	rotate : (vert: Vertex,
		  sin: number,
		  cos: number) : Vertex => {
	    return new Vertex( vert.x*cos - vert.y*sin, vert.x*sin + vert.y*cos );
	},
	// rotates a vector [x, y] about the angle alpha
	rotateAngle: (vert: Vertex, alpha: number) : Vertex => {
	    return HobbyPath.utils.rotate(vert, Math.sin(alpha), Math.cos(alpha));
	},
	// returns a normalized version of the vector
	normalize : (vec: Vertex) : Vertex => {
	    let n = Math.hypot(vec.x, vec.y);
	    if (n == 0)
		return new Vertex(0,0);
	    else
		return new Vertex( vec.x/n, vec.y/n ); // TODO: do in-place
	},
	// the "velocity function" (also called rho in the video); a and b are
	// the angles alpha and beta, the return value is the distance between
	// a control point and its neighboring point; to compute sigma(a,b)
	// we'll simply use rho(b,a)
	rho : (a: number ,
	       b: number) : number => {
	    // see video for formula
	    let sa = Math.sin(a);
	    let sb = Math.sin(b);
	    let ca = Math.cos(a);
	    let cb = Math.cos(b);
	    let s5 = Math.sqrt(5);
	    let num = 4 + Math.sqrt(8) * (sa - sb/16) * (sb - sa/16) * (ca - cb);
	    let den = 2 + (s5 - 1) * ca + (3 - s5) * cb;
	    return num/den;
	},
	// Implements the Thomas algorithm for a tridiagonal system with i-th
	// row a[i]x[i-1] + b[i]x[i] + c[i]x[i+1] = d[i] starting with row
	// i=0, ending with row i=n-1 and with a[0] = c[n-1] = 0.  Returns the
	// values x[i] as an array.
	thomas : (a: Array<number>,
		  b: Array<number>,
		  c: Array<number>,
		  d: Array<number>) : Array<number> => {
	    let n : number = a.length;
	    let cc : Array<number> = new Array<number>(n);
	    let dd : Array<number> = new Array<number>(n);
	    // forward sweep
	    cc[0] = c[0] / b[0];
	    dd[0] = d[0] / b[0];
	    for (let i = 1; i < n; i++) {
		let den : number = b[i] - cc[i-1]*a[i];
		cc[i] = c[i] / den;
		dd[i] = (d[i] - dd[i-1]*a[i]) / den;
	    }
	    let x : Array<number> = new Array<number>(n);
	    // back substitution
	    x[n-1] = dd[n-1];
	    for (let i = n-2; i >= 0; i--)
		x[i] = dd[i] - cc[i]*x[i+1];
	    return x;
	},
	// Solves an "almost" tridiagonal linear system with i-th row
	// a[i]x[i-1] + b[i]x[i] + c[i]x[i+1] = d[i] starting with row i=0,
	// ending with row i=n-1 and with a[0] = c[n-1] = 0.  Returns the
	// values x[i] as an array.  The system is not really tridiagonal
	// because the 0-th row is b[0]x[0] + c[0]x[1] + sx[n-1] = d[0] and
	// row n-1 is tx[0] + a[n-1]x[n-2] + b[n-1]x[n-1] = d[n-1].  The
	// Sherman-Morrison-Woodbury formula is used so that the function
	// "thomas" can be called to solve the system.
	sherman : ( a: Array<number>,
		    b: Array<number>,
		    c: Array<number>,
		    d: Array<number>,
		    s: number ,
		    t: number) : Array<number> => {
	    const n : number = a.length;
	    const u : Array<number> = new Array<number>(n);
	    u.fill(0, 1, n-1);
	    u[0] = 1;
	    u[n-1] = 1;
	    let v : Array<number> = new Array<number>(n);
	    v.fill(0, 1, n-1);
	    v[0] = t;
	    v[n-1] = s;
	    b[0] -= t;
	    b[n-1] -= s;
	    // this would be more efficient if computed in parallel, but hey...
	    const Td : Array<number> = HobbyPath.utils.thomas(a, b, c, d);
	    const Tu : Array<number> = HobbyPath.utils.thomas(a, b, c, u);
	    const factor : number = (t*Td[0] + s*Td[n-1]) / (1 + t*Tu[0] + s*Tu[n-1]);
	    const x = new Array(n);
	    for (let i = 0; i < n; i++)
		x[i] = Td[i] - factor * Tu[i];
	    return x;
	}
    };
}; // END class
