/**
 * DOOM Engine - WebAssembly wrapper for doomgeneric
 */

import { existsSync, readFileSync } from "node:fs";
import { createRequire } from "node:module";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";

export interface DoomModule {
	_doomgeneric_Create: (argc: number, argv: number) => void;
	_doomgeneric_Tick: () => void;
	_DG_GetFrameBuffer: () => number;
	_DG_GetScreenWidth: () => number;
	_DG_GetScreenHeight: () => number;
	_DG_PushKeyEvent: (pressed: number, key: number) => void;
	_malloc: (size: number) => number;
	_free: (ptr: number) => void;
	HEAPU8: Uint8Array;
	HEAPU32: Uint32Array;
	FS_createDataFile: (parent: string, name: string, data: number[], canRead: boolean, canWrite: boolean) => void;
	FS_createPath: (parent: string, path: string, canRead: boolean, canWrite: boolean) => string;
	setValue: (ptr: number, value: number, type: string) => void;
	getValue: (ptr: number, type: string) => number;
}

export class DoomEngine {
	private module: DoomModule | null = null;
	private frameBufferPtr: number = 0;
	private initialized = false;
	private wadPath: string;
	private _width = 640;
	private _height = 400;

	constructor(wadPath: string) {
		this.wadPath = wadPath;
	}

	get width(): number {
		return this._width;
	}

	get height(): number {
		return this._height;
	}

	async init(): Promise<void> {
		// Locate WASM build
		const __dirname = dirname(fileURLToPath(import.meta.url));
		const buildDir = join(__dirname, "doom", "build");
		const doomJsPath = join(buildDir, "doom.js");

		if (!existsSync(doomJsPath)) {
			throw new Error(`WASM not found at ${doomJsPath}. Run ./doom/build.sh first`);
		}

		// Read WAD file
		const wadData = readFileSync(this.wadPath);
		const wadArray = Array.from(new Uint8Array(wadData));

		// Load WASM module - eval to bypass jiti completely
		const doomJsCode = readFileSync(doomJsPath, "utf-8");
		const moduleExports: { exports: unknown } = { exports: {} };
		const nativeRequire = createRequire(doomJsPath);
		const moduleFunc = new Function("module", "exports", "__dirname", "__filename", "require", doomJsCode);
		moduleFunc(moduleExports, moduleExports.exports, buildDir, doomJsPath, nativeRequire);
		const createDoomModule = moduleExports.exports as (config: unknown) => Promise<DoomModule>;

		const moduleConfig = {
			locateFile: (path: string) => {
				if (path.endsWith(".wasm")) {
					return join(buildDir, path);
				}
				return path;
			},
			print: () => {},
			printErr: () => {},
			preRun: [
				(module: DoomModule) => {
					// Create /doom directory and add WAD
					module.FS_createPath("/", "doom", true, true);
					module.FS_createDataFile("/doom", "doom1.wad", wadArray, true, false);
				},
			],
		};

		this.module = await createDoomModule(moduleConfig);
		if (!this.module) {
			throw new Error("Failed to initialize DOOM module");
		}

		// Initialize DOOM
		this.initDoom();

		// Get framebuffer info
		this.frameBufferPtr = this.module._DG_GetFrameBuffer();
		this._width = this.module._DG_GetScreenWidth();
		this._height = this.module._DG_GetScreenHeight();
		this.initialized = true;
	}

	private initDoom(): void {
		if (!this.module) return;

		const args = ["doom", "-iwad", "/doom/doom1.wad"];
		const argPtrs: number[] = [];

		for (const arg of args) {
			const ptr = this.module._malloc(arg.length + 1);
			for (let i = 0; i < arg.length; i++) {
				this.module.setValue(ptr + i, arg.charCodeAt(i), "i8");
			}
			this.module.setValue(ptr + arg.length, 0, "i8");
			argPtrs.push(ptr);
		}

		const argvPtr = this.module._malloc(argPtrs.length * 4);
		for (let i = 0; i < argPtrs.length; i++) {
			this.module.setValue(argvPtr + i * 4, argPtrs[i]!, "i32");
		}

		this.module._doomgeneric_Create(args.length, argvPtr);

		for (const ptr of argPtrs) {
			this.module._free(ptr);
		}
		this.module._free(argvPtr);
	}

	/**
	 * Run one game tick
	 */
	tick(): void {
		if (!this.module || !this.initialized) return;
		this.module._doomgeneric_Tick();
	}

	/**
	 * Get current frame as RGBA pixel data
	 * DOOM outputs ARGB, we convert to RGBA
	 */
	getFrameRGBA(): Uint8Array {
		if (!this.module || !this.initialized) {
			return new Uint8Array(this._width * this._height * 4);
		}

		const pixels = this._width * this._height;
		const buffer = new Uint8Array(pixels * 4);

		for (let i = 0; i < pixels; i++) {
			const argb = this.module.getValue(this.frameBufferPtr + i * 4, "i32");
			const offset = i * 4;
			buffer[offset + 0] = (argb >> 16) & 0xff; // R
			buffer[offset + 1] = (argb >> 8) & 0xff; // G
			buffer[offset + 2] = argb & 0xff; // B
			buffer[offset + 3] = 255; // A
		}

		return buffer;
	}

	/**
	 * Push a key event
	 */
	pushKey(pressed: boolean, key: number): void {
		if (!this.module || !this.initialized) return;
		this.module._DG_PushKeyEvent(pressed ? 1 : 0, key);
	}

	isInitialized(): boolean {
		return this.initialized;
	}
}
