// NIRAX --- A lightweight router

import { EventEmitter } from "eventemitter3";
import { Ref, Component, ref, shallowRef, ShallowRef } from "vue";
import { pleaseLogin } from "@/scripts/please-login";
import { safeURIDecode } from "@/scripts/safe-uri-decode";

type RouteDef = {
	path: string;
	component: Component;
	query?: Record<string, string>;
	loginRequired?: boolean;
	name?: string;
	hash?: string;
	globalCacheKey?: string;
	children?: RouteDef[];
};

type ParsedPath = (
	| string
	| {
			name: string;
			startsWith?: string;
			wildcard?: boolean;
			optional?: boolean;
	  }
)[];

export type Resolved = {
	route: RouteDef;
	props: Map<string, string>;
	child?: Resolved;
};

function parsePath(path: string): ParsedPath {
	const res = [] as ParsedPath;

	path = path.substring(1);

	for (const part of path.split("/")) {
		if (part.includes(":")) {
			const prefix = part.substring(0, part.indexOf(":"));
			const placeholder = part.substring(part.indexOf(":") + 1);
			const wildcard = placeholder.includes("(*)");
			const optional = placeholder.endsWith("?");
			res.push({
				name: placeholder.replace("(*)", "").replace("?", ""),
				startsWith: prefix !== "" ? prefix : undefined,
				wildcard,
				optional,
			});
		} else if (part.length !== 0) {
			res.push(part);
		}
	}

	return res;
}

export class Router extends EventEmitter<{
	change: (ctx: {
		beforePath: string;
		path: string;
		resolved: Resolved;
		key: string;
	}) => void;
	replace: (ctx: {
		path: string;
		key: string;
	}) => void;
	push: (ctx: {
		beforePath: string;
		path: string;
		route: RouteDef | null;
		props: Map<string, string> | null;
		key: string;
	}) => void;
	same: () => void;
}> {
	private routes: RouteDef[];
	public current: Resolved;
	public currentRef: ShallowRef<Resolved> = shallowRef();
	public currentRoute: ShallowRef<RouteDef> = shallowRef();
	private currentPath: string;
	private currentKey = Date.now().toString();

	public navHook: ((path: string, flag?: any) => boolean) | null = null;

	constructor(routes: Router["routes"], currentPath: Router["currentPath"]) {
		super();

		this.routes = routes;
		this.currentPath = currentPath;
		this.navigate(currentPath, null, false);
	}

	public resolve(path: string): Resolved | null {
		let queryString: string | null = null;
		let hash: string | null = null;
		if (path[0] === "/") path = path.substring(1);
		if (path.includes("#")) {
			hash = path.substring(path.indexOf("#") + 1);
			path = path.substring(0, path.indexOf("#"));
		}
		if (path.includes("?")) {
			queryString = path.substring(path.indexOf("?") + 1);
			path = path.substring(0, path.indexOf("?"));
		}

		if (_DEV_) console.log("Routing: ", path, queryString);

		function check(routes: RouteDef[], _parts: string[]): Resolved | null {
			forEachRouteLoop: for (const route of routes) {
				let parts = [..._parts];
				const props = new Map<string, string>();

				pathMatchLoop: for (const p of parsePath(route.path)) {
					if (typeof p === "string") {
						if (p === parts[0]) {
							parts.shift();
						} else {
							continue forEachRouteLoop;
						}
					} else {
						if (parts[0] == null && !p.optional) {
							continue forEachRouteLoop;
						}
						if (p.wildcard) {
							if (parts.length !== 0) {
								props.set(p.name, safeURIDecode(parts.join("/")));
								parts = [];
							}
							break pathMatchLoop;
						} else {
							if (p.startsWith) {
								if (parts[0] == null || !parts[0].startsWith(p.startsWith))
									continue forEachRouteLoop;

								props.set(
									p.name,
									safeURIDecode(parts[0].substring(p.startsWith.length)),
								);
								parts.shift();
							} else {
								if (parts[0]) {
									props.set(p.name, safeURIDecode(parts[0]));
								}
								parts.shift();
							}
						}
					}
				}

				if (parts.length === 0) {
					if (route.children) {
						const child = check(route.children, []);
						if (child) {
							return {
								route,
								props,
								child,
							};
						} else {
							continue forEachRouteLoop;
						}
					}

					if (route.hash != null && hash != null) {
						props.set(route.hash, safeURIDecode(hash));
					}

					if (route.query != null && queryString != null) {
						const queryObject = [
							...new URLSearchParams(queryString).entries(),
						].reduce((obj, entry) => ({ ...obj, [entry[0]]: entry[1] }), {});

						for (const q in route.query) {
							const as = route.query[q];
							if (queryObject[q]) {
								props.set(as, safeURIDecode(queryObject[q]));
							}
						}
					}

					return {
						route,
						props,
					};
				} else {
					if (route.children) {
						const child = check(route.children, parts);
						if (child) {
							return {
								route,
								props,
								child,
							};
						} else {
						}
					} else {
					}
				}
			}

			return null;
		}

		const _parts = path.split("/").filter((part) => part.length !== 0);

		return check(this.routes, _parts);
	}

	private navigate(
		path: string,
		key: string | null | undefined,
		emitChange = true,
	) {
		const beforePath = this.currentPath;
		this.currentPath = path;

		const res = this.resolve(this.currentPath);

		if (res == null) {
			throw new Error(`no route found for: ${path}`);
		}

		if (res.route.loginRequired) {
			pleaseLogin("/");
		}

		const isSamePath = beforePath === path;
		if (isSamePath && key == null) key = this.currentKey;
		this.current = res;
		this.currentRef.value = res;
		this.currentRoute.value = res.route;
		this.currentKey = res.route.globalCacheKey ?? key ?? path;

		if (emitChange) {
			this.emit("change", {
				beforePath,
				path,
				resolved: res,
				key: this.currentKey,
			});
		}

		return res;
	}

	public getCurrentPath() {
		return this.currentPath;
	}

	public getCurrentKey() {
		return this.currentKey;
	}

	public push(path: string, flag?: any) {
		const beforePath = this.currentPath;
		if (path === beforePath) {
			this.emit("same");
			return;
		}
		if (this.navHook) {
			const cancel = this.navHook(path, flag);
			if (cancel) return;
		}
		const res = this.navigate(path, null);
		this.emit("push", {
			beforePath,
			path,
			route: res.route,
			props: res.props,
			key: this.currentKey,
		});
	}

	public replace(path: string, key?: string | null, emitEvent = true) {
		this.navigate(path, key);
		if (emitEvent) {
			this.emit("replace", {
				path,
				key: this.currentKey,
			});
		}
	}
}