import { Vector3 } from './Vector3';
import { Sphere } from './Sphere';
import { Box3 } from './Box3';
import { Matrix4 } from './Matrix4';

export class Ray {
	constructor(
		public origin: Vector3 = new Vector3(),
		public direction: Vector3 = new Vector3()
	) { }
	set(origin: Vector3, direction: Vector3) {
		this.origin.copy(origin);
		this.direction.copy(direction);
		return this;
	}

	clone() {
		return new Ray().copy(this);
	}
	copy(ray: Ray) {
		this.origin.copy(ray.origin);
		this.direction.copy(ray.direction);
		return this;
	}

	at(t: number, target: Vector3) {
		return target.copy(this.direction).multiplyScalar(t).add(this.origin);
	}
	lookAt(v: Vector3) {
		this.direction.copy(v).sub(this.origin).normalize();
		return this;
	}
	recast(t: number) {
		var v1 = new Vector3();
		this.origin.copy(this.at(t, v1));
		return this;
	}

	closestPointToPoint(point: Vector3, out: Vector3 = new Vector3()) {

		out.subVectors(point, this.origin);

		var directionDistance = out.dot(this.direction);

		if (directionDistance < 0) return out.copy(this.origin);

		return out.copy(this.direction).multiplyScalar(directionDistance).add(this.origin);
	}

	distanceToPoint(point: Vector3) {
		return Math.sqrt(this.distanceSqToPoint(point));
	}

	distanceSqToPoint(point: Vector3) {

		var v1 = new Vector3();

		var directionDistance = v1.subVectors(point, this.origin).dot(this.direction);

		// point behind the ray

		if (directionDistance < 0) {

			return this.origin.distanceToSquared(point);

		}

		v1.copy(this.direction).multiplyScalar(directionDistance).add(this.origin);

		return v1.distanceToSquared(point);

	};

	intersectSphere(sphere: Sphere, out: Vector3) {

		var v1 = new Vector3();
		v1.subVectors(sphere.center, this.origin);
		var tca = v1.dot(this.direction);
		var d2 = v1.dot(v1) - tca * tca;
		var radius2 = sphere.radius * sphere.radius;

		if (d2 > radius2) return null;

		var thc = Math.sqrt(radius2 - d2);

		// t0 = first intersect point - entrance on front of sphere
		var t0 = tca - thc;

		// t1 = second intersect point - exit point on back of sphere
		var t1 = tca + thc;

		// test to see if both t0 and t1 are behind the ray - if so, return null
		if (t0 < 0 && t1 < 0) return null;

		// test to see if t0 is behind the ray:
		// if it is, the ray is inside the sphere, so return the second exit point scaled by t1,
		// in order to always return an intersect point that is in front of the ray.
		if (t0 < 0) return this.at(t1, out);

		// else t0 is in front of the ray, so return the first collision point scaled by t0
		return this.at(t0, out);
	};

	intersectsSphere(sphere: Sphere) {
		return this.distanceSqToPoint(sphere.center) <= (sphere.radius * sphere.radius);
	}

	intersectBox(box: Box3, out: Vector3) {

		var tmin, tmax, tymin, tymax, tzmin, tzmax;

		var invdirx = 1 / this.direction.x,
			invdiry = 1 / this.direction.y,
			invdirz = 1 / this.direction.z;

		var origin = this.origin;

		if (invdirx >= 0) {

			tmin = (box.min.x - origin.x) * invdirx;
			tmax = (box.max.x - origin.x) * invdirx;

		} else {

			tmin = (box.max.x - origin.x) * invdirx;
			tmax = (box.min.x - origin.x) * invdirx;

		}

		if (invdiry >= 0) {

			tymin = (box.min.y - origin.y) * invdiry;
			tymax = (box.max.y - origin.y) * invdiry;

		} else {

			tymin = (box.max.y - origin.y) * invdiry;
			tymax = (box.min.y - origin.y) * invdiry;

		}

		if ((tmin > tymax) || (tymin > tmax)) return null;

		// These lines also handle the case where tmin or tmax is NaN
		// (result of 0 * Infinity). x !== x returns true if x is NaN

		if (tymin > tmin || tmin !== tmin) tmin = tymin;

		if (tymax < tmax || tmax !== tmax) tmax = tymax;

		if (invdirz >= 0) {

			tzmin = (box.min.z - origin.z) * invdirz;
			tzmax = (box.max.z - origin.z) * invdirz;

		} else {

			tzmin = (box.max.z - origin.z) * invdirz;
			tzmax = (box.min.z - origin.z) * invdirz;

		}

		if ((tmin > tzmax) || (tzmin > tmax)) return null;

		if (tzmin > tmin || tmin !== tmin) tmin = tzmin;

		if (tzmax < tmax || tmax !== tmax) tmax = tzmax;

		//return point closest to the ray (positive side)

		if (tmax < 0) return null;

		return this.at(tmin >= 0 ? tmin : tmax, out);

	}

	intersectsBox(box: Box3) {
		return this.intersectBox(box, new Vector3()) !== null;
	};

	intersectTriangle(
		a:Vector3,
		b:Vector3,
		c:Vector3,
		backfaceCulling:boolean,
		out:Vector3
	) {

		// Compute the offset origin, edges, and normal.
		var diff = new Vector3();
		var edge1 = new Vector3();
		var edge2 = new Vector3();
		var normal = new Vector3();



		// from http://www.geometrictools.com/GTEngine/Include/Mathematics/GteIntrRay3Triangle3.h

		edge1.subVectors(b, a);
		edge2.subVectors(c, a);
		normal.crossVectors(edge1, edge2);

		// Solve Q + t*D = b1*E1 + b2*E2 (Q = kDiff, D = ray direction,
		// E1 = kEdge1, E2 = kEdge2, N = Cross(E1,E2)) by
		//   |Dot(D,N)|*b1 = sign(Dot(D,N))*Dot(D,Cross(Q,E2))
		//   |Dot(D,N)|*b2 = sign(Dot(D,N))*Dot(D,Cross(E1,Q))
		//   |Dot(D,N)|*t = -sign(Dot(D,N))*Dot(Q,N)
		var DdN = this.direction.dot(normal);
		var sign;

		if (DdN > 0) {

			if (backfaceCulling) return null;
			sign = 1;

		} else if (DdN < 0) {

			sign = - 1;
			DdN = - DdN;

		} else {

			return null;

		}

		diff.subVectors(this.origin, a);
		var DdQxE2 = sign * this.direction.dot(edge2.crossVectors(diff, edge2));

		// b1 < 0, no intersection
		if (DdQxE2 < 0) {

			return null;

		}

		var DdE1xQ = sign * this.direction.dot(edge1.cross(diff));

		// b2 < 0, no intersection
		if (DdE1xQ < 0) {

			return null;

		}

		// b1+b2 > 1, no intersection
		if (DdQxE2 + DdE1xQ > DdN) {

			return null;

		}

		// Line intersects triangle, check if ray does.
		var QdN = - sign * diff.dot(normal);

		// t < 0, no intersection
		if (QdN < 0) {

			return null;

		}

		// Ray intersects triangle.
		return this.at(QdN / DdN, out);

	};



	applyMatrix4(matrix4: Matrix4) {
		this.origin.applyMatrix4(matrix4);
		this.direction.transformDirection(matrix4);
		return this;
	};

	equals(ray: Ray) {
		return ray.origin.equals(this.origin) && ray.direction.equals(this.direction);
	}
}


// function vec3Cross(v1, v2) {
//     v1 = v1.elements;
//     v2 = v2.elements;
//     return new Vector3([
//         v1[1] * v2[2] - v1[2] * v2[1],
//         v1[2] * v2[0] - v1[0] * v2[2],
//         v1[0] * v2[1] - v1[1] * v2[0],
//     ])
// }

// function vec3Sub(v1, v2) {
//     v1 = v1.elements;
//     v2 = v2.elements
//     return new Vector3([v1[0] - v2[0], v1[1] - v2[1], v1[2] - v2[2]]);
// }

// function vec3Dot(v1, v2) {
//     v1 = v1.elements;
//     v2 = v2.elements
//     return v1[0] * v2[0] + v1[1] + v2[1] + v1[2] + v2[2];
// }

// // Determine whether a ray intersect with a triangle
// // Parameters
// // orig: origin of the ray
// // dir: direction of the ray
// // v0, v1, v2: vertices of triangle
// // t(out): weight of the intersection for the ray
// // u(out), v(out): barycentric coordinate of intersection

// function intersectTriangle(orig, dir, v0, v1, v2) {
//     let t, u, v;

//     // E1
//     const E1 = vec3Sub(v1, v0);

//     // E2
//     const E2 = vec3Sub(v2, v0);

//     // P
//     const P = vec3Cross(dir, E2);

//     // determinant
//     let det = vec3Dot(E1, P);

//     // keep det > 0, modify T accordingly
//     let T;
//     if (det > 0) {
//         T = vec3Sub(orig, v0);
//     } else {
//         T = vec3Sub(v0, orig);
//         det = -det;
//     }

//     // If determinant is near zero, ray lies in plane of triangle
//     if (det < 0.0001) return false;

//     // Calculate u and make sure u <= 1
//     u = vec3Dot(T, P);
//     if (u < 0.0 || u > det) return false;

//     // Q
//     const Q = vec3Cross(T, E1);

//     // Calculate v and make sure u + v <= 1
//     v = vec3Dot(dir, Q);
//     if (v < 0.0 || u + v > det) return false;

//     // Calculate t, scale parameters, ray intersects triangle
//     t = vec3Dot(E2, Q);

//     const fInvDet = 1.0 / det;

//     t *= fInvDet;
//     u *= fInvDet;
//     v *= fInvDet;

//     console.log(t, u, v);

//     return true;
// }

// const intersect = intersectTriangle(
//     new Vector3([0, 0, 0]), new Vector3([0, 0, 1]),
//     new Vector3([-1, 1, 1]),
//     new Vector3([1, 1, 1]),
//     new Vector3([0, -1, 1]),
// );

// console.log(intersect);



