import { Vec2dModel } from '@tldraw/tlschema'
import { EASINGS } from './easings'

/** @public */
export type VecLike = Vec2d | Vec2dModel

/** @public */
export class Vec2d {
	constructor(public x = 0, public y = 0, public z = 1) {}

	get pressure() {
		return this.z
	}

	set(x = this.x, y = this.y, z = this.z) {
		this.x = x
		this.y = y
		this.z = z
		return this
	}

	setTo({ x = 0, y = 0, z = 1 }: VecLike) {
		this.x = x
		this.y = y
		this.z = z
		return this
	}

	rot(r: number) {
		if (r === 0) return this
		const { x, y } = this
		const s = Math.sin(r)
		const c = Math.cos(r)
		this.x = x * c - y * s
		this.y = x * s + y * c
		return this
	}

	rotWith(C: VecLike, r: number) {
		if (r === 0) return this
		const x = this.x - C.x
		const y = this.y - C.y
		const s = Math.sin(r)
		const c = Math.cos(r)
		this.x = C.x + (x * c - y * s)
		this.y = C.y + (x * s + y * c)
		return this
	}

	clone(): Vec2d {
		const { x, y, z } = this
		return new Vec2d(x, y, z)
	}

	sub(V: VecLike) {
		this.x -= V.x
		this.y -= V.y
		return this
	}

	subXY(x: number, y: number) {
		this.x -= x
		this.y -= y
		return this
	}

	subScalar(n: number) {
		this.x -= n
		this.y -= n
		// this.z -= n

		return this
	}

	add(V: VecLike) {
		this.x += V.x
		this.y += V.y
		return this
	}

	addXY(x: number, y: number) {
		this.x += x
		this.y += y
		return this
	}

	addScalar(n: number) {
		this.x += n
		this.y += n
		// this.z += n

		return this
	}

	clamp(min: number, max?: number) {
		this.x = Math.max(this.x, min)
		this.y = Math.max(this.y, min)
		if (max !== undefined) {
			this.x = Math.min(this.x, max)
			this.y = Math.min(this.y, max)
		}
		return this
	}

	div(t: number) {
		this.x /= t
		this.y /= t
		// this.z /= t
		return this
	}

	divV(V: VecLike) {
		this.x /= V.x
		this.y /= V.y
		// this.z /= V.z
		return this
	}

	mul(t: number) {
		this.x *= t
		this.y *= t
		// this.z *= t
		return this
	}

	mulV(V: VecLike) {
		this.x *= V.x
		this.y *= V.y
		// this.z *= V.z
		return this
	}

	abs() {
		this.x = Math.abs(this.x)
		this.y = Math.abs(this.y)
		return this
	}

	nudge(B: VecLike, distance: number) {
		const tan = Vec2d.Tan(B, this)
		return this.add(tan.mul(distance))
	}

	neg() {
		this.x *= -1
		this.y *= -1
		// this.z *= -1
		return this
	}

	cross(V: VecLike) {
		this.x = this.y * V.z! - this.z * V.y
		this.y = this.z * V.x - this.x * V.z!
		// this.z = this.x * V.y - this.y * V.x
		return this
	}

	dpr(V: VecLike): number {
		return Vec2d.Dpr(this, V)
	}

	cpr(V: VecLike) {
		return Vec2d.Cpr(this, V)
	}

	len2(): number {
		return Vec2d.Len2(this)
	}

	len(): number {
		return Vec2d.Len(this)
	}

	pry(V: VecLike): number {
		return Vec2d.Pry(this, V)
	}

	per() {
		const { x, y } = this
		this.x = y
		this.y = -x
		return this
	}

	uni() {
		return Vec2d.Uni(this)
	}

	tan(V: VecLike): Vec2d {
		return Vec2d.Tan(this, V)
	}

	dist(V: VecLike): number {
		return Vec2d.Dist(this, V)
	}

	distanceToLineSegment(A: VecLike, B: VecLike): number {
		return Vec2d.DistanceToLineSegment(A, B, this)
	}

	slope(B: VecLike): number {
		return Vec2d.Slope(this, B)
	}

	snapToGrid(gridSize: number) {
		this.x = Math.round(this.x / gridSize) * gridSize
		this.y = Math.round(this.y / gridSize) * gridSize
		return this
	}

	angle(B: VecLike): number {
		return Vec2d.Angle(this, B)
	}

	toAngle() {
		return Vec2d.ToAngle(this)
	}

	lrp(B: VecLike, t: number): Vec2d {
		this.x = this.x + (B.x - this.x) * t
		this.y = this.y + (B.y - this.y) * t
		return this
	}

	equals(B: VecLike) {
		return Vec2d.Equals(this, B)
	}

	equalsXY(x: number, y: number) {
		return Vec2d.EqualsXY(this, x, y)
	}

	norm() {
		const l = this.len()
		this.x = l === 0 ? 0 : this.x / l
		this.y = l === 0 ? 0 : this.y / l
		return this
	}

	toFixed() {
		return Vec2d.ToFixed(this)
	}

	toString() {
		return Vec2d.ToString(Vec2d.ToFixed(this))
	}

	toJson(): Vec2dModel {
		return Vec2d.ToJson(this)
	}

	toArray(): number[] {
		return Vec2d.ToArray(this)
	}

	static Add(A: VecLike, B: VecLike): Vec2d {
		return new Vec2d(A.x + B.x, A.y + B.y)
	}

	static AddXY(A: VecLike, x: number, y: number): Vec2d {
		return new Vec2d(A.x + x, A.y + y)
	}

	static Sub(A: VecLike, B: VecLike): Vec2d {
		return new Vec2d(A.x - B.x, A.y - B.y)
	}

	static SubXY(A: VecLike, x: number, y: number): Vec2d {
		return new Vec2d(A.x - x, A.y - y)
	}

	static AddScalar(A: VecLike, n: number): Vec2d {
		return new Vec2d(A.x + n, A.y + n)
	}

	static SubScalar(A: VecLike, n: number): Vec2d {
		return new Vec2d(A.x - n, A.y - n)
	}

	static Div(A: VecLike, t: number): Vec2d {
		return new Vec2d(A.x / t, A.y / t)
	}

	static Mul(A: VecLike, t: number): Vec2d {
		return new Vec2d(A.x * t, A.y * t)
	}

	static DivV(A: VecLike, B: VecLike): Vec2d {
		return new Vec2d(A.x / B.x, A.y / B.y)
	}

	static MulV(A: VecLike, B: VecLike): Vec2d {
		return new Vec2d(A.x * B.x, A.y * B.y)
	}

	static Neg(A: VecLike): Vec2d {
		return new Vec2d(-A.x, -A.y)
	}

	static Per(A: VecLike): Vec2d {
		return new Vec2d(A.y, -A.x)
	}

	static Dist2(A: VecLike, B: VecLike): number {
		return Vec2d.Sub(A, B).len2()
	}

	static Abs(A: VecLike): Vec2d {
		return new Vec2d(Math.abs(A.x), Math.abs(A.y))
	}

	static Dist(A: VecLike, B: VecLike): number {
		return Math.hypot(A.y - B.y, A.x - B.x)
	}

	static Dpr(A: VecLike, B: VecLike): number {
		return A.x * B.x + A.y * B.y
	}

	static Cross(A: VecLike, V: VecLike) {
		return new Vec2d(
			A.y * V.z! - A.z! * V.y,
			A.z! * V.x - A.x * V.z!
			// A.z = A.x * V.y - A.y * V.x
		)
	}

	static Cpr(A: VecLike, B: VecLike) {
		return A.x * B.y - B.x * A.y
	}

	static Len2(A: VecLike): number {
		return A.x * A.x + A.y * A.y
	}

	static Len(A: VecLike): number {
		return Math.sqrt(Vec2d.Len2(A))
	}

	static Pry(A: VecLike, B: VecLike): number {
		return Vec2d.Dpr(A, B) / Vec2d.Len(B)
	}

	static Uni(A: VecLike) {
		return Vec2d.Div(A, Vec2d.Len(A))
	}

	static Tan(A: VecLike, B: VecLike): Vec2d {
		return Vec2d.Uni(Vec2d.Sub(A, B))
	}

	static Min(A: VecLike, B: VecLike): Vec2d {
		return new Vec2d(Math.min(A.x, B.x), Math.min(A.y, B.y))
	}

	static Max(A: VecLike, B: VecLike): Vec2d {
		return new Vec2d(Math.max(A.x, B.x), Math.max(A.y, B.y))
	}

	static From({ x, y, z = 1 }: Vec2dModel) {
		return new Vec2d(x, y, z)
	}

	static FromArray(v: number[]): Vec2d {
		return new Vec2d(v[0], v[1])
	}

	static Rot(A: VecLike, r = 0): Vec2d {
		const s = Math.sin(r)
		const c = Math.cos(r)
		return new Vec2d(A.x * c - A.y * s, A.x * s + A.y * c)
	}

	static RotWith(A: VecLike, C: VecLike, r: number): Vec2d {
		const x = A.x - C.x
		const y = A.y - C.y
		const s = Math.sin(r)
		const c = Math.cos(r)
		return new Vec2d(C.x + (x * c - y * s), C.y + (x * s + y * c))
	}

	/**
	 * Get the nearest point on a line with a known unit vector that passes through point A
	 *
	 * ```ts
	 * Vec.nearestPointOnLineThroughPoint(A, u, Point)
	 * ```
	 *
	 * @param A - Any point on the line
	 * @param u - The unit vector for the line.
	 * @param P - A point not on the line to test.
	 */
	static NearestPointOnLineThroughPoint(A: VecLike, u: VecLike, P: VecLike): Vec2d {
		return Vec2d.Mul(u, Vec2d.Sub(P, A).pry(u)).add(A)
	}

	static NearestPointOnLineSegment(A: VecLike, B: VecLike, P: VecLike, clamp = true): Vec2d {
		const u = Vec2d.Tan(B, A)
		const C = Vec2d.Add(A, Vec2d.Mul(u, Vec2d.Sub(P, A).pry(u)))

		// todo: fix error P is B or A, which leads to a NaN value

		if (clamp) {
			if (C.x < Math.min(A.x, B.x)) return Vec2d.Cast(A.x < B.x ? A : B)
			if (C.x > Math.max(A.x, B.x)) return Vec2d.Cast(A.x > B.x ? A : B)
			if (C.y < Math.min(A.y, B.y)) return Vec2d.Cast(A.y < B.y ? A : B)
			if (C.y > Math.max(A.y, B.y)) return Vec2d.Cast(A.y > B.y ? A : B)
		}

		return C
	}

	static DistanceToLineThroughPoint(A: VecLike, u: VecLike, P: VecLike): number {
		return Vec2d.Dist(P, Vec2d.NearestPointOnLineThroughPoint(A, u, P))
	}

	static DistanceToLineSegment(A: VecLike, B: VecLike, P: VecLike, clamp = true): number {
		return Vec2d.Dist(P, Vec2d.NearestPointOnLineSegment(A, B, P, clamp))
	}

	static Snap(A: VecLike, step = 1) {
		return new Vec2d(Math.round(A.x / step) * step, Math.round(A.y / step) * step)
	}

	static Cast(A: VecLike): Vec2d {
		if (A instanceof Vec2d) return A
		return Vec2d.From(A)
	}

	static Slope(A: VecLike, B: VecLike): number {
		if (A.x === B.y) return NaN
		return (A.y - B.y) / (A.x - B.x)
	}

	static Angle(A: VecLike, B: VecLike): number {
		return Math.atan2(B.y - A.y, B.x - A.x)
	}

	static Lrp(A: VecLike, B: VecLike, t: number): Vec2d {
		return Vec2d.Sub(B, A).mul(t).add(A)
	}

	static Med(A: VecLike, B: VecLike): Vec2d {
		return new Vec2d((A.x + B.x) / 2, (A.y + B.y) / 2)
	}

	static Equals(A: VecLike, B: VecLike): boolean {
		return Math.abs(A.x - B.x) < 0.0001 && Math.abs(A.y - B.y) < 0.0001
	}

	static EqualsXY(A: VecLike, x: number, y: number): boolean {
		return A.x === x && A.y === y
	}

	static Clockwise(A: VecLike, B: VecLike, C: VecLike): boolean {
		return (C.x - A.x) * (B.y - A.y) - (B.x - A.x) * (C.y - A.y) < 0
	}

	static Rescale(A: VecLike, n: number) {
		const l = Vec2d.Len(A)
		return new Vec2d((n * A.x) / l, (n * A.y) / l)
	}

	static ScaleWithOrigin(A: VecLike, scale: number, origin: VecLike) {
		return Vec2d.Sub(A, origin).mul(scale).add(origin)
	}

	static ToFixed(A: VecLike, n = 2) {
		return new Vec2d(+A.x.toFixed(n), +A.y.toFixed(n), +A.z!.toFixed(n))
	}

	static Nudge(A: VecLike, B: VecLike, distance: number) {
		return Vec2d.Add(A, Vec2d.Tan(B, A).mul(distance))
	}

	static ToString(A: VecLike) {
		return `${A.x}, ${A.y}`
	}

	static ToAngle(A: VecLike) {
		let r = Math.atan2(A.y, A.x)
		if (r < 0) r += Math.PI * 2

		return r
	}

	static ToArray(A: VecLike) {
		return [A.x, A.y, A.z!]
	}

	static ToJson(A: VecLike) {
		const { x, y, z } = A
		return { x, y, z }
	}

	static Average(arr: VecLike[]) {
		const len = arr.length
		const avg = new Vec2d(0, 0)
		for (let i = 0; i < len; i++) {
			avg.add(arr[i])
		}
		return avg.div(len)
	}

	static Clamp(A: Vec2d, min: number, max?: number) {
		if (max === undefined) {
			return new Vec2d(Math.min(Math.max(A.x, min)), Math.min(Math.max(A.y, min)))
		}

		return new Vec2d(Math.min(Math.max(A.x, min), max), Math.min(Math.max(A.y, min), max))
	}

	/**
	 * Get an array of points (with simulated pressure) between two points.
	 *
	 * @param A - The first point.
	 * @param B - The second point.
	 * @param steps - The number of points to return.
	 */
	static PointsBetween(A: Vec2dModel, B: Vec2dModel, steps = 6): Vec2d[] {
		const results: Vec2d[] = []

		for (let i = 0; i < steps; i++) {
			const t = EASINGS.easeInQuad(i / (steps - 1))
			const point = Vec2d.Lrp(A, B, t)
			point.z = Math.min(1, 0.5 + Math.abs(0.5 - ease(t)) * 0.65)
			results.push(point)
		}

		return results
	}

	static SnapToGrid(A: VecLike, gridSize = 8) {
		return new Vec2d(Math.round(A.x / gridSize) * gridSize, Math.round(A.y / gridSize) * gridSize)
	}
}

const ease = (t: number) => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t)
