import { linreg } from "@iosevka/util";
import * as SpiroJs from "spiro";

import * as CurveUtil from "./curve-util.mjs";
import { Point } from "./point.mjs";
import { MonoKnot } from "./spiro-to-outline.mjs";

export class PenKnotCollector {
	constructor(gizmo, defaultProfile, fProfileFixed) {
		this.gizmo = gizmo;
		this.m_profile = defaultProfile;
		this.m_lastKnot = null;
		this.m_finished = false;
		this.m_profileFixed = fProfileFixed;

		this.knots = [];
		this.closed = false;
	}
	finish() {
		this.m_finished = true;
	}
	pushKnot(c) {
		if (this.m_finished) throw new Error("Cannot push knot after finish");
		const k = new PenKnot(c.type, c.x, c.y, this.m_profile);
		this.knots.push(k);
		this.m_lastKnot = k;

		c.applyTo(this);
	}

	setWidth() {}
	headsTo() {}
	setUnimportant() {
		if (this.m_lastKnot) this.m_lastKnot.profile = null;
	}
	setImportant() {} // Not used
	setContrast() {}

	setProfile(profile) {
		if (this.m_profileFixed) return;
		if (profile.length !== this.m_profile.length)
			throw new Error("Pen profile length mismatch");
		if (this.m_lastKnot) this.m_lastKnot.profile = profile;
		this.m_profile = profile;
	}
}

export class PenKnot {
	constructor(type, x, y, profile) {
		this.type = type;
		this.x = x;
		this.y = y;
		this.profile = profile;
	}
	clone() {
		const k1 = new PenKnot(this.type, this.x, this.y, this.profile);
		return k1;
	}
	withGizmo(gizmo) {
		const tfZ = gizmo.applyXY(this.x, this.y);
		const k1 = new PenKnot(this.type, tfZ.x, tfZ.y, this.profile);
		return k1;
	}
	hash(h) {
		h.beginStruct("PenKnot");
		h.str(this.type);
		h.f64(this.x);
		h.f64(this.y);
		h.bool(this.profile != null);
		if (this.profile) {
			h.beginArray(this.profile.length);
			for (let i = 0; i < this.profile.length; i++) {
				h.f64(this.profile[i].x);
				h.f64(this.profile[i].y);
			}
			h.endArray();
		}
		h.endStruct();
	}
}

export class PenSpiroExpander {
	constructor(gizmo, profile, closed, knots) {
		this.m_gizmo = gizmo;
		this.m_closed = closed;
		this.m_knotsU = Array.from(knots);
		this.m_knotsT = knots.map(k => k.withGizmo(gizmo));
		this.m_profileEdges = profile.length;

		this.m_traces = [];
	}

	getGeometry() {
		this.traceAll();

		const contours = [];
		for (let i = 0; i < this.m_traces.length; i++) {
			const iNext = (i + 1) % this.m_traces.length;
			if (this.m_traces[i].length !== this.m_traces[iNext].length) {
				throw new Error("Different number of arcs in traces");
			}
			for (let j = 0; j < this.m_traces[i].length; j++) {
				const arcForward = this.m_traces[i][j];
				const arcBackward = this.m_traces[iNext][j];
				makeProfiledStroke(contours, arcForward, arcBackward);
			}
		}

		return contours;
	}

	traceAll() {
		for (let i = 0; i < this.m_profileEdges; i++) {
			this.calculateShiftedProfile(i);
		}
	}
	calculateShiftedProfile(iEdge) {
		const traceT = [],
			traceU = [];
		for (let i = 0; i < this.m_knotsT.length; i++) {
			const trT = this.getTrace(this.m_knotsT[i], iEdge);
			const trU = trT.clone();
			this.m_gizmo.unapplyToSink(trT, trU);
			traceT.push(trT);
			traceU.push(trU);
		}
		this.interpolateUnimportantTraceKnots(traceU, traceT);

		const arcc = new SimplyCollectArcs();
		SpiroJs.spiroToArcsOnContext(traceT, this.m_closed, arcc);
		this.m_traces.push(arcc.arcs);
	}
	getTrace(k, iEdge) {
		if (k.profile) {
			return new MonoKnot(k.type, false, k.x + k.profile[iEdge].x, k.y + k.profile[iEdge].y);
		} else {
			return new MonoKnot(k.type, true, k.x, k.y);
		}
	}
	interpolateUnimportantTraceKnots(traceU, traceT) {
		let firstImportantIdx = -1;
		let lastImportantIdx = -1;

		for (let i = 0; i < traceU.length; i++) {
			if (traceU[i].unimportant) continue;

			if (lastImportantIdx !== -1) {
				this.interpolateUnimportantTraceKnotsRg(traceU, traceT, lastImportantIdx, i);
			}

			if (firstImportantIdx === -1) firstImportantIdx = i;
			lastImportantIdx = i;
		}

		if (firstImportantIdx !== -1 && lastImportantIdx !== -1) {
			this.interpolateUnimportantTraceKnotsRg(
				traceU,
				traceT,
				lastImportantIdx,
				firstImportantIdx,
			);
		}
	}
	interpolateUnimportantTraceKnotsRg(traceU, traceT, last, next) {
		const count = next > last ? next - last : traceU.length - last + next;
		for (let offset = 1; offset < count; offset++) {
			const i = (last + offset) % traceU.length;
			this.interpolateKnot(
				this.m_knotsU[last],
				traceU[last],
				this.m_knotsU[i],
				traceU[i],
				traceT[i],
				this.m_knotsU[next],
				traceU[next],
			);
		}
	}
	interpolateKnot(rBefore, uBefore, rCurr, uCurr, tCurr, rAfter, uAfter) {
		uCurr.x = linreg(rBefore.x, uBefore.x, rAfter.x, uAfter.x, rCurr.x);
		uCurr.y = linreg(rBefore.y, uBefore.y, rAfter.y, uAfter.y, rCurr.y);
		this.m_gizmo.applyToSink(uCurr, tCurr);
	}
}

class SimplyCollectArcs {
	constructor() {
		this.arcs = [];
	}
	beginShape() {}
	endShape() {}
	moveTo() {}
	arcTo(arc) {
		this.arcs.push(arc);
	}
}

// arcForward and arcBackward must be spiro arcs.
function makeProfiledStroke(contours, arcForward, arcBackward) {
	const [subdividesForward, subdividesBackward] = subdivideKnotPair(
		arcForward,
		arcBackward,
		CurveUtil.GEOMETRY_PRECISION,
	);
	for (let i = 0; i < subdividesForward.length; i++) {
		const [a1, b1, c1, d1] = subdividesForward[i].toCubicBezier();
		const [a2, b2, c2, d2] = subdividesBackward[i].toCubicBezier();
		contours.push([
			Point.from(Point.Type.Corner, a1),
			Point.from(Point.Type.CubicStart, b1),
			Point.from(Point.Type.CubicEnd, c1),
			Point.from(Point.Type.Corner, d1),
			Point.from(Point.Type.Corner, d2),
			Point.from(Point.Type.CubicStart, c2),
			Point.from(Point.Type.CubicEnd, b2),
			Point.from(Point.Type.Corner, a2),
		]);
	}
}

function subdivideKnotPair(arcForward, arcBackward, delta) {
	const MAX_STOPS = 16;

	const sinkForward = [],
		sinkBackward = [];
	for (let stops = 1; stops < MAX_STOPS; stops++) {
		sinkForward.length = 0;
		sinkBackward.length = 0;

		uniformSubdivide(arcForward, stops, sinkForward);
		uniformSubdivide(arcBackward, stops, sinkBackward);

		let flatEnough = true;
		for (const fwd of sinkForward) if (fwd.bend > delta) flatEnough = false;
		for (const bwd of sinkBackward) if (bwd.bend > delta) flatEnough = false;

		if (flatEnough) break;
	}
	return [sinkForward, sinkBackward];
}
function uniformSubdivide(arc, stops, sink) {
	for (; stops > 1; stops--) {
		const f = arc.subdivide(1 / stops);
		sink.push(f[0]);
		arc = f[1];
	}
	sink.push(arc);
}
