import os
from time import sleep

class PyBoyAdvance:

    def __init__(self, rom: GamePak | str | os.PathLike, bios: str | os.PathLike | None=None, skip_bios: bool=False, emulation_speed: float=0):
        self.gamepak = GamePak.from_file(rom) if isinstance(rom, (str, os.PathLike)) else rom
        if bios:
            with open(bios, 'rb') as bios_file:
                bios_data = bytearray(bios_file.read())
        else:
            bios_data = b''
        self.scheduler = Scheduler()
        self.memory = Memory(self.scheduler, self.gamepak, bios_data)
        self.interrupt_controller = InterruptController(self.scheduler)
        self.dma_controller = DMAController(self.scheduler, self.memory)
        self.timers = Timers(self.scheduler, self.interrupt_controller)
        self.ppu = PPU(self.scheduler, self.interrupt_controller)
        self.keypad = Keypad(self.interrupt_controller)
        self.memory.io = IO(self.memory, self.interrupt_controller, self.dma_controller, self.timers, self.ppu, self.keypad)
        self.cpu = CPU(self.scheduler, self.memory)
        self.cpu.arm_decoder = arm_decoder
        self.cpu.thumb_decoder = thumb_decoder
        self._time_per_frame = 0
        self._last_frame_time = 0
        self._accumulated_time = 0
        self.set_emulation_speed(emulation_speed)
        self.scheduler.schedule(self._frame_limiter, CYCLES_FRAME, EventTrigger.IMMEDIATELY)
        self._frame_overshoot = 0
        if skip_bios:
            self.cpu.regs.banked_sp[int(BankIndex.BANK_SYSTEM_USER)] = 50364160
            self.cpu.regs.banked_sp[int(BankIndex.BANK_FIQ)] = 50364160
            self.cpu.regs.banked_sp[int(BankIndex.BANK_IRQ)] = 50364320
            self.cpu.regs.banked_sp[int(BankIndex.BANK_SWI)] = 50364384
            self.cpu.regs.banked_sp[int(BankIndex.BANK_ABORT)] = 50364160
            self.cpu.regs.banked_sp[int(BankIndex.BANK_UNDEFINED)] = 50364160
            self.cpu.regs.set_sp(50364160)
            self.cpu.regs.set_pc(134217728)
            self.cpu.flush_pipeline()
        else:
            self.cpu.interrupt(ExceptionVector.RESET)
        self.screen = Screen(self.ppu)

    def step(self):
        if self.dma_controller.get_active():
            self.dma_controller.perform_transfers()
        else:
            self.cpu.step()

    def frame(self, count: int=1, render: bool=True):
        self.ppu.rendering_enabled = False
        for frame_num in range(count):
            if frame_num == count - 1 and render:
                self.ppu.rendering_enabled = True
            end_time = self.scheduler.cycles + CYCLES_FRAME - self._frame_overshoot
            while self.scheduler.cycles < end_time:
                self.step()
            self._frame_overshoot = self.scheduler.cycles - end_time

    def run(self):
        with Window() as window:
            running = True
            while running:
                self.frame()
                for event in window.get_events():
                    if event == WindowEvent.WindowEvent_NONE:
                        continue
                    elif event == WindowEvent.QUIT:
                        running = False
                    elif event == WindowEvent.FULLSCREEN:
                        window.set_fullscreen(not window.get_fullscreen())
                    else:
                        self.keypad.process_window_event(event)
                window.render(self.ppu.get_frame_buffer_ptr())

    def press_key(self, key: Key):
        self.keypad.press_key(key)

    def release_key(self, key: Key):
        self.keypad.release_key(key)

    def set_emulation_speed(self, speed: float):
        if speed < 0:
            raise ValueError('Emulation speed must be 0 or greater')
        self._time_per_frame = int(1 / (speed * CLOCK_SPEED_HZ / CYCLES_FRAME) * NANOSECONDS_PER_SECOND) if speed > 0 else 0
        self._last_frame_time = perf_counter_ns()
        self._accumulated_time = 0

    def _frame_limiter(self):
        self.scheduler.schedule(self._frame_limiter, CYCLES_FRAME, EventTrigger.IMMEDIATELY)
        if self._time_per_frame == 0:
            return
        now = perf_counter_ns()
        self._accumulated_time += now - self._last_frame_time
        self._last_frame_time = now
        if self._accumulated_time < self._time_per_frame:
            delay = (self._time_per_frame - self._accumulated_time) / float(NANOSECONDS_PER_SECOND)
            sleep(delay)
        self._accumulated_time -= self._time_per_frame