import { lineString, pointToLineDistance } from "@turf/turf";
import { FSSource, hasPMTiles } from "./OfflineTiles";
import { PMTiles } from "pmtiles";
import { VectorTile } from "@mapbox/vector-tile";
import Protobuf from "pbf";

function getFeatureDistance(f: GeoJSON.Feature, point: [number, number]) {
	if (f.geometry.type === "LineString") {
		return pointToLineDistance(point, lineString(f.geometry.coordinates));
	} else if (f.geometry.type === "MultiLineString") {
		// Compute the min distance across all parts
		return Math.min(
			...f.geometry.coordinates.map((coords) =>
				pointToLineDistance(point, lineString(coords)),
			),
		);
	} else {
		return Infinity;
	}
}

export async function getFeature(coord: WorldLocation, layer: string, filter?: (feature: GeoJSON.Feature) => boolean) {
	const zxy = coordToTile(coord, 14);
	const tile = await fetchTile(zxy.z, zxy.x, zxy.y);
	const layerData = tile.layers[layer];
	if (!layerData) return null;

	const features: GeoJSON.Feature[] = [];
	for (let i = 0; i < layerData.length; i++) {
		const feature = layerData.feature(i);
		features.push(feature.toGeoJSON(zxy.x, zxy.y, zxy.z));
	}
	const filtered = features.filter(
		(f) =>
			f.geometry.type === "LineString" || f.geometry.type == "MultiLineString",
	).filter((f) => (filter ? filter(f) : true));
	if (filtered.length === 0) return null;
	const nearest = filtered.reduce((a, b) => {
		const distA = getFeatureDistance(a, [coord.lon, coord.lat]);
		const distB = getFeatureDistance(b, [coord.lon, coord.lat]);
		return distA < distB ? a : b;
	});
	return nearest;
}

export async function getMeta(coord: WorldLocation, layer: string, filter?: (feature: GeoJSON.Feature) => boolean) {
	const nearest = await getFeature(coord, layer, filter);
	return nearest ? nearest.properties : null;
}

const IMPLICIT_SPEEDS: Record<string, number> = {
	"DE:urban": 50,
	"DE:rural": 100, // TODO: 80 (hgv weight > 3.5t, or trailer), 60 (weight > 7.5t)
	"DE:living_street": 7,
	"DE:bicycle_road": 30,
};

export function getSpeed(maxspeed: string): number | null {
	if (!isNaN(parseInt(maxspeed))) return parseInt(maxspeed);
	if (maxspeed.endsWith(" mph")) {
		const val = parseInt(maxspeed.replace(" mph", ""));
		if (!isNaN(val)) return Math.round(val * 1.60934); // Convert to km/h
	}
	if (maxspeed === "walk") return 7; // https://wiki.openstreetmap.org/wiki/Proposed_features/maxspeed_walk
	return IMPLICIT_SPEEDS[maxspeed as keyof typeof IMPLICIT_SPEEDS] || null;
}

function coordToTile(coord: WorldLocation, zoom: number) {
	const z = zoom;
	const x = Math.floor(((coord.lon + 180) / 360) * Math.pow(2, z));
	const y = Math.floor(
		((1 -
			Math.log(
				Math.tan((coord.lat * Math.PI) / 180) +
					1 / Math.cos((coord.lat * Math.PI) / 180),
			) /
				Math.PI) /
			2) *
			Math.pow(2, z),
	);
	return { z, x, y };
}

class Cache<T> {
	data: Map<string, T>;
	size: number;

	constructor(size: number) {
		this.data = new Map<string, T>();
		this.size = size;
	}

	get(key: string): T | undefined {
		if (this.data.has(key)) {
			const value = this.data.get(key)!;
			this.data.delete(key);
			this.data.set(key, value);
			return value;
		}
		return this.data.get(key);
	}

	has(key: string): boolean {
		return this.data.has(key);
	}

	set(key: string, value: T): void {
		this.data.set(key, value);
		if (this.data.size > this.size) {
			const firstKey = this.data.keys().next().value;
			if (firstKey) {
				this.data.delete(firstKey);
			}
		}
	}
}

const tileCache = new Cache<VectorTile>(5);

export async function fetchTile(z: number, x: number, y: number) {
	const cacheKey = `${z}/${x}/${y}`;
	if (tileCache.has(cacheKey)) {
		return tileCache.get(cacheKey)!;
	}
	const pmtiles = (await hasPMTiles("tiles"))
		? new PMTiles(new FSSource("tiles"))
		: new PMTiles("https://trafficcue-tiles.picoscratch.de/germany.pmtiles");
	const tile = await pmtiles.getZxy(z, x, y);
	if (!tile) {
		console.log(tile);
		throw new Error(`Tile not found: z${z} x${x} y${y}`);
	}
	const data = tile.data;
	const vectorTile = new VectorTile(new Protobuf(data));
	tileCache.set(cacheKey, vectorTile);
	return vectorTile;
}
