
// GTIA
// https://user.xmission.com/~trevin/atari/gtia_regs.html
// https://user.xmission.com/~trevin/atari/gtia_pinout.html

import { dumpRAM, gtia_ntsc_to_rgb } from "../../common/emu";
import { hex, lpad, safe_extend } from "../../common/util";


// write regs
const HPOSP0 = 0x0;
const HPOSM0 = 0x4;
const SIZEP0 = 0x8;
const SIZEM = 0x0c;
const GRAFP0 = 0x0d;
const GRAFM = 0x11;
const COLPM0 = 0x12;
const COLPF0 = 0x16;
const COLPF1 = 0x17;
const COLPF2 = 0x18;
const COLPF3 = 0x19;
const COLBK = 0x1a;
const PRIOR = 0x1b;
const VDELAY = 0x1c; // TODO
const GRACTL = 0x1d;
const HITCLR = 0x1e;
const CONSPK = 0x1f;
// read regs
const M0PF = 0x0;
const P0PF = 0x4;
const M0PL = 0x8;
const P0PL = 0xc;
export const TRIG0 = 0x10;
export const CONSOL = 0x1f;

const HOFFSET = -9; // bias to account for antic->gtia delay

const PRIOR_TABLE : number[] = [
  0,1,2,3, 7,7,7,7, 8,8,8,8, 4,5,6,7,   // 0001 - 0
  0,1,2,3, 7,7,7,7, 8,8,8,8, 4,5,6,7,   // 0001
  0,1,6,7, 5,5,5,5, 8,8,8,8, 2,3,4,5,   // 0010 - 2
  0,1,6,7, 5,5,5,5, 8,8,8,8, 2,3,4,5,   // 0010
  4,5,6,7, 3,3,3,3, 8,8,8,8, 0,1,2,3,   // 0100 - 4
  4,5,6,7, 3,3,3,3, 8,8,8,8, 0,1,2,3,   // 0100
  4,5,6,7, 3,3,3,3, 8,8,8,8, 0,1,2,3,   // 0100
  4,5,6,7, 3,3,3,3, 8,8,8,8, 0,1,2,3,   // 0100
  2,3,4,5, 7,7,7,7, 8,8,8,8, 0,1,6,7,   // 1000 - 8
  2,3,4,5, 7,7,7,7, 8,8,8,8, 0,1,6,7,   // 1000
  2,3,4,5, 7,7,7,7, 8,8,8,8, 0,1,6,7,   // 1000
  2,3,4,5, 7,7,7,7, 8,8,8,8, 0,1,6,7,   // 1000
  2,3,4,5, 7,7,7,7, 8,8,8,8, 0,1,6,7,   // 1000
  2,3,4,5, 7,7,7,7, 8,8,8,8, 0,1,6,7,   // 1000
  2,3,4,5, 7,7,7,7, 8,8,8,8, 0,1,6,7,   // 1000
  2,3,4,5, 7,7,7,7, 8,8,8,8, 0,1,6,7,   // 1000
];

const MODE_9_LOOKUP = [
    COLPM0+0, COLPM0+1, COLPM0+2, COLPM0+3,
    COLPF0+0, COLPF0+1, COLPF0+2, COLPF0+3,
    COLBK, COLBK, COLBK, COLBK,
    COLPF0+0, COLPF0+1, COLPF0+2, COLPF0+3,
]

export class GTIA {
    regs = new Uint8Array(0x20);
    readregs = new Uint8Array(0x20);
    shiftregs = new Uint32Array(8);

    count = 0;
    an = 0;
    rgb = 0;
    pmcol = 0;
    gtiacol = 0;
    gtiacol2 = 0;
    hbias = HOFFSET;
    pmDebugMask = -1;

    reset() {
        this.regs.fill(0);
        this.readregs.fill(0); // TODO?
        this.readregs[0x14] = 0xf; // NTSC
        this.readregs.fill(0xf, 0x15); // default value for write-only regs
        this.count = 0;
    }
    saveState() {
        return safe_extend(0, {}, this);
    }
    loadState(s) {
        safe_extend(0, this, s);
    }
    setReg(a: number, v: number) {
        switch (a) {
            case COLPM0: case COLPM0+1: case COLPM0+2: case COLPM0+3:
            case COLPF0: case COLPF0+1: case COLPF0+2: case COLPF0+3:
            case COLBK:
                v &= 0xfe; // bit 0 unused in color regs
                break;
            case HITCLR:
                this.readregs.fill(0, 0, 16);
                return;
        }
        this.regs[a] = v;
    }
    readReg(a: number) {
        switch (a) {
            case CONSOL:
                return this.readregs[a] & ~this.regs[CONSPK];
        }
        return this.readregs[a];
    }
    sync() {
        this.count = 0;
    }
    setBias(b: number) {
        this.hbias = HOFFSET + b;
    }
    updateGfx(h: number, v: number, data: number) {
        switch (h) {
            case 0:
                if (this.regs[GRACTL] & 1) {
                    // TODO: VDELAY
                    this.regs[GRAFM] = data;
                }
                break;
            case 2: case 3: case 4: case 5:
                if (this.regs[GRACTL] & 2) {
                    if (!(v&1) || !(this.regs[VDELAY] & (1<<(h+2))))
                        this.regs[GRAFP0 - 2 + h] = data;
                }
                break;
        }
    }
    getPlayfieldColor(): number {
        // which GTIA mode?
        switch (this.regs[PRIOR] >> 6) {
            // normal mode
            case 0:
                switch (this.an) {
                    case 0:
                        return COLBK;
                    case 4: case 5: case 6: case 7:
                        return COLPF0 + this.an - 4;
                    case 8:
                        // combine PF2 hue and PF1 luminance
                        return (this.regs[COLPF2] & 0xf0) | (this.regs[COLPF1] & 0x0f) | 0x100;
                }
                break;
            // mode 9 -- 16 luminances
            case 1:
                return (this.regs[COLBK] & 0xf0) | (this.gtiacol & 0xf) | 0x100;
            // mode 10 -- 9 colors from registers
            case 2:
                return MODE_9_LOOKUP[this.gtiacol];
            // mode 11 -- 16 hues
            case 3:
                return (this.regs[COLBK] & 0xf) | (this.gtiacol << 4) | 0x100;
        }
        return 0x100; // black
    }
    anySpriteActive() {
        return this.shiftregs[0] || this.shiftregs[1] || this.shiftregs[2]
          || this.shiftregs[3] || this.shiftregs[4] || this.shiftregs[5]
          || this.shiftregs[6] || this.shiftregs[7];
    }
    processPlayerMissile() {
        // no p/m gfx, just evaluate horiz. triggers
        if (!this.anySpriteActive()) {
            this.evalTrigger(0);
            this.evalTrigger(1);
            this.evalTrigger(2);
            this.evalTrigger(3);
            this.evalTrigger(4);
            this.evalTrigger(5);
            this.evalTrigger(6);
            this.evalTrigger(7);
            this.pmcol = -1;
            return;
        }
        // no collisions in blank area, but shift and trigger anyway
        if (this.an == 2) {
            this.shiftObject(0);
            this.shiftObject(1);
            this.shiftObject(2);
            this.shiftObject(3);
            this.shiftObject(4);
            this.shiftObject(5);
            this.shiftObject(6);
            this.shiftObject(7);
            this.pmcol = -1;
            return;
        }
        // TODO: gtia, hi-res mode collisions
        // compute gfx and collisions for players/missiles
        let priobias = (this.regs[PRIOR] & 15) << 4; // TODO
        let topprio = PRIOR_TABLE[(this.an & 7) + 8 + priobias];
        let pfset = this.an - 4; // TODO?
        let topobj = -1;
        let ppmask = 0;
        // players
        for (let i = 0; i < 4; i++) {
            let bit = this.shiftObject(i);
            if (bit) {
                if (pfset >= 0) { // TODO: hires and GTIA modes
                    this.readregs[P0PF + i] |= 1 << pfset;
                }
                ppmask |= 1 << i;
                let prio = PRIOR_TABLE[i + priobias];
                if (prio < topprio) {
                    topobj = i;
                    topprio = prio;
                }
            }
        }
        // missiles
        for (let i = 0; i < 4; i++) {
            let bit = this.shiftObject(i + 4);
            if (bit) {
                if (pfset >= 0) {
                    this.readregs[M0PF + i] |= 1 << pfset;
                }
                this.readregs[M0PL + i] |= ppmask;
                let prio = (this.regs[PRIOR] & 0x10) 
                    ? PRIOR_TABLE[priobias + 15]
                    : PRIOR_TABLE[i + priobias];
                if (prio < topprio) {
                    topobj = i + 4;
                    topprio = prio;
                }
            }
        }
        // set player-player collision flags
        // TODO: either as a player or a GTIA mode 2 color
        if (ppmask & 1) this.readregs[P0PL + 0] |= ppmask & ~1;
        if (ppmask & 2) this.readregs[P0PL + 1] |= ppmask & ~2;
        if (ppmask & 4) this.readregs[P0PL + 2] |= ppmask & ~4;
        if (ppmask & 8) this.readregs[P0PL + 3] |= ppmask & ~8;
        this.pmcol = topobj >= 0 ? this.getObjectColor(topobj) : -1;
    }
    shiftObject(i: number) {
        let bit = (this.shiftregs[i] & 0x80000000) != 0;
        this.shiftregs[i] <<= 1;
        this.evalTrigger(i);
        return bit;
    }
    getObjectColor(i: number) {
        if ((this.regs[PRIOR] & 0x10) && i >= 4) {
            return this.regs[COLPF3];
        } else {
            return this.regs[COLPM0 + (i & 3)];
        }
    }
    evalTrigger(i: number) {
        if (this.regs[HPOSP0 + i] + this.hbias == this.count) {
            this.triggerObject(i);
        }
    }
    triggerObject(i: number) {
        let size, data;
        if (!(this.pmDebugMask & (1<<i))) return;
        if (i < 4) {
            size = this.regs[SIZEP0 + i] & 3;
            data = this.regs[GRAFP0 + i];
        } else {
            let s = (i - 4) << 1;
            size = (this.regs[SIZEM] >> s) & 3;
            data = ((this.regs[GRAFM] >> s) & 3) << 6;
        }
        if (size & 1) data = expandBits(data); else data <<= 8;
        if (size == 3) data = expandBits(data); else data <<= 16;
        this.shiftregs[i] |= data;
    }

    clockPulse1(): void {
        this.processPlayerMissile();
        this.clockPulse2();
        this.count++;
    }

    clockPulse2(): void {
        var col: number;
        if (this.pmcol >= 0) {
            col = this.pmcol;
        } else {
            let pf = this.getPlayfieldColor();
            col = pf & 0x100 ? pf & 0xff : this.regs[pf];
        }
        this.rgb = COLORS_RGBA[col];
        // TODO: hires modes return 8, so other modes wont work
        this.gtiacol2 = (this.gtiacol2 << 1) | (this.an >> 3);
    }

    clockPulse4() {
        // latch GTIA buffer
        this.gtiacol = this.gtiacol2 & 15;
    }

    static stateToLongString(state): string {
        let s = ''
        s += `X: ${lpad(state.count, 3)}  ANTIC: ${hex(state.an, 1)}  PM: ${hex(state.pmcol, 3)}\n`;
        s += "Write Registers:\n";
        s += dumpRAM(state.regs, 0, 32);
        s += "Read Registers:\n";
        s += dumpRAM(state.readregs, 0, 32);
        return s;
    }
}

function expandBits(x: number): number {
    x = (x | (x << 8)) & 0x00FF00FF;
    x = (x | (x << 4)) & 0x0F0F0F0F;
    x = (x | (x << 2)) & 0x33333333;
    x = (x | (x << 1)) & 0x55555555;
    return x | (x << 1);
}

var COLORS_RGBA = new Uint32Array(256);
for (var i = 0; i < 256; i++) {
    COLORS_RGBA[i] = gtia_ntsc_to_rgb(i);
}

