import { assertNever } from "@firebox/tsutil";
import { pngToRom } from "./pngToRom";
import { RenderCart, renderCart as rawRenderCart } from "./rawRenderCart";
import { Watched, watch } from "../util/watch";

export type PicoCart = {
	name: string;
	src: string;
} | {
	name: string;
	rom: number[];
}

type PlayerButtons = {
	left: boolean;
	right: boolean;
	up: boolean;
	down: boolean;
	o: boolean;
	x: boolean;
	menu: boolean;
}

type RawHandle = ReturnType<RenderCart>;

export type PicoPlayerHandle = {
	raw: RawHandle;
	rawModule: unknown;
	// external things
	readonly canvas: HTMLCanvasElement;

	// i/o
	buttons: Watched<NonNullable<RawHandle["pico8_buttons"]>>;
	setButtons: (buttons: PlayerButtons[]) => void;
	setMouse: (mouse: {
		x: number;
		y: number;
		leftClick: boolean;
		rightClick: boolean;
	}) => void;
	setGamepadCount: (count: number) => void;
	gpio: (
		number[]
		// & {subscribe: (f: (gpio: number[]) => void) => void}
	); // read + write (should be 256-tuple)

	// state
	readonly state: {
		frameNumber: number;
		isPaused: boolean;
		hasFocus: boolean;
		requestPointerLock: boolean;
		requirePageNavigateConfirmation: boolean;
		showDpad: boolean;
		shutdownRequested: boolean;
		soundVolume: number;
	};

	// misc?
	setTouchDetected: (touchDetected: boolean) => void;
	dropCart: (cart: PicoCart) => void;

	// Module
	toggleSound: () => void;
	toggleControlMenu: () => void;
	togglePaused: () => void;
	// TODO: rename these two better (what do they do??)
	modDragOver: () => void;
	modDragStop: () => void;
}

const bitfield = (...args: boolean[]): number => {
	if (!args.length) {
		return 0;
	}
	return (args[0]?1:0)+2*bitfield(...args.slice(1));
}

const getRom = async (cart: PicoCart) => {
	if ("src" in cart) {
		return await pngToRom(cart.src);
	} else if ("rom" in cart) {
		return cart.rom;
	}
	assertNever(cart);
}

export const makePicoConsole = async (props: {
	canvas?: HTMLCanvasElement;
	codoTextarea?: HTMLTextAreaElement;
	audioContext?: AudioContext;
	carts: PicoCart[];
}): Promise<PicoPlayerHandle> => {
	const {carts, canvas = document.createElement("canvas"), codoTextarea = document.createElement("textarea"), audioContext = new AudioContext()} = props;
	canvas.style.imageRendering = "pixelated";
	codoTextarea.style.display="none";
	codoTextarea.style.position="fixed";
	codoTextarea.style.left="-9999px";
	codoTextarea.style.height="0px";
	codoTextarea.style.overflow="hidden";
	const Module = {canvas, keyboardListeningElement: canvas};
	const cartsDatas = await Promise.all(carts.map(cart => getRom(cart)));
	const handle = rawRenderCart(Module, carts.map(cart => cart.name), cartsDatas, audioContext);
	handle.pico8_state = {};
	handle.pico8_buttons = [0,0,0,0,0,0,0,0];
	handle.pico8_mouse = [0,0,0];
	handle.pico8_gpio = [
		0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
		0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
		0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
		0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
		0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
		0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
		0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
		0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
		0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
		0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
		0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
		0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
		0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
		0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
		0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
		0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
	];
	// let gpioChanged = (gpio: number[]) => {};
	// const gpioInner = [
	// 	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
	// 	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
	// 	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
	// 	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
	// 	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
	// 	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
	// 	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
	// 	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
	// 	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
	// 	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
	// 	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
	// 	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
	// 	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
	// 	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
	// 	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
	// 	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
	// ];
	// handle.pico8_gpio = new Proxy(gpioInner, {
	// 	get(target, prop) {
	// 		return target[prop as any];
	// 	},
	// 	set(target, prop, newValue) {
	// 		const t = target as any;
	// 		if (t.setting) {
	// 			return false;
	// 		}
	// 		const prev = [...target];
	// 		target[prop as any] = newValue;
	// 		const next = [...target];
	// 		if (!t.dontSend && prev.some((p, i) => p !== next[i])) {
	// 			gpioChanged(target);
	// 		}
	// 		return true;
	// 	}
	// });
	// (handle as any).pico8_gpio.subscribe = (f: (gpio: number[]) => void) => {
	// 	gpioChanged = f;
	// }
	handle.pico8_gamepads = {count: 0};
	return {
		raw: handle,
		rawModule: Module,
		canvas,
		state: {
			frameNumber: handle.pico8_state.frame_number!,
			isPaused: !!handle.pico8_state.is_paused!,
			hasFocus: !!handle.pico8_state.has_focus!,
			requestPointerLock: !!handle.pico8_state.request_pointer_lock!,
			requirePageNavigateConfirmation: !!handle.pico8_state.require_page_navigate_confirmation!,
			showDpad: !!handle.pico8_state.show_dpad!,
			shutdownRequested: !!handle.pico8_state.shutdown_requested!,
			soundVolume: handle.pico8_state.sound_volume!,
		},
		gpio: handle.pico8_gpio as PicoPlayerHandle["gpio"],
		setMouse({x, y, leftClick, rightClick}) {
			handle.pico8_mouse = [x, y, bitfield(leftClick, rightClick)];
		},
		buttons: watch(handle.pico8_buttons!),
		setButtons(buttons) {
			// TODO: pad this properly here instead of casting
			handle.pico8_buttons = buttons.map(({left, right, up, down, o, x, menu}) => bitfield(left, right, up, down, o, x, menu)) as any;
		},
		setGamepadCount(count) {
			handle.pico8_gamepads = {count};
		},
		setTouchDetected(touchDetected) {
			handle.p8_touch_detected = touchDetected ? 1 : 0;
		},
		dropCart(cart) {
			handle.p8_dropped_cart_name = cart.name;
			// TODO: make sure this is a dataURL first, and if not, load it and then pass it in
			// handle.p8_dropped_cart = cart.src;
			// handle.codo_command = 9;
		},
		modDragOver: (Module as any).pico8DragOver,
		modDragStop: (Module as any).pico8DragStop,
		togglePaused: (Module as any).pico8TogglePaused,
		toggleSound: (Module as any).pico8ToggleSound,
		toggleControlMenu: (Module as any).pico8ToggleControlMenu,
	}
}