/**
 * spider-3d.ts — Pure computation for 3D stacked spider/radar plots.
 *
 * No DOM dependencies. Generates polar mesh data, z-aggregation layers,
 * overlap detection, and color mapping for any renderer.
 */

import type { FlowKind } from "./layer-types";
import { FLOW_COLORS } from "./layer-types";
import type { SpaceConnection } from "./connection-types";

// ── Types ──

export interface Spider3DAxis {
	id: string;
	label: string;
	max?: number; // axis maximum (default: 1)
}

export interface Spider3DDataset {
	id: string;
	label: string;
	color: string;
	values: Record<string, number>; // axisId → value (0 to axis.max)
}

export interface Spider3DConfig {
	axes: Spider3DAxis[];
	datasets: Spider3DDataset[];
	resolution?: number; // radial subdivisions for overlap mesh (default: 36)
}

export interface Spider3DSample {
	angle: number;        // radians
	radius: number;       // 0-1 normalized
	x: number;
	y: number;            // cartesian (for SVG)
	height: number;       // aggregate z value (sum of datasets covering this point)
	contributors: string[]; // dataset IDs that reach this point
}

export interface Spider3DLayer {
	datasetId: string;
	color: string;
	label: string;
	zIndex: number;       // stack order (0 = bottom)
	vertices: { x: number; y: number }[];
}

export interface Spider3DOverlapRegion {
	contributorIds: string[];
	vertices: { x: number; y: number }[];
	height: number;
}

export interface Spider3DResult {
	layers: Spider3DLayer[];
	samples: Spider3DSample[];
	maxHeight: number;
	overlapRegions: Spider3DOverlapRegion[];
}

// ── Helpers ──

/** Interpolate the radar radius at a given angle for a dataset */
function datasetRadiusAtAngle(
	dataset: Spider3DDataset,
	axes: Spider3DAxis[],
	angle: number,
): number {
	const n = axes.length;
	if (n === 0) return 0;
	const step = (2 * Math.PI) / n;

	// Normalize angle to [0, 2π)
	let a = ((angle % (2 * Math.PI)) + 2 * Math.PI) % (2 * Math.PI);

	// Find which two axes this angle falls between
	const idx = a / step;
	const i0 = Math.floor(idx) % n;
	const i1 = (i0 + 1) % n;
	const t = idx - Math.floor(idx);

	const max0 = axes[i0].max ?? 1;
	const max1 = axes[i1].max ?? 1;
	const v0 = (dataset.values[axes[i0].id] ?? 0) / max0;
	const v1 = (dataset.values[axes[i1].id] ?? 0) / max1;

	return v0 * (1 - t) + v1 * t; // linear interpolation, 0-1 normalized
}

/** Point-in-polygon test (ray casting) */
function pointInPolygon(
	px: number,
	py: number,
	polygon: { x: number; y: number }[],
): boolean {
	let inside = false;
	const n = polygon.length;
	for (let i = 0, j = n - 1; i < n; j = i++) {
		const xi = polygon[i].x, yi = polygon[i].y;
		const xj = polygon[j].x, yj = polygon[j].y;
		if (
			yi > py !== yj > py &&
			px < ((xj - xi) * (py - yi)) / (yj - yi) + xi
		) {
			inside = !inside;
		}
	}
	return inside;
}

// ── Core functions ──

/**
 * Compute the radar polygon vertices for a single dataset.
 */
export function computeRadarPolygon(
	dataset: Spider3DDataset,
	axes: Spider3DAxis[],
	cx: number,
	cy: number,
	radius: number,
): { x: number; y: number }[] {
	const n = axes.length;
	if (n === 0) return [];
	const angleStep = (2 * Math.PI) / n;

	return axes.map((axis, i) => {
		const max = axis.max ?? 1;
		const val = Math.min((dataset.values[axis.id] ?? 0) / max, 1);
		const angle = i * angleStep - Math.PI / 2;
		return {
			x: cx + radius * val * Math.cos(angle),
			y: cy + radius * val * Math.sin(angle),
		};
	});
}

/**
 * Fine-grained sampling for height computation.
 * At each sample point: count how many datasets' polygons contain it.
 */
export function computeOverlapMesh(
	datasets: Spider3DDataset[],
	axes: Spider3DAxis[],
	cx: number,
	cy: number,
	radius: number,
	resolution: number = 36,
): Spider3DSample[] {
	const samples: Spider3DSample[] = [];
	const radialSteps = Math.max(4, Math.floor(resolution / 3));

	// Pre-compute all dataset polygons
	const polygons = datasets.map((ds) =>
		computeRadarPolygon(ds, axes, cx, cy, radius),
	);

	for (let ai = 0; ai < resolution; ai++) {
		const angle = (ai / resolution) * 2 * Math.PI;
		for (let ri = 1; ri <= radialSteps; ri++) {
			const r = ri / radialSteps;
			const x = cx + radius * r * Math.cos(angle - Math.PI / 2);
			const y = cy + radius * r * Math.sin(angle - Math.PI / 2);

			const contributors: string[] = [];
			for (let di = 0; di < datasets.length; di++) {
				if (polygons[di].length >= 3 && pointInPolygon(x, y, polygons[di])) {
					contributors.push(datasets[di].id);
				}
			}

			samples.push({
				angle,
				radius: r,
				x,
				y,
				height: contributors.length,
				contributors,
			});
		}
	}

	return samples;
}

/**
 * Main computation: produces layers, samples, and overlap regions.
 */
export function computeSpider3D(
	config: Spider3DConfig,
	cx: number,
	cy: number,
	radius: number,
): Spider3DResult {
	const { axes, datasets, resolution = 36 } = config;

	// Build one layer per dataset
	const layers: Spider3DLayer[] = datasets.map((ds, i) => ({
		datasetId: ds.id,
		color: ds.color,
		label: ds.label,
		zIndex: i,
		vertices: computeRadarPolygon(ds, axes, cx, cy, radius),
	}));

	// Compute overlap mesh
	const samples = computeOverlapMesh(
		datasets,
		axes,
		cx,
		cy,
		radius,
		resolution,
	);

	const maxHeight = samples.reduce((m, s) => Math.max(m, s.height), 0);

	// Build overlap regions by grouping contiguous samples with height >= 2
	const overlapRegions = buildOverlapRegions(samples, datasets.length);

	return { layers, samples, maxHeight, overlapRegions };
}

/**
 * Build overlap region summaries from sample data.
 * Groups samples by their exact contributor set, then creates a convex hull
 * of the sample points for each group.
 */
function buildOverlapRegions(
	samples: Spider3DSample[],
	_datasetCount: number,
): Spider3DOverlapRegion[] {
	// Group by contributor set (sorted key)
	const groups = new Map<string, Spider3DSample[]>();

	for (const s of samples) {
		if (s.contributors.length < 2) continue;
		const key = [...s.contributors].sort().join("|");
		if (!groups.has(key)) groups.set(key, []);
		groups.get(key)!.push(s);
	}

	const regions: Spider3DOverlapRegion[] = [];
	for (const [key, points] of groups) {
		if (points.length < 3) continue;
		const contributorIds = key.split("|");
		const vertices = convexHull(points.map((p) => ({ x: p.x, y: p.y })));
		const height = Math.max(...points.map((p) => p.height));
		regions.push({ contributorIds, vertices, height });
	}

	return regions;
}

/** Simple convex hull (Graham scan) for overlap region outlines */
function convexHull(points: { x: number; y: number }[]): { x: number; y: number }[] {
	if (points.length < 3) return points;

	// Find lowest-y (then leftmost) point
	let pivot = points[0];
	for (const p of points) {
		if (p.y < pivot.y || (p.y === pivot.y && p.x < pivot.x)) pivot = p;
	}

	const sorted = points
		.filter((p) => p !== pivot)
		.sort((a, b) => {
			const angleA = Math.atan2(a.y - pivot.y, a.x - pivot.x);
			const angleB = Math.atan2(b.y - pivot.y, b.x - pivot.x);
			if (angleA !== angleB) return angleA - angleB;
			const distA = (a.x - pivot.x) ** 2 + (a.y - pivot.y) ** 2;
			const distB = (b.x - pivot.x) ** 2 + (b.y - pivot.y) ** 2;
			return distA - distB;
		});

	const stack: { x: number; y: number }[] = [pivot];
	for (const p of sorted) {
		while (stack.length >= 2) {
			const a = stack[stack.length - 2];
			const b = stack[stack.length - 1];
			const cross = (b.x - a.x) * (p.y - a.y) - (b.y - a.y) * (p.x - a.x);
			if (cross <= 0) stack.pop();
			else break;
		}
		stack.push(p);
	}

	return stack;
}

// ── Membrane preset ──

/** All standard FlowKinds (excluding "custom") for axis defaults */
const ALL_FLOW_KINDS: FlowKind[] = [
	"economic",
	"trust",
	"data",
	"governance",
	"resource",
	"attention",
];

/** Per-space color tinting: shift the base FlowKind color per space index */
const SPACE_TINTS = [
	"#4ade80", // green
	"#c084fc", // violet
	"#60a5fa", // blue
	"#f59e0b", // amber
	"#ec4899", // pink
	"#14b8a6", // teal
	"#f97316", // orange
	"#8b5cf6", // purple
];

/**
 * Map SpaceConnection[] + FlowKind[] into a Spider3DConfig.
 *
 * - Axes = one per FlowKind
 * - Datasets = one per connected remote space
 * - Values = aggregate connection strength per FlowKind for that space
 */
export function membranePreset(
	connections: SpaceConnection[],
	flowKinds: FlowKind[] = ALL_FLOW_KINDS,
): Spider3DConfig {
	const axes: Spider3DAxis[] = flowKinds.map((kind) => ({
		id: kind,
		label: kind.charAt(0).toUpperCase() + kind.slice(1),
		max: 1,
	}));

	// Group connections by remote space
	const byRemote = new Map<string, SpaceConnection[]>();
	for (const conn of connections) {
		if (conn.state !== "active") continue;
		if (!byRemote.has(conn.remoteSlug)) byRemote.set(conn.remoteSlug, []);
		byRemote.get(conn.remoteSlug)!.push(conn);
	}

	const datasets: Spider3DDataset[] = [];
	let spaceIdx = 0;
	for (const [remoteSlug, conns] of byRemote) {
		const values: Record<string, number> = {};

		for (const kind of flowKinds) {
			// Average strength across all connections of this kind to this space
			const matching = conns.filter((c) => c.flowKinds.includes(kind));
			if (matching.length > 0) {
				values[kind] =
					matching.reduce((sum, c) => sum + c.strength, 0) / matching.length;
			} else {
				values[kind] = 0;
			}
		}

		datasets.push({
			id: remoteSlug,
			label: remoteSlug,
			color: SPACE_TINTS[spaceIdx % SPACE_TINTS.length],
			values,
		});
		spaceIdx++;
	}

	return { axes, datasets };
}

// ── Demo data ──

export const DEMO_CONFIG: Spider3DConfig = {
	axes: [
		{ id: "economic", label: "Economic", max: 1 },
		{ id: "trust", label: "Trust", max: 1 },
		{ id: "data", label: "Data", max: 1 },
		{ id: "governance", label: "Governance", max: 1 },
		{ id: "resource", label: "Resource", max: 1 },
		{ id: "attention", label: "Attention", max: 1 },
	],
	datasets: [
		{
			id: "commons-dao",
			label: "Commons DAO",
			color: "#4ade80",
			values: {
				economic: 0.8,
				trust: 0.6,
				data: 0.5,
				governance: 0.9,
				resource: 0.3,
				attention: 0.4,
			},
		},
		{
			id: "mycelial-lab",
			label: "Mycelial Lab",
			color: "#c084fc",
			values: {
				economic: 0.3,
				trust: 0.9,
				data: 0.7,
				governance: 0.4,
				resource: 0.8,
				attention: 0.6,
			},
		},
		{
			id: "regen-fund",
			label: "Regenerative Fund",
			color: "#60a5fa",
			values: {
				economic: 0.7,
				trust: 0.5,
				data: 0.4,
				governance: 0.6,
				resource: 0.5,
				attention: 0.9,
			},
		},
	],
};
