import CPU6502 from './cpu6502';
import { Card, Memory, MemoryPages, TapeData, byte, Restorable } from './types';
import { debug, garbage } from './util';
import { VideoModes } from './videomodes';

export type slot = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7;
export type button = 0 | 1 | 2;
export type paddle = 0 | 1 | 2 | 3;
export type annunciator = 0 | 1 | 2 | 3;

type Annunciators = Record<annunciator, boolean>;

export interface Apple2IOState {
    annunciators: Annunciators;
    cards: Array<unknown | null>;
}

export type SampleListener = (sample: number[]) => void;

const LOC = {
    KEYBOARD: 0x00, // keyboard data (latched) (Read),
    STROBE: 0x10, // clear bit 7 of keyboard data ($C000)
    TAPEOUT: 0x20, // toggle the cassette output.
    SPEAKER: 0x30, // toggle speaker diaphragm
    C040STB: 0x40, // trigger game port sync
    CLRTEXT: 0x50, // display graphics
    SETTEXT: 0x51, // display text
    CLRMIXED: 0x52, // clear mixed mode- enable full graphics
    SETMIXED: 0x53, // enable graphics/text mixed mode
    PAGE1: 0x54, // select text/graphics page1
    PAGE2: 0x55, // select text/graphics page2
    CLRHIRES: 0x56, // select Lo-res
    SETHIRES: 0x57, // select Hi-res
    CLRAN0: 0x58, // Set annunciator-0 output to 0
    SETAN0: 0x59, // Set annunciator-0 output to 1
    CLRAN1: 0x5A, // Set annunciator-1 output to 0
    SETAN1: 0x5B, // Set annunciator-1 output to 1
    CLRAN2: 0x5C, // Set annunciator-2 output to 0
    SETAN2: 0x5D, // Set annunciator-2 output to 1
    CLRAN3: 0x5E, // Set annunciator-3 output to 0
    SETAN3: 0x5F, // Set annunciator-3 output to 1
    TAPEIN: 0x60, // bit 7: data from cassette
    PB0: 0x61, // game Pushbutton 0 / open apple (command) key data
    PB1: 0x62, // game Pushbutton 1 / closed apple (option) key data
    PB2: 0x63, // game Pushbutton 2 (read)
    PADDLE0: 0x64, // bit 7: status of pdl-0 timer (read)
    PADDLE1: 0x65, // bit 7: status of pdl-1 timer (read)
    PADDLE2: 0x66, // bit 7: status of pdl-2 timer (read)
    PADDLE3: 0x67, // bit 7: status of pdl-3 timer (read)
    PDLTRIG: 0x70, // trigger paddles
    ACCEL: 0x74, // CPU Speed control
};

export default class Apple2IO implements MemoryPages, Restorable<Apple2IOState> {
    private _slot: Array<Card | null> = new Array<Card | null>(7).fill(null);
    private _auxRom: Memory | null = null;

    private _khz = 1023;
    private _rate = 44000;
    private _sample_size = 4096;

    private _cycles_per_sample: number;

    private _buffer: string[] = [];
    private _key = 0;
    private _keyDown = false;
    private _button = [false, false, false];
    private _paddle = [0.0, 0.0, 0.0, 0.0];
    private _phase = -1;
    private _sample: number[] = [];
    private _sampleIdx = 0;
    private _sampleTime = 0;
    private _didAudio = false;

    private _high = 0.5;
    private _low = -0.5;

    private _audioListener: SampleListener | undefined;

    private _trigger = 0;
    private _annunciators: Annunciators = [false, false, false, false];

    private _tape: TapeData = [];
    private _tapeOffset: number = 0;
    private _tapeNext: number = 0;
    private _tapeCurrent = false;

    constructor(private readonly cpu: CPU6502, private readonly vm: VideoModes) {
        this.init();
    }

    init() {
        this._calcSampleRate();
    }

    _debug(..._args: unknown[]) {
        // debug.apply(this, arguments);
    }

    _tick() {
        const now = this.cpu.getCycles();
        const phase = this._didAudio ? (this._phase > 0 ? this._high : this._low) : 0.0;
        for (; this._sampleTime < now; this._sampleTime += this._cycles_per_sample) {
            this._sample[this._sampleIdx++] = phase;
            if (this._sampleIdx === this._sample_size) {
                if (this._audioListener) {
                    this._audioListener(this._sample);
                }
                this._sample = new Array<number>(this._sample_size);
                this._sampleIdx = 0;
            }
        }
        this._didAudio = false;
    }

    _calcSampleRate() {
        this._cycles_per_sample = this._khz * 1000 / this._rate;
    }

    _updateKHz(khz: number) {
        this._khz = khz;
        this._calcSampleRate();
    }

    _access(off: byte, val?: byte): byte | undefined {
        let result: number | undefined = 0;
        const now = this.cpu.getCycles();
        const writeMode = val === undefined;
        const delta = now - this._trigger;
        switch (off) {
            case LOC.CLRTEXT:
                this._debug('Graphics Mode');
                this.vm.text(false);
                break;
            case LOC.SETTEXT:
                this._debug('Text Mode');
                this.vm.text(true);
                break;
            case LOC.CLRMIXED:
                this._debug('Mixed Mode off');
                this.vm.mixed(false);
                break;
            case LOC.SETMIXED:
                this._debug('Mixed Mode on');
                this.vm.mixed(true);
                break;
            case LOC.CLRHIRES:
                this._debug('LoRes Mode');
                this.vm.hires(false);
                break;
            case LOC.SETHIRES:
                this._debug('HiRes Mode');
                this.vm.hires(true);
                break;
            case LOC.PAGE1:
                this.vm.page(1);
                break;
            case LOC.PAGE2:
                this.vm.page(2);
                break;
            case LOC.SETAN0:
                this._debug('Annunciator 0 on');
                this._annunciators[0] = true;
                break;
            case LOC.SETAN1:
                this._debug('Annunciator 1 on');
                this._annunciators[1] = true;
                break;
            case LOC.SETAN2:
                this._debug('Annunciator 2 on');
                this._annunciators[2] = true;
                break;
            case LOC.SETAN3:
                this._debug('Annunciator 3 on');
                this._annunciators[3] = true;
                break;
            case LOC.CLRAN0:
                this._debug('Annunciator 0 off');
                this._annunciators[0] = false;
                break;
            case LOC.CLRAN1:
                this._debug('Annunciator 1 off');
                this._annunciators[1] = false;
                break;
            case LOC.CLRAN2:
                this._debug('Annunciator 2 off');
                this._annunciators[2] = false;
                break;
            case LOC.CLRAN3:
                this._debug('Annunciator 3 off');
                this._annunciators[3] = false;
                break;
            case LOC.PB0:
                result = this._button[0] ? 0x80 : 0;
                break;
            case LOC.PB1:
                result = this._button[1] ? 0x80 : 0;
                break;
            case LOC.PB2:
                result = this._button[2] ? 0x80 : 0;
                break;
            case LOC.PADDLE0:
                result = (delta < (this._paddle[0] * 2756) ? 0x80 : 0x00);
                break;
            case LOC.PADDLE1:
                result = (delta < (this._paddle[1] * 2756) ? 0x80 : 0x00);
                break;
            case LOC.PADDLE2:
                result = (delta < (this._paddle[2] * 2756) ? 0x80 : 0x00);
                break;
            case LOC.PADDLE3:
                result = (delta < (this._paddle[3] * 2756) ? 0x80 : 0x00);
                break;
            case LOC.ACCEL:
                if (val !== undefined) {
                    this._updateKHz(val & 0x01 ? 1023 : 4096);
                }
                break;
            case LOC.TAPEIN:
                if (this._tapeOffset === -1) {
                    this._tapeOffset = 0;
                    this._tapeNext = now;
                }

                if (this._tapeOffset < this._tape.length) {
                    this._tapeCurrent = this._tape[this._tapeOffset][1];
                    while (now >= this._tapeNext) {
                        if ((this._tapeOffset % 1000) === 0) {
                            debug(`Read ${this._tapeOffset / 1000}`);
                        }
                        this._tapeCurrent = this._tape[this._tapeOffset][1];
                        this._tapeNext += this._tape[this._tapeOffset++][0];
                    }

                }

                result = this._tapeCurrent ? 0x80 : 0x00;
                break;

            default:
                switch (off & 0xf0) {
                    case LOC.KEYBOARD: // C00x
                        result = this._key;
                        break;
                    case LOC.STROBE: // C01x
                        if (off === LOC.STROBE || writeMode) {
                            this._key &= 0x7f;
                        }
                        if (this._buffer.length > 0) {
                            let val = this._buffer.shift() as string;
                            if (val === '\n') {
                                val = '\r';
                            }
                            this._key = val.charCodeAt(0) | 0x80;
                        }
                        result = this._key & 0x7f;
                        if (off === LOC.STROBE) {
                            result |= this._keyDown ? 0x80 : 0x00;
                        }
                        break;
                    case LOC.TAPEOUT: // C02x
                        // this._phase = -this._phase;
                        // this._didAudio = true;
                        // this._tick();
                        break;
                    case LOC.SPEAKER: // C03x
                        this._phase = -this._phase;
                        this._didAudio = true;
                        this._tick();
                        break;
                    case LOC.C040STB: // C04x
                        // I/O Strobe
                        break;
                    case LOC.PDLTRIG: // C07x
                        this._trigger = this.cpu.getCycles();
                        break;
                }
        }

        if (val !== undefined) {
            result = undefined;
        }

        return result;
    }

    start() {
        return 0xc0;
    }

    end() {
        return 0xcf;
    }

    ioSwitch(off: byte, val?: byte) {
        let result;
        if (off < 0x80) {
            result = this._access(off, val);
        } else {
            const slot = (off & 0x70) >> 4;
            const card = this._slot[slot];
            if (card && card.ioSwitch) {
                result = card.ioSwitch(off, val);
            }
        }

        return result;
    }

    reset() {
        for (let slot = 0; slot < 8; slot++) {
            const card = this._slot[slot];
            if (card) {
                card.reset?.();
            }
        }
        this.vm.reset();
    }

    blit() {
        const card = this._slot[3];
        if (card) {
            return card.blit?.();
        }
        return undefined;
    }

    read(page: byte, off: byte) {
        let result: number = 0;
        let slot;
        let card;

        switch (page) {
            case 0xc0:
                result = this.ioSwitch(off, undefined) || 0;
                break;
            case 0xc1:
            case 0xc2:
            case 0xc3:
            case 0xc4:
            case 0xc5:
            case 0xc6:
            case 0xc7:
                slot = page & 0x0f;
                card = this._slot[slot];
                if (this._auxRom !== card) {
                // _debug('Setting auxRom to slot', slot);
                    this._auxRom = card;
                }
                if (card) {
                    result = card.read(page, off);
                } else {
                    result = garbage();
                }
                break;
            default:
                if (this._auxRom) {
                    result = this._auxRom.read(page, off);
                }
                break;
        }
        return result;
    }

    write(page: byte, off: byte, val: byte) {
        let slot;
        let card;

        switch (page) {
            case 0xc0:
                this.ioSwitch(off, val);
                break;
            case 0xc1:
            case 0xc2:
            case 0xc3:
            case 0xc4:
            case 0xc5:
            case 0xc6:
            case 0xc7:
                slot = page & 0x0f;
                card = this._slot[slot];
                if (this._auxRom !== card) {
                // _debug('Setting auxRom to slot', slot);
                    this._auxRom = card;
                }
                if (card) {
                    card.write(page, off, val);
                }
                break;
            default:
                if (this._auxRom) {
                    this._auxRom.write(page, off, val);
                }
                break;
        }
    }

    getState(): Apple2IOState {
        // TODO vet more potential state
        return {
            annunciators: this._annunciators,
            cards: this._slot.map((card) => card ? card.getState() : null)
        };
    }

    setState(state: Apple2IOState) {
        this._annunciators = state.annunciators;
        state.cards.map((cardState, idx) => this._slot[idx]?.setState(cardState));
    }

    setSlot(slot: slot, card: Card) {
        this._slot[slot] = card;
    }

    getSlot(slot: slot): Card | null {
        return this._slot[slot];
    }

    keyDown(ascii: byte) {
        this._keyDown = true;
        this._key = ascii | 0x80;
    }

    keyUp() {
        this._keyDown = false;
    }

    buttonDown(b: button, state = true) {
        this._button[b] = state;
    }

    buttonUp(b: button) {
        this._button[b] = false;
    }

    paddle(p: paddle, v: byte) {
        this._paddle[p] = v;
    }

    updateKHz(khz: number) {
        this._updateKHz(khz);
    }

    getKHz() {
        return this._khz;
    }

    setKeyBuffer(buffer: string) {
        this._buffer = buffer.split(''); // split to charaters
        if (this._buffer.length > 0) {
            this._keyDown = true;
            const key = this._buffer.shift() as string; // never undefined
            this._key = key.charCodeAt(0) | 0x80;
        }
    }

    setTape(tape: TapeData) {
        debug(`Tape length: ${tape.length}`);
        this._tape = tape;
        this._tapeOffset = -1;
    }

    sampleRate(rate: number, sample_size: number) {
        this._rate = rate;
        this._sample_size = sample_size;
        this._sample = new Array<number>(this._sample_size);
        this._sampleIdx = 0;
        this._calcSampleRate();
    }

    tick() {
        this._tick();
        for (let idx = 0; idx < 8; idx++) {
            this._slot[idx]?.tick?.();
        }
    }

    addSampleListener(cb: SampleListener) {
        this._audioListener = cb;
    }

    annunciator(idx: annunciator) {
        return this._annunciators[idx];
    }

    cycles() {
        return this.cpu.getCycles();
    }
}
