import { DiSpiroGeometry, SpiroGeometry } from "@iosevka/geometry";
import {
	AfBase,
	DecorInterpolator,
	InterpolatorBase,
	SpiroFlattener,
	TerminateInstruction,
	UserCloseKnotPair,
	UserControlKnot,
	VirtualControlKnot,
} from "@iosevka/geometry/spiro-control";
import { bez3, clamp, fallback, mix } from "@iosevka/util";

import { BiKnotCollector } from "../../geometry/src/spiro-expand.mjs";

///////////////////////////////////////////////////////////////////////////////////////////////////

class SpiroImplBase {
	constructor(bindings, controls) {
		this.bindings = bindings;
		this.controls = controls;
	}

	collectTo(collector) {
		const flattener = new SpiroFlattener();
		for (const control of this.controls) flattener.add(control);
		flattener.flatten();
		flattener.pipe(collector);
	}
}

class DispiroImpl extends SpiroImplBase {
	applyToGlyph(glyph) {
		const gizmo = glyph.gizmo || this.bindings.GlobalTransform;
		const collector = new BiKnotCollector(this.bindings.Contrast);
		this.collectTo(collector);
		const dsp = new DiSpiroProxy(gizmo, collector);
		glyph.includeGeometry(dsp.geometry);
		return dsp;
	}
}

class SpiroOutlineImpl extends SpiroImplBase {
	applyToGlyph(glyph) {
		const gizmo = glyph.gizmo || this.bindings.GlobalTransform;
		const collector = new BiKnotCollector(this.bindings.Contrast);
		this.collectTo(collector);
		return glyph.includeGeometry(
			new SpiroGeometry(gizmo, collector.closed, collector.getMonoKnots()),
		);
	}
}

class DiSpiroProxy {
	constructor(gizmo, collector) {
		this.geometry = new DiSpiroGeometry(
			gizmo,
			collector.contrast,
			collector.closed,
			collector.knots,
		);
		this.m_origKnots = collector.knots;
	}
	get knots() {
		return this.m_origKnots;
	}
	get lhsKnots() {
		return this.geometry.expand().lhsUntransformed;
	}
	get rhsKnots() {
		return this.geometry.expand().rhsUntransformed;
	}
}

/// The builder for directed knot pairs
function KnotType(type) {
	return (x, y, f) => {
		if (!UserControlKnot.isCoordinateValid(x)) throw new TypeError("NaN detected for X");
		if (!UserControlKnot.isCoordinateValid(y)) throw new TypeError("NaN detected for Y");
		return new UserControlKnot(type, x, y, f);
	};
}

function virtualKnot(x, y, f) {
	if (!UserControlKnot.isCoordinateValid(x)) throw new TypeError("NaN detected for X");
	if (!UserControlKnot.isCoordinateValid(y)) throw new TypeError("NaN detected for Y");
	return new VirtualControlKnot(x, y, f);
}

/// The builder for directed knot pairs
class DirectedKnotPairBuilder {
	constructor(bindings, kPre, kCenter, kPost, deltaX, deltaY) {
		const { TINY } = bindings;
		this.start = DirPairImpl(kPre, kCenter, kPost, deltaX, deltaY, 0, TINY);
		this.mid = DirPairImpl(kPre, kCenter, kPost, deltaX, deltaY, -0.5 * TINY, 0.5 * TINY);
		this.end = DirPairImpl(kPre, kCenter, kPost, deltaX, deltaY, -TINY, 0);
	}
}

function DirPairImpl(kPre, kCenter, kPost, dirX, dirY, dPre, dPost) {
	const tyPre = kPre(0, 0).type;
	const tyPost = kPost(0, 0).type;
	return (x, y, af) =>
		new UserCloseKnotPair(kCenter(x, y, af), tyPre, tyPost, dirX, dirY, dPre, dPost);
}

export function SetupBuilders(bindings) {
	const { Stroke, Superness } = bindings;

	// Simple knot types
	const g4 = KnotType("g4");
	const g2 = KnotType("g2");
	const corner = KnotType("corner");
	const flat = KnotType("left");
	const curl = KnotType("right");
	const virt = virtualKnot;
	const close = f => new TerminateInstruction("close", f);
	const end = f => new TerminateInstruction("end", f);

	// Pair knots
	const straight = { l: flat, r: curl };
	const g2c = { l: g2, r: corner };
	const cg2 = { l: corner, r: g2 };
	const flatc = { l: flat, r: corner };
	const ccurl = { l: corner, r: curl };

	// Add the directed/heading knot builders
	{
		// biome-ignore format: keep the columns aligned for readability
		const knotTypes = [
			[ g4,       g4,     g4,     g4     ],
			[ g2,       g2,     g2,     g2     ],
			[ corner,   corner, corner, corner ],
			[ straight, flat,   g2,     curl   ],
			[ g2c,      g2,     corner, corner ],
			[ cg2,      corner, corner, g2     ],
			[ flatc,    flat,   corner, corner ],
			[ ccurl,    corner, corner, curl   ],
		];
		const directions = [
			// Straights
			{ name: "up", x: 0, y: 1 },
			{ name: "down", x: 0, y: -1 },
			{ name: "left", x: -1, y: 0 },
			{ name: "right", x: 1, y: 0 },
			{ name: "u", x: 0, y: 1 },
			{ name: "d", x: 0, y: -1 },
			{ name: "l", x: -1, y: 0 },
			{ name: "r", x: 1, y: 0 },

			// Diagonals
			{ name: "ru", x: 1, y: 1 },
			{ name: "rd", x: 1, y: -1 },
			{ name: "lu", x: -1, y: 1 },
			{ name: "ld", x: -1, y: -1 },
		];
		for (const [sink, kl, kc, kr] of knotTypes) {
			sink.sl = s => new DirectedKnotPairBuilder(bindings, kl, kc, kr, -1, -s);
			sink.sr = s => new DirectedKnotPairBuilder(bindings, kl, kc, kr, 1, s);
			sink.dir = (dx, dy) => new DirectedKnotPairBuilder(bindings, kl, kc, kr, dx, dy);
			for (const d of directions) {
				sink[d.name] = new DirectedKnotPairBuilder(bindings, kl, kc, kr, d.x, d.y);
			}
		}
	}

	class AfSetWidths extends AfBase {
		constructor(l, r) {
			super();
			this.l = l;
			this.r = r;
		}
		applyTo(target) {
			target.setWidth(this.l, this.r);
		}
	}

	function widths(l, r) {
		if (!Number.isFinite(l)) throw new TypeError("NaN detected for left width");
		if (!Number.isFinite(r)) throw new TypeError("NaN detected for right width");
		return new AfSetWidths(l, r);
	}
	widths.lhs = w => {
		w = fallback(w, Stroke);
		if (!Number.isFinite(w)) throw new TypeError("NaN detected for left width");
		return widths(w, 0);
	};
	widths.rhs = w => {
		w = fallback(w, Stroke);
		if (!Number.isFinite(w)) throw new TypeError("NaN detected for left width");
		return widths(0, w);
	};
	widths.center = w => {
		w = fallback(w, Stroke);
		if (!Number.isFinite(w)) throw new TypeError("NaN detected for left width");
		return widths(w / 2, w / 2);
	};

	class AfHeading extends AfBase {
		constructor(d) {
			super();
			this.d = d;
		}
		applyTo(target) {
			target.headsTo(this.d);
		}
	}
	function heading(d) {
		if (!Number.isFinite(d.x) || !Number.isFinite(d.y))
			throw new TypeError("NaN detected for heading directions");
		return new AfHeading(d);
	}

	class AfWidthsHeading extends AfBase {
		constructor(l, r, d) {
			super();
			this.l = l;
			this.r = r;
			this.d = d;
		}
		applyTo(target) {
			target.setWidth(this.l, this.r);
			target.headsTo(this.d);
		}
	}

	widths.heading = (l, r, d) => {
		if (!Number.isFinite(l)) throw new TypeError("NaN detected for left width");
		if (!Number.isFinite(r)) throw new TypeError("NaN detected for right width");
		if (!Number.isFinite(d.x) || !Number.isFinite(d.y))
			throw new TypeError("NaN detected for heading directions");
		return new AfWidthsHeading(l, r, d);
	};
	widths.lhs.heading = (w, d) => {
		w = fallback(w, Stroke);
		if (!Number.isFinite(w)) throw new TypeError("NaN detected for left width");
		if (!Number.isFinite(d.x) || !Number.isFinite(d.y))
			throw new TypeError("NaN detected for heading directions");
		return new AfWidthsHeading(w, 0, d);
	};
	widths.rhs.heading = (w, d) => {
		w = fallback(w, Stroke);
		if (!Number.isFinite(w)) throw new TypeError("NaN detected for left width");
		if (!Number.isFinite(d.x) || !Number.isFinite(d.y))
			throw new TypeError("NaN detected for heading directions");
		return new AfWidthsHeading(0, w, d);
	};
	widths.center.heading = (w, d) => {
		w = fallback(w, Stroke);
		if (!Number.isFinite(w)) throw new TypeError("NaN detected for left width");
		if (!Number.isFinite(d.x) || !Number.isFinite(d.y))
			throw new TypeError("NaN detected for heading directions");
		return new AfWidthsHeading(w / 2, w / 2, d);
	};

	class AfDisableContrast extends AfBase {
		applyTo(target) {
			target.setContrast(1);
		}
	}
	function disableContrast() {
		return new AfDisableContrast();
	}

	class AfUnimportant extends AfBase {
		applyTo(target) {
			target.setUnimportant();
		}
	}
	const unimportant = new AfUnimportant();

	class AfImportant extends AfBase {
		applyTo(target) {
			target.setImportant();
		}
	}
	const important = new AfImportant();

	/// Simple (single mix) interpolator
	class SimpleMixInterpolator extends InterpolatorBase {
		constructor(ty, rx, ry, deltaX, deltaY, raf) {
			super();
			this.ty = ty;
			this.rx = rx;
			this.ry = fallback(ry, rx);
			this.deltaX = deltaX;
			this.deltaY = deltaY;
			this.raf = fallback(raf, unimportant);
		}
		resolveInterpolation(before, after) {
			return this.ty(
				mix(before.x, after.x, this.rx) + this.deltaX,
				mix(before.y, after.y, this.ry) + this.deltaY,
				this.raf,
			);
		}
	}

	class DistanceFromStartInterpolator extends InterpolatorBase {
		constructor(ty, distFromStart, deltaX, deltaY, raf) {
			super();
			this.ty = ty;
			this.distFromStart = distFromStart;
			this.deltaX = deltaX;
			this.deltaY = deltaY;
			this.raf = fallback(raf, unimportant);
		}
		resolveInterpolation(before, after) {
			const totalDist = Math.hypot(after.x - before.x, after.y - before.y);
			const r = clamp(0, 1, this.distFromStart / totalDist);
			return this.ty(
				mix(before.x, after.x, r) + this.deltaX,
				mix(before.y, after.y, r) + this.deltaY,
				this.raf,
			);
		}
	}

	class DistTillEndInterpolator extends InterpolatorBase {
		constructor(ty, distTillEnd, deltaX, deltaY, raf) {
			super();
			this.ty = ty;
			this.distTillEnd = distTillEnd;
			this.deltaX = deltaX;
			this.deltaY = deltaY;
			this.raf = fallback(raf, unimportant);
		}
		resolveInterpolation(before, after) {
			const totalDist = Math.hypot(after.x - before.x, after.y - before.y);
			const r = clamp(0, 1, 1 - this.distTillEnd / totalDist);
			return this.ty(
				mix(before.x, after.x, r) + this.deltaX,
				mix(before.y, after.y, r) + this.deltaY,
				this.raf,
			);
		}
	}

	function AlsoThruSeries(ty) {
		const fn = (rx, ry, raf) => new SimpleMixInterpolator(ty, rx, ry, 0, 0, raf);
		fn.withOffset = (rx, ry, deltaX, deltaY, raf) =>
			new SimpleMixInterpolator(ty, rx, ry, deltaX, deltaY, raf);
		fn.distFromStart = (dist, raf) => new DistanceFromStartInterpolator(ty, dist, 0, 0, raf);
		fn.distTillEnd = (dist, raf) => new DistTillEndInterpolator(ty, dist, 0, 0, raf);
		fn.distFromStart.withOffset = (dist, deltaX, deltaY, raf) =>
			new DistanceFromStartInterpolator(ty, dist, deltaX, deltaY, raf);
		fn.distTillEnd.withOffset = (dist, deltaX, deltaY, raf) =>
			new DistTillEndInterpolator(ty, dist, deltaX, deltaY, raf);
		return fn;
	}

	const alsoThru = AlsoThruSeries(g4);
	alsoThru.g2 = AlsoThruSeries(g2);
	alsoThru.g4 = AlsoThruSeries(g2);
	alsoThru.flat = AlsoThruSeries(flat);
	alsoThru.curl = AlsoThruSeries(curl);

	/// Multi-mix interpolator
	class MultiMixInterpolator extends InterpolatorBase {
		constructor(rs, raf, ty) {
			super();
			this.rs = rs;
			this.raf = raf;
			this.ty = fallback(ty, g2);
		}
		resolveInterpolation(before, after) {
			const innerKnots = [];
			for (const [rx, ry, rt] of this.rs) {
				const x = mix(before.x, after.x, rx);
				const y = mix(before.y, after.y, ry);
				const af =
					this.raf?.blend && rt !== void 0
						? this.raf.blend(rt)
						: this.raf
							? this.raf
							: unimportant;
				innerKnots.push(this.ty(x, y, af));
			}
			return innerKnots;
		}
	}
	function alsoThruThem(rs, raf, ty) {
		return new MultiMixInterpolator(rs, raf, ty);
	}

	/// Multi-mix interpolator that use function set to compute proportion/deltas
	class MultiMixComputeInterpolator extends InterpolatorBase {
		constructor(rs, raf, ty) {
			super();
			this.rs = rs;
			this.raf = raf;
			this.ty = fallback(ty, g2);
		}
		resolveInterpolation(before, after) {
			const innerKnots = [];
			for (const rt of this.rs) {
				innerKnots.push(
					this.ty(
						mix(before.x, after.x, this.raf.rx(rt)) + this.raf.deltaX(rt),
						mix(before.y, after.y, this.raf.ry(rt)) + this.raf.deltaY(rt),
						this.raf.modifier(rt),
					),
				);
			}
			return innerKnots;
		}
	}
	alsoThruThem.computed = (rs, raf, ty) => new MultiMixComputeInterpolator(rs, raf, ty);

	// Bezier control interpolator

	function bezControlsImpl(x1, y1, x2, y2, samples, raf, _ty) {
		const rs = [];
		for (let j = 1; j < samples; j = j + 1)
			rs.push([
				bez3(0, x1, x2, 1, j / samples),
				bez3(0, y1, y2, 1, j / samples),
				j / samples,
			]);
		return alsoThruThem(rs, raf);
	}
	function bezControls(x1, y1, x2, y2, _samples, raf) {
		return bezControlsImpl(x1, y1, x2, y2, fallback(_samples, 3), raf);
	}
	function quadControls(x1, y1, _samples, raf) {
		return bezControlsImpl(
			(x1 * 2) / 3,
			(y1 * 2) / 3,
			mix(1, x1, 2 / 3),
			mix(1, y1, 2 / 3),
			fallback(_samples, 3),
			raf,
		);
	}

	// ArcHV and ArcVH interpolators
	class ArcHvInterpolator extends InterpolatorBase {
		constructor(steps, superness) {
			super();
			this.steps = steps;
			this.superness = superness;
		}
		resolveInterpolation(before, after) {
			const innerKnots = [];
			for (let j = 1; j < this.steps; j++) {
				const theta = (((j + 1) / (this.steps + 2)) * Math.PI) / 2;
				const c = Math.cos(theta) ** (2 / this.superness);
				const s = Math.sin(theta) ** (2 / this.superness);
				const x = mix(before.x, after.x, s);
				const y = mix(before.y, after.y, 1 - c);
				innerKnots.push(g2(x, y, unimportant));
			}
			return innerKnots;
		}
	}
	class ArcVhInterpolator extends InterpolatorBase {
		constructor(steps, superness) {
			super();
			this.steps = steps;
			this.superness = superness;
		}
		resolveInterpolation(before, after) {
			const innerKnots = [];
			for (let j = 1; j < this.steps; j++) {
				const theta = (((j + 1) / (this.steps + 2)) * Math.PI) / 2;
				const c = Math.cos(theta) ** (2 / this.superness);
				const s = Math.sin(theta) ** (2 / this.superness);
				const x = mix(before.x, after.x, 1 - c);
				const y = mix(before.y, after.y, s);
				innerKnots.push(g2(x, y, unimportant));
			}
			return innerKnots;
		}
	}

	const DEFAULT_STEPS = 6;
	function archv(samples, superness) {
		return new ArcHvInterpolator(
			fallback(samples, DEFAULT_STEPS),
			fallback(superness, Superness),
		);
	}
	archv.superness = s => new ArcHvInterpolator(DEFAULT_STEPS, s);
	function arcvh(samples, superness) {
		return new ArcVhInterpolator(
			fallback(samples, DEFAULT_STEPS),
			fallback(superness, Superness),
		);
	}
	arcvh.superness = s => new ArcVhInterpolator(DEFAULT_STEPS, s);
	archv.yFromX = (px, _s) => {
		const s = fallback(_s, Superness);
		return 1 - (1 - px ** s) ** (1 / s);
	};
	archv.sCos = (angle, _s) => {
		const c = Math.cos((angle / 180) * Math.PI);
		if (c < 0) return -((-c) ** (2 / fallback(_s, Superness)));
		else return c ** (2 / fallback(_s, Superness));
	};
	archv.sSin = (angle, _s) => {
		const s = Math.sin((angle / 180) * Math.PI);
		if (s < 0) return -((-s) ** (2 / fallback(_s, Superness)));
		else return s ** (2 / fallback(_s, Superness));
	};

	function dispiro(...controls) {
		return new DispiroImpl(bindings, controls);
	}
	function spiroOutline(...controls) {
		return new SpiroOutlineImpl(bindings, controls);
	}
	function spiroCollect(collector, ...controls) {
		const spb = new SpiroImplBase(bindings, controls);
		return spb.collectTo(collector);
	}

	return {
		g4,
		g2,
		corner,
		flat,
		curl,
		virt,
		close,
		end,
		straight,
		g2c,
		cg2,
		flatc,
		ccurl,
		widths,
		heading,
		"disable-contrast": disableContrast,
		unimportant,
		important,
		alsoThru,
		alsoThruThem,
		bezControls,
		quadControls,
		archv,
		arcvh,
		dispiro,
		"spiro-outline": spiroOutline,
		"spiro-collect": spiroCollect,
		"decor@": x => new DecorInterpolator(x),
		"decor@@": x => new DecorInterpolator(new DecorInterpolator(x)),
		"decor@@@": x => new DecorInterpolator(new DecorInterpolator(new DecorInterpolator(x))),
	};
}
