import { CpuEvent, MemoryCellType } from "./events";
import { u8 } from "./etc";
import { EventHandler } from "./eventHandler";
import { ConstParam, Instruction, ISA, MemorParam, RegisParam } from "./instructionSet";

export type TempInstrState = {
	pos: u8;
	params_found: number;
	instr: Instruction;
	params: Uint8Array;
};
export type ComputerState = {
	memory: Uint8Array;
	program_counter: u8;
	registers: Uint8Array;
	current_instruction: TempInstrState | null;
};

export class Computer {
	private memory: Uint8Array;
	private program_counter: u8;
	private registers: Uint8Array;
	private current_instr: TempInstrState | null;
	events: EventHandler<CpuEvent>;

	constructor() {
		// 256 bytes for both program and general purpose memory.
		this.memory = new Uint8Array(256);
		// 8 registers
		this.registers = new Uint8Array(8);
		this.program_counter = 0;
		this.current_instr = null;
		this.events = new EventHandler<CpuEvent>();

		// Add events
		for (const [, e_type] of Object.entries(CpuEvent)) {
			this.events.register_event(e_type as CpuEvent);
		}
		this.events.seal();
	}

	cycle(): void {
		const current_byte = this.memory[this.program_counter];
		if (this.current_instr === null) {
			const parsed_instruction = ISA.getInstruction(current_byte);
			if (parsed_instruction === null) {
				this.events.dispatch(CpuEvent.MemoryByteParsed, {
					type: MemoryCellType.InvalidInstruction,
					pos: this.program_counter,
				});
				console.log(`Invalid instruction: ${current_byte.toString(16).toUpperCase().padStart(2, "0")}`);
				this.step_forward();
				return;
			}
			this.current_instr = {
				pos: this.program_counter,
				instr: parsed_instruction,
				params_found: 0,
				params: new Uint8Array(parsed_instruction.params.length),
			};
			this.events.dispatch(CpuEvent.MemoryByteParsed, {
				type: MemoryCellType.Instruction,
				pos: this.program_counter,
				instr: parsed_instruction,
			});
		}

		if (this.current_instr.pos === this.program_counter && this.current_instr.params.length > 0) {
			this.step_forward();
			return;
		}

		if (this.current_instr.params.length !== this.current_instr.params_found) {
			let a;
			const b = this.current_instr.instr.params[this.current_instr.params_found];
			if (b instanceof ConstParam) {
				a = MemoryCellType.Constant;
			} else if (b instanceof RegisParam) {
				a = MemoryCellType.Register;
			} else if (b instanceof MemorParam) {
				a = MemoryCellType.Memory;
			}
			this.events.dispatch(CpuEvent.MemoryByteParsed, { type: a, pos: this.program_counter, param: b });
			this.current_instr.params[this.current_instr.params_found] = current_byte;
			this.current_instr.params_found += 1;
			if (this.current_instr.params.length !== this.current_instr.params_found) {
				this.step_forward();
				return;
			}
		}
		const execution_post_action_state = {
			should_step: true,
			noStep: function (): void {
				this.should_step = false;
			},
			event: this.events.dispatch.bind(this.events),
		};
		this.current_instr.instr.execute(this, this.current_instr.params, execution_post_action_state);

		this.current_instr = null;

		if (execution_post_action_state.should_step) {
			this.step_forward();
		}
	}

	getMemory(address: u8): u8 {
		return this.memory[address];
	}
	setMemory(address: u8, value: u8): void {
		this.events.dispatch(CpuEvent.MemoryChanged, { address, value });
		this.memory[address] = value;
	}

	getRegister(register_no: u8): u8 {
		return this.registers[register_no];
	}
	setRegister(register_no: u8, value: u8): void {
		this.events.dispatch(CpuEvent.RegisterChanged, { register_no, value });
		this.registers[register_no] = value;
	}

	getProgramCounter(): u8 {
		return this.program_counter;
	}
	setProgramCounter(new_value: u8): void {
		this.events.dispatch(CpuEvent.ProgramCounterChanged, { counter: new_value });
		this.program_counter = new_value;
	}

	reset(): void {
		this.memory = new Uint8Array(256);
		this.registers = new Uint8Array(8);
		this.current_instr = null;
		this.program_counter = 0;
		this.events.dispatch(CpuEvent.Reset, null);
	}

	load_memory(program: Array<u8>): void {
		const max_loop = Math.min(this.memory.length, program.length);
		for (let i = 0; i < max_loop; i++) {
			this.memory[i] = program[i];
			this.events.dispatch(CpuEvent.MemoryChanged, { address: i, value: program[i] });
		}
		this.program_counter = 0;
	}

	private step_forward(): void {
		this.program_counter = (this.program_counter + 1) % 256;
		this.events.dispatch(CpuEvent.ProgramCounterChanged, { counter: this.program_counter });
	}
}