import { getBoxToBoxArrow } from "perfect-arrows";
import { getStroke, type StrokeOptions } from "perfect-freehand";
import { FolkElement } from "./folk-element";
import { css } from "./tags";

// Point interface for bezier curves
interface Point {
	x: number;
	y: number;
}

// Utility functions for bezier curve rendering
function lerp(a: Point, b: Point, t: number): Point {
	return { x: a.x + (b.x - a.x) * t, y: a.y + (b.y - a.y) * t };
}

function distanceSquared(a: Point, b: Point): number {
	const dx = b.x - a.x;
	const dy = b.y - a.y;
	return dx * dx + dy * dy;
}

function distance(a: Point, b: Point): number {
	return Math.sqrt(distanceSquared(a, b));
}

function flatness(points: readonly Point[], offset: number): number {
	const p1 = points[offset + 0];
	const p2 = points[offset + 1];
	const p3 = points[offset + 2];
	const p4 = points[offset + 3];

	let ux = 3 * p2.x - 2 * p1.x - p4.x;
	ux *= ux;
	let uy = 3 * p2.y - 2 * p1.y - p4.y;
	uy *= uy;
	let vx = 3 * p3.x - 2 * p4.x - p1.x;
	vx *= vx;
	let vy = 3 * p3.y - 2 * p4.y - p1.y;
	vy *= vy;

	if (ux < vx) ux = vx;
	if (uy < vy) uy = vy;

	return ux + uy;
}

function getPointsOnBezierCurveWithSplitting(
	points: readonly Point[],
	offset: number,
	tolerance: number,
	outPoints: Point[] = [],
): Point[] {
	if (flatness(points, offset) < tolerance) {
		const p0 = points[offset + 0];
		if (outPoints.length) {
			const d = distance(outPoints[outPoints.length - 1], p0);
			if (d > 1) {
				outPoints.push(p0);
			}
		} else {
			outPoints.push(p0);
		}
		outPoints.push(points[offset + 3]);
	} else {
		const t = 0.5;
		const p1 = points[offset + 0];
		const p2 = points[offset + 1];
		const p3 = points[offset + 2];
		const p4 = points[offset + 3];

		const q1 = lerp(p1, p2, t);
		const q2 = lerp(p2, p3, t);
		const q3 = lerp(p3, p4, t);

		const r1 = lerp(q1, q2, t);
		const r2 = lerp(q2, q3, t);

		const red = lerp(r1, r2, t);

		getPointsOnBezierCurveWithSplitting([p1, q1, r1, red], 0, tolerance, outPoints);
		getPointsOnBezierCurveWithSplitting([red, r2, q3, p4], 0, tolerance, outPoints);
	}
	return outPoints;
}

function pointsOnBezierCurves(points: readonly Point[], tolerance: number = 0.15): Point[] {
	const newPoints: Point[] = [];
	const numSegments = (points.length - 1) / 3;
	for (let i = 0; i < numSegments; i++) {
		const offset = i * 3;
		getPointsOnBezierCurveWithSplitting(points, offset, tolerance, newPoints);
	}
	return newPoints;
}

function getSvgPathFromStroke(stroke: number[][]): string {
	if (stroke.length === 0) return "";

	for (const point of stroke) {
		point[0] = Math.round(point[0] * 100) / 100;
		point[1] = Math.round(point[1] * 100) / 100;
	}

	const d = stroke.reduce(
		(acc, [x0, y0], i, arr) => {
			const [x1, y1] = arr[(i + 1) % arr.length];
			acc.push(x0, y0, (x0 + x1) / 2, (y0 + y1) / 2);
			return acc;
		},
		["M", ...stroke[0], "Q"] as (string | number)[],
	);

	d.push("Z");
	return d.join(" ");
}

const styles = css`
	:host {
		display: block;
		position: absolute;
		inset: 0;
		pointer-events: none;
		z-index: -1;
	}
`;

declare global {
	interface HTMLElementTagNameMap {
		"folk-arrow": FolkArrow;
	}
}

export class FolkArrow extends FolkElement {
	static override tagName = "folk-arrow";
	static styles = styles;

	#sourceSelector: string = "";
	#targetSelector: string = "";
	#sourceElement: Element | null = null;
	#targetElement: Element | null = null;
	#sourceRect: DOMRect | null = null;
	#targetRect: DOMRect | null = null;
	#resizeObserver: ResizeObserver;
	#color: string = "#374151";
	#strokeWidth: number = 3;

	#options: StrokeOptions = {
		size: 7,
		thinning: 0.5,
		smoothing: 0.5,
		streamline: 0.5,
		simulatePressure: true,
		easing: (t) => t,
		start: {
			taper: 50,
			easing: (t) => t,
			cap: true,
		},
		end: {
			taper: 0,
			easing: (t) => t,
			cap: true,
		},
	};

	constructor() {
		super();
		this.#resizeObserver = new ResizeObserver(() => this.#updateArrow());
	}

	get source() {
		return this.#sourceSelector;
	}

	set source(value: string) {
		this.#sourceSelector = value;
		this.#observeSource();
		this.requestUpdate("source");
	}

	get target() {
		return this.#targetSelector;
	}

	set target(value: string) {
		this.#targetSelector = value;
		this.#observeTarget();
		this.requestUpdate("target");
	}

	get sourceId() {
		return this.#sourceSelector.replace("#", "");
	}

	set sourceId(value: string) {
		this.source = `#${value}`;
	}

	get targetId() {
		return this.#targetSelector.replace("#", "");
	}

	set targetId(value: string) {
		this.target = `#${value}`;
	}

	get color() {
		return this.#color;
	}

	set color(value: string) {
		this.#color = value;
		this.#updateArrow();
	}

	get strokeWidth() {
		return this.#strokeWidth;
	}

	set strokeWidth(value: number) {
		this.#strokeWidth = value;
		this.#options.size = value * 2 + 1;
		this.#updateArrow();
	}

	override connectedCallback() {
		super.connectedCallback();

		// Parse attributes
		const sourceAttr = this.getAttribute("source");
		const targetAttr = this.getAttribute("target");
		const colorAttr = this.getAttribute("color");
		const strokeAttr = this.getAttribute("stroke-width");

		if (sourceAttr) this.source = sourceAttr;
		if (targetAttr) this.target = targetAttr;
		if (colorAttr) this.#color = colorAttr;
		if (strokeAttr) this.strokeWidth = parseFloat(strokeAttr);

		// Start animation frame loop for position updates
		this.#startPositionTracking();
	}

	override disconnectedCallback() {
		super.disconnectedCallback();
		this.#resizeObserver.disconnect();
		this.#stopPositionTracking();
	}

	#animationFrameId: number | null = null;

	#startPositionTracking() {
		const track = () => {
			this.#updateRects();
			this.#updateArrow();
			this.#animationFrameId = requestAnimationFrame(track);
		};
		this.#animationFrameId = requestAnimationFrame(track);
	}

	#stopPositionTracking() {
		if (this.#animationFrameId !== null) {
			cancelAnimationFrame(this.#animationFrameId);
			this.#animationFrameId = null;
		}
	}

	#observeSource() {
		if (this.#sourceElement) {
			this.#resizeObserver.unobserve(this.#sourceElement);
		}

		if (this.#sourceSelector) {
			this.#sourceElement = document.querySelector(this.#sourceSelector);
			if (this.#sourceElement) {
				this.#resizeObserver.observe(this.#sourceElement);
			}
		}
	}

	#observeTarget() {
		if (this.#targetElement) {
			this.#resizeObserver.unobserve(this.#targetElement);
		}

		if (this.#targetSelector) {
			this.#targetElement = document.querySelector(this.#targetSelector);
			if (this.#targetElement) {
				this.#resizeObserver.observe(this.#targetElement);
			}
		}
	}

	#updateRects() {
		if (this.#sourceElement) {
			this.#sourceRect = this.#sourceElement.getBoundingClientRect();
		}
		if (this.#targetElement) {
			this.#targetRect = this.#targetElement.getBoundingClientRect();
		}
	}

	#updateArrow() {
		if (!this.#sourceRect || !this.#targetRect) {
			this.style.clipPath = "";
			this.style.display = "none";
			return;
		}

		this.style.display = "";

		const [sx, sy, cx, cy, ex, ey] = getBoxToBoxArrow(
			this.#sourceRect.x,
			this.#sourceRect.y,
			this.#sourceRect.width,
			this.#sourceRect.height,
			this.#targetRect.x,
			this.#targetRect.y,
			this.#targetRect.width,
			this.#targetRect.height,
		);

		const points = pointsOnBezierCurves([
			{ x: sx, y: sy },
			{ x: cx, y: cy },
			{ x: ex, y: ey },
			{ x: ex, y: ey },
		]);

		const stroke = getStroke(points, this.#options);
		const path = getSvgPathFromStroke(stroke);

		this.style.clipPath = `path('${path}')`;
		this.style.backgroundColor = this.#color;
	}

	override createRenderRoot() {
		const root = super.createRenderRoot();
		this.#updateArrow();
		return root;
	}

	toJSON() {
		return {
			type: "folk-arrow",
			id: this.id,
			sourceId: this.sourceId,
			targetId: this.targetId,
			color: this.#color,
			strokeWidth: this.#strokeWidth,
		};
	}
}
