import Apple2IO from './apple2io';
import {
    HiresPage,
    LoresPage,
    VideoModes,
    VideoModesState,
} from './videomodes';
import {
    HiresPage2D,
    LoresPage2D,
    VideoModes2D,
} from './canvas';
import {
    HiresPageGL,
    LoresPageGL,
    VideoModesGL,
} from './gl';
import ROM from './roms/rom';
import { Apple2IOState } from './apple2io';
import CPU6502, {
    CpuState,
    FLAVOR_6502,
    FLAVOR_ROCKWELL_65C02,
} from './cpu6502';
import MMU, { MMUState } from './mmu';
import RAM, { RAMState } from './ram';

import SYMBOLS from './symbols';
import Debugger, { DebuggerContainer } from './debugger';

import { ReadonlyUint8Array, Restorable, rom } from './types';
import { processGamepad } from './ui/gamepad';

export interface Apple2Options {
    characterRom: string;
    enhanced: boolean;
    e: boolean;
    gl: boolean;
    rom: string;
    canvas: HTMLCanvasElement;
    tick: () => void;
}

export interface Stats {
    cycles: number;
    frames: number;
    renderedFrames: number;
}

export interface State {
    cpu: CpuState;
    vm: VideoModesState;
    io: Apple2IOState;
    mmu: MMUState | undefined;
    ram: RAMState[] | undefined;
}

export class Apple2 implements Restorable<State>, DebuggerContainer {
    private paused = false;

    private theDebugger: Debugger | undefined;

    private runTimer: number | null = null;
    private runAnimationFrame: number | null = null;
    private cpu: CPU6502;

    private gr: LoresPage;
    private gr2: LoresPage;
    private hgr: HiresPage;
    private hgr2: HiresPage;
    private vm: VideoModes;

    private io: Apple2IO;
    private mmu: MMU | undefined;
    private ram: [RAM, RAM, RAM] | undefined;
    private characterRom: rom;
    private rom: ROM;

    private tick: () => void;

    private stats: Stats = {
        cycles: 0,
        frames: 0,
        renderedFrames: 0
    };

    public ready: Promise<void>;

    constructor(options: Apple2Options) {
        this.ready = this.init(options);
    }

    async init(options: Apple2Options) {
        const romImportPromise = import(`./roms/system/${options.rom}`) as Promise<{ default: new () => ROM }>;
        const characterRomImportPromise = import(`./roms/character/${options.characterRom}`) as Promise<{ default: ReadonlyUint8Array }>;

        const LoresPage = options.gl ? LoresPageGL : LoresPage2D;
        const HiresPage = options.gl ? HiresPageGL : HiresPage2D;
        const VideoModes = options.gl ? VideoModesGL : VideoModes2D;

        this.cpu = new CPU6502({
            flavor: options.enhanced ? FLAVOR_ROCKWELL_65C02 : FLAVOR_6502
        });
        this.vm = new VideoModes(options.canvas, options.e);

        const [{ default: Apple2ROM }, { default: characterRom }] = await Promise.all([
            romImportPromise,
            characterRomImportPromise,
            this.vm.ready,
        ]);

        this.rom = new Apple2ROM();
        this.characterRom = characterRom;

        this.gr = new LoresPage(this.vm, 1, this.characterRom, options.e);
        this.gr2 = new LoresPage(this.vm, 2, this.characterRom, options.e);
        this.hgr = new HiresPage(this.vm, 1);
        this.hgr2 = new HiresPage(this.vm, 2);
        this.io = new Apple2IO(this.cpu, this.vm);
        this.tick = options.tick;

        if (options.e) {
            this.mmu = new MMU(this.cpu, this.vm, this.gr, this.gr2, this.hgr, this.hgr2, this.io, this.rom);
            this.cpu.addPageHandler(this.mmu);
        } else {
            this.ram = [
                new RAM(0x00, 0x03),
                new RAM(0x0C, 0x1F),
                new RAM(0x60, 0xBF)
            ];

            this.cpu.addPageHandler(this.ram[0]);
            this.cpu.addPageHandler(this.gr);
            this.cpu.addPageHandler(this.gr2);
            this.cpu.addPageHandler(this.ram[1]);
            this.cpu.addPageHandler(this.hgr);
            this.cpu.addPageHandler(this.hgr2);
            this.cpu.addPageHandler(this.ram[2]);
            this.cpu.addPageHandler(this.io);
            this.cpu.addPageHandler(this.rom);
        }
    }

    /**
     * Runs the emulator. If the emulator is already running, this does
     * nothing. When this function exits either `runTimer` or
     * `runAnimationFrame` will be non-null.
     */
    run() {
        this.paused = false;
        if (this.runTimer || this.runAnimationFrame) {
            return; // already running
        }

        this.theDebugger = new Debugger(this.cpu, this);
        this.theDebugger.addSymbols(SYMBOLS);

        const interval = 30;

        let now, last = Date.now();
        const runFn = () => {
            const kHz = this.io.getKHz();
            now = Date.now();

            const stepMax = kHz * interval;
            let step = (now - last) * kHz;
            last = now;
            if (step > stepMax) {
                step = stepMax;
            }

            if (this.theDebugger) {
                this.theDebugger.stepCycles(step);
            } else {
                this.cpu.stepCycles(step);
            }
            if (this.mmu) {
                this.mmu.resetVB();
            }
            if (this.io.annunciator(0)) {
                const imageData = this.io.blit();
                if (imageData) {
                    this.vm.blit(imageData);
                    this.stats.renderedFrames++;
                }
            } else {
                if (this.vm.blit()) {
                    this.stats.renderedFrames++;
                }
            }
            this.stats.cycles = this.cpu.getCycles();
            this.stats.frames++;
            this.io.tick();
            this.tick();
            processGamepad(this.io);

            if (!this.paused && requestAnimationFrame) {
                this.runAnimationFrame = requestAnimationFrame(runFn);
            }
        };
        if (requestAnimationFrame) {
            this.runAnimationFrame = requestAnimationFrame(runFn);
        } else {
            this.runTimer = window.setInterval(runFn, interval);
        }
    }

    stop() {
        this.paused = true;
        if (this.runTimer) {
            clearInterval(this.runTimer);
        }
        if (this.runAnimationFrame) {
            cancelAnimationFrame(this.runAnimationFrame);
        }
        this.runTimer = null;
        this.runAnimationFrame = null;
    }

    isRunning() {
        return !this.paused;
    }

    getState(): State {
        const state: State = {
            cpu: this.cpu.getState(),
            vm: this.vm.getState(),
            io: this.io.getState(),
            mmu: this.mmu?.getState(),
            ram: this.ram?.map(bank => bank.getState()),
        };

        return state;
    }

    setState(state: State) {
        this.cpu.setState(state.cpu);
        this.vm.setState(state.vm);
        this.io.setState(state.io);
        if (this.mmu && state.mmu) {
            this.mmu.setState(state.mmu);
        }
        if (this.ram) {
            this.ram.forEach((bank, idx) => {
                if (state.ram) {
                    bank.setState(state.ram[idx]);
                }
            });
        }
    }

    reset() {
        this.cpu.reset();
    }

    getStats(): Stats {
        return this.stats;
    }

    getCPU() {
        return this.cpu;
    }

    getIO() {
        return this.io;
    }

    getMMU() {
        return this.mmu;
    }

    getROM() {
        return this.rom;
    }

    getVideoModes() {
        return this.vm;
    }

    getDebugger() {
        return this.theDebugger;
    }
}
