import { assertExists } from "https://deno.land/std@0.224.0/assert/assert_exists.ts";
import { dirname } from "https://deno.land/std@0.224.0/path/dirname.ts";
import { TextLineStream } from "https://deno.land/std@0.224.0/streams/text_line_stream.ts";

export function* enumerate<T>(iterator: Iterable<T>) {
	let index = 0;
	for (const item of iterator) yield [index++, item] as const;
}

export function* zip<A, B>(iterable_a: Iterable<A>, iterable_b: Iterable<B>) {
	const iter_a = iterable_a[Symbol.iterator]();
	const iter_b = iterable_b[Symbol.iterator]();
	while (true) {
		const next_a = iter_a.next().value;
		const next_b = iter_b.next().value;
		if (next_a === undefined) return;
		if (next_b === undefined) return;
		yield [next_a as A, next_b as B] as const;
	}
}

export function* range(from: number, to: number) {
	while (from < to) yield from++;
}

export async function wait(ms: number) {
	await new Promise((resolver) => setTimeout(resolver, ms));
}

export type Awaitable<T> = T | Promise<T>;

export function last<T>(iterable: Iterable<T>) {
	let last = undefined as T | undefined;
	for (const item of iterable) last = item;
	return last;
}

export function first<T>(iterable: Iterable<T>) {
	for (const item of iterable) return item;
}

export function dbg<T>(value: T, ...rest: unknown[]) {
	console.log("[DBG]", value, ...rest);
	return value;
}

export type Value<T> = { value: T };

export function next<T>(iterator: Iterator<T>) {
	const result = iterator.next().value;
	if (result === undefined) return null;
	else return result as T;
}

export async function async_next<T>(iterator: AsyncIterator<T>) {
	const result = await iterator.next();
	const value = result.value;
	if (value === undefined) return null;
	else return value as T;
}

export class LineReader {
	iterator;

	constructor(readable: ReadableStream<Uint8Array>) {
		this.iterator = readable
			.pipeThrough(new TextDecoderStream())
			.pipeThrough(new TextLineStream())
			[Symbol.asyncIterator]();
	}

	async read() {
		return await async_next(this.iterator);
	}
}

export class LineWriter {
	writable;

	constructor(writable: WritableStream<Uint8Array>) {
		const encoder = new TextEncoderStream();
		encoder.readable.pipeTo(writable);
		this.writable = encoder.writable.getWriter();
	}

	async write(line: string) {
		await this.writable.write(line);
	}
}

export function write_lines(writable: WritableStream<Uint8Array>) {
	const queue = new AsyncQueue<string>();
	ReadableStream.from(queue.iter()).pipeThrough(new TextEncoderStream()).pipeTo(writable);
	return { queue, write: (line: string) => queue.push(line) };
}

export type Consumer<T> = (value: T) => void;

export function split_promise<T = void>() {
	let resolver = null as null | Consumer<T>;
	const promise = new Promise<T>((res) => resolver = res);
	assertExists(resolver);
	return [promise, resolver] as const;
}

class AsyncQueue<T> {
	items;
	item_resolver;

	constructor() {
		this.items = [] as T[];
		this.item_resolver = null as null | Consumer<T>;
	}

	push(item: T) {
		if (this.item_resolver !== null) this.item_resolver(item);
		else this.items.push(item);
	}

	async pull() {
		const [first] = this.items.splice(0, 1);
		if (first !== undefined) return first;
		const [promise, resolver] = split_promise<T>();
		this.item_resolver = resolver;
		return await promise;
	}

	async *iter() {
		while (true) yield await this.pull();
	}
}

export async function project_root() {
	const this_url = new URL(import.meta.url);
	const this_path = await Deno.realPath(this_url.pathname);
	return dirname(dirname(dirname(this_path)));
}
