'use strict';
/*
 * js99'er - TI-99/4A emulator written in JavaScript
 *
 * Created 2014 by Rasmus Moustgaard <rasmus.moustgaard@gmail.com>
 *
 * TMS9918A VDP emulation.
 *
 * https://github.com/Rasmus-M/Js99er
 * GNU General Public License v2.0
 */

import { hex, lpad, RGBA } from "../util";

enum TMS9918A_Mode {
  GRAPHICS = 0,
  TEXT = 1,
  BITMAP = 2,
  MULTICOLOR = 3,
  MODE4 = 4,
  BITMAP_TEXT = 5,
  BITMAP_MULTICOLOR = 6,
  ILLEGAL = 7,
};

interface CPUInterface {
    setVDPInterrupt(b:boolean);
}

export class TMS9918A {

  cru : { setVDPInterrupt: (b:boolean) => void };
  enableFlicker : boolean;

  ram = new Uint8Array(16384); // VDP RAM
  registers = new Uint8Array(8);
  spriteBuffer = new Uint8Array(256);
  addressRegister : number;
  statusRegister : number;

  palette : number[];

  latch : boolean;
  prefetchByte : number;

  displayOn : boolean = false;
  interruptsOn : boolean = false;
  screenMode : number;
  bitmapMode : boolean;
  textMode : boolean;
  colorTable : number;
  nameTable : number;
  charPatternTable : number;
  spriteAttributeTable : number;
  spritePatternTable : number;
  colorTableMask : number;
  patternTableMask : number;
  ramMask : number;
  fgColor : number;
  bgColor : number;

  flicker : boolean;
  redrawRequired : boolean;

  fb32 : Uint32Array;

  width : number;
  height : number;

  constructor(fb32:Uint32Array, cru:CPUInterface, enableFlicker:boolean) {

    this.fb32 = fb32;
    this.cru = cru;
    this.enableFlicker = enableFlicker;

    this.palette = [
        RGBA(0, 0, 0),
        RGBA(0, 0, 0),
        RGBA(33, 200, 66),
        RGBA(94, 220, 120),
        RGBA(84, 85, 237),
        RGBA(125, 118, 252),
        RGBA(212, 82, 77),
        RGBA(66, 235, 245),
        RGBA(252, 85, 84),
        RGBA(255, 121, 120),
        RGBA(212, 193, 84),
        RGBA(230, 206, 128),
        RGBA(33, 176, 59),
        RGBA(201, 91, 186),
        RGBA(204, 204, 204),
        RGBA(255, 255, 255)
    ];
  }

    reset() {

        var i;
        this.ram.fill(0);
        this.registers.fill(0);
        
        this.addressRegister = 0;
        this.statusRegister = 0;

        this.prefetchByte = 0;
        this.latch = false;

        this.displayOn = false;
        this.interruptsOn = false;
        this.screenMode = TMS9918A_Mode.GRAPHICS;
        this.bitmapMode = false;
        this.textMode = false;
        this.colorTable = 0;
        this.nameTable = 0;
        this.charPatternTable = 0;
        this.spriteAttributeTable = 0;
        this.spritePatternTable = 0;
        this.colorTableMask = 0x3FFF;
        this.patternTableMask = 0x3FFF;
        this.ramMask = 0x3FFF;
        this.fgColor = 0;
        this.bgColor = 0;

        this.flicker = this.enableFlicker;
        this.redrawRequired = true;

        this.width = 304;
        this.height = 240;
    }

    drawScanline(y:number) {
        var imageData = this.fb32,
            width = this.width,
            imageDataAddr = (y * width),
            screenMode = this.screenMode,
            textMode = this.textMode,
            bitmapMode = this.bitmapMode,
            drawWidth = !textMode ? 256 : 240,
            drawHeight = 192,
            hBorder = (width - drawWidth) >> 1,
            vBorder = (this.height - drawHeight) >> 1,
            fgColor = this.fgColor,
            bgColor = this.bgColor,
            ram = this.ram,
            nameTable = this.nameTable,
            colorTable = this.colorTable,
            charPatternTable = this.charPatternTable,
            colorTableMask = this.colorTableMask,
            patternTableMask = this.patternTableMask,
            spriteAttributeTable = this.spriteAttributeTable,
            spritePatternTable = this.spritePatternTable,
            spriteSize = (this.registers[1] & 0x2) !== 0,
            spriteMagnify = this.registers[1] & 0x1,
            spriteDimension = (spriteSize ? 16 : 8) << (spriteMagnify ? 1 : 0),
            maxSpritesOnLine = this.flicker ? 4 : 32,
            palette = this.palette,
            collision = false, fifthSprite = false, fifthSpriteIndex = 31,
            x, color, rgbColor, name, tableOffset, colorByte, patternByte;
        if (y >= vBorder && y < vBorder + drawHeight && this.displayOn) {
            var y1 = y - vBorder;
            // Pre-process sprites
            if (!textMode) {
                var spriteBuffer = this.spriteBuffer;
                spriteBuffer.fill(0);
                var spritesOnLine = 0;
                var endMarkerFound = false;
                var spriteAttributeAddr = spriteAttributeTable;
                var s;
                for (s = 0; s < 32 && spritesOnLine <= maxSpritesOnLine && !endMarkerFound; s++) {
                    var sy = ram[spriteAttributeAddr];
                    if (sy !== 0xD0) {
                        if (sy > 0xD0) {
                            sy -= 256;
                        }
                        sy++;
                        var sy1 = sy + spriteDimension;
                        var y2 = -1;
                        if (s < 8 || !bitmapMode) {
                            if (y1 >= sy && y1 < sy1) {
                                y2 = y1;
                            }
                        }
                        else {
                            // Emulate sprite duplication bug
                            var yMasked = y1 & (((this.registers[4] & 0x03) << 6) | 0x3F);
                            if (yMasked >= sy && yMasked < sy1) {
                                y2 = yMasked;
                            }
                            else if (y1 >= 64 && y1 < 128 && y1 >= sy && y1 < sy1) {
                                y2 = y1;
                            }
                        }
                        if (y2 !== -1) {
                            if (spritesOnLine < maxSpritesOnLine) {
                                var sx = ram[spriteAttributeAddr + 1];
                                var sPatternNo = ram[spriteAttributeAddr + 2] & (spriteSize ? 0xFC : 0xFF);
                                var sColor = ram[spriteAttributeAddr + 3] & 0x0F;
                                if ((ram[spriteAttributeAddr + 3] & 0x80) !== 0) {
                                    sx -= 32;
                                }
                                var sLine = (y2 - sy) >> spriteMagnify;
                                var sPatternBase = spritePatternTable + (sPatternNo << 3) + sLine;
                                for (var sx1 = 0; sx1 < spriteDimension; sx1++) {
                                    var sx2 = sx + sx1;
                                    if (sx2 >= 0 && sx2 < drawWidth) {
                                        var sx3 = sx1 >> spriteMagnify;
                                        var sPatternByte = ram[sPatternBase + (sx3 >= 8 ? 16 : 0)];
                                        if ((sPatternByte & (0x80 >> (sx3 & 0x07))) !== 0) {
                                            if (spriteBuffer[sx2] === 0) {
                                                spriteBuffer[sx2] = sColor + 1;
                                            }
                                            else {
                                                collision = true;
                                            }
                                        }
                                    }
                                }
                            }
                            spritesOnLine++;
                        }
                        spriteAttributeAddr += 4;
                    }
                    else {
                        endMarkerFound = true;
                    }
                }
                if (spritesOnLine > 4) {
                    fifthSprite = true;
                    fifthSpriteIndex = s;
                }
            }
            // Draw
            var rowOffset = !textMode ? (y1 >> 3) << 5 : (y1 >> 3) * 40;
            var lineOffset = y1 & 7;
            for (x = 0; x < width; x++) {
                if (x >= hBorder && x < hBorder + drawWidth) {
                    var x1 = x - hBorder;
                    // Tiles
                    switch (screenMode) {
                        case TMS9918A_Mode.GRAPHICS:
                            name = ram[nameTable + rowOffset + (x1 >> 3)];
                            colorByte = ram[colorTable + (name >> 3)];
                            patternByte = ram[charPatternTable + (name << 3) + lineOffset];
                            color = (patternByte & (0x80 >> (x1 & 7))) !== 0 ? (colorByte & 0xF0) >> 4 : colorByte & 0x0F;
                            break;
                        case TMS9918A_Mode.BITMAP:
                            name = ram[nameTable + rowOffset + (x1 >> 3)];
                            tableOffset = ((y1 & 0xC0) << 5) + (name << 3);
                            colorByte = ram[colorTable + (tableOffset & colorTableMask) + lineOffset];
                            patternByte = ram[charPatternTable + (tableOffset & patternTableMask) + lineOffset];
                            color = (patternByte & (0x80 >> (x1 & 7))) !== 0 ? (colorByte & 0xF0) >> 4 : colorByte & 0x0F;
                            break;
                        case TMS9918A_Mode.MULTICOLOR:
                            name = ram[nameTable + rowOffset + (x1 >> 3)];
                            lineOffset = (y1 & 0x1C) >> 2;
                            patternByte = ram[charPatternTable + (name << 3) + lineOffset];
                            color = (x1 & 4) === 0 ? (patternByte & 0xF0) >> 4 : patternByte & 0x0F;
                            break;
                        case TMS9918A_Mode.TEXT:
                            name = ram[nameTable + rowOffset + Math.floor(x1 / 6)];
                            patternByte = ram[charPatternTable + (name << 3) + lineOffset];
                            color = (patternByte & (0x80 >> (x1 % 6))) !== 0 ? fgColor : bgColor;
                            break;
                        case TMS9918A_Mode.BITMAP_TEXT:
                            name = ram[nameTable + rowOffset + Math.floor(x1 / 6)];
                            tableOffset = ((y1 & 0xC0) << 5) + (name << 3);
                            patternByte = ram[charPatternTable + (tableOffset & patternTableMask) + lineOffset];
                            color = (patternByte & (0x80 >> (x1 % 6))) !== 0 ? fgColor : bgColor;
                            break;
                        case TMS9918A_Mode.BITMAP_MULTICOLOR:
                            name = ram[nameTable + rowOffset + (x1 >> 3)];
                            lineOffset = (y1 & 0x1C) >> 2;
                            tableOffset = ((y1 & 0xC0) << 5) + (name << 3);
                            patternByte = ram[charPatternTable + (tableOffset & patternTableMask) + lineOffset];
                            color = (x1 & 4) === 0 ? (patternByte & 0xF0) >> 4 : patternByte & 0x0F;
                            break;
                        case TMS9918A_Mode.ILLEGAL:
                            color = (x1 & 4) === 0 ? fgColor : bgColor;
                            break;
                    }
                    if (color === 0) {
                        color = bgColor;
                    }
                    // Sprites
                    if (!textMode) {
                        var spriteColor = spriteBuffer[x1] - 1;
                        if (spriteColor > 0) {
                            color = spriteColor;
                        }
                    }
                }
                else {
                    color = bgColor;
                }
                rgbColor = palette[color];
                imageData[imageDataAddr++] = rgbColor;
            }
        }
        // Top/bottom border
        else {
            rgbColor = palette[bgColor];
            for (x = 0; x < width; x++) {
                imageData[imageDataAddr++] = rgbColor;
            }
        }
        if (y === vBorder + drawHeight) {
            this.statusRegister |= 0x80;
            if (this.interruptsOn) {
                this.cru.setVDPInterrupt(true);
            }
        }
        if (collision) {
            this.statusRegister |= 0x20;
        }
        if ((this.statusRegister & 0x40) === 0) {
            this.statusRegister |= fifthSpriteIndex;
        }
        if (fifthSprite) {
            this.statusRegister |= 0x40;
        }
    }

    setReadAddress(i:number) {
        this.addressRegister = ((i & 0x3f) << 8) | (this.addressRegister & 0x00FF);
        this.prefetchByte = this.ram[this.addressRegister++];
        this.addressRegister &= 0x3FFF;
    }
    
    setWriteAddress(i:number) {
        this.addressRegister = ((i & 0x3f) << 8) | (this.addressRegister & 0x00FF);
    }
    
    setVDPWriteRegister(i:number) {
        var regmask = this.registers.length-1;
        this.registers[i & regmask] = this.addressRegister & 0x00FF;
        switch (i & regmask) {
            // Mode
            case 0:
                this.updateMode(this.registers[0], this.registers[1]);
                break;
            case 1:
                this.ramMask = (this.registers[1] & 0x80) !== 0 ? 0x3FFF : 0x1FFF;
                this.displayOn = (this.registers[1] & 0x40) !== 0;
                this.interruptsOn = (this.registers[1] & 0x20) !== 0;
                this.updateMode(this.registers[0], this.registers[1]);
                break;
            // Name table
            case 2:
                this.nameTable = (this.registers[2] & 0xf) << 10;
                break;
            // Color table
            case 3:
                if (this.bitmapMode) {
                    this.colorTable = (this.registers[3] & 0x80) << 6;
                }
                else {
                    this.colorTable = this.registers[3] << 6;
                }
                this.updateTableMasks();
                break;
            // Pattern table
            case 4:
                if (this.bitmapMode) {
                    this.charPatternTable = (this.registers[4] & 0x4) << 11;
                }
                else {
                    this.charPatternTable = (this.registers[4] & 0x7) << 11;
                }
                this.updateTableMasks();
                break;
            // Sprite attribute table
            case 5:
                this.spriteAttributeTable = (this.registers[5] & 0x7f) << 7;
                break;
            // Sprite pattern table
            case 6:
                this.spritePatternTable = (this.registers[6] & 0x7) << 11;
                break;
            // Background
            case 7:
                this.fgColor = (this.registers[7] & 0xf0) >> 4;
                this.bgColor = this.registers[7] & 0x0f;
                break;
        }
        // this.logRegisters();
        // this.log.info("Name table: " + this.nameTable.toHexWord());
        // this.log.info("Pattern table: " + this.charPatternTable.toHexWord());
    }

    setVDPWriteCommand3(i:number) {
            this.setVDPWriteRegister(i);
    }
    
    writeAddress(i:number) {
        if (!this.latch) {
            this.addressRegister = (this.addressRegister & 0xFF00) | i;
        } else {
            switch ((i & 0xc0) >> 6) {
                // Set read address
                case 0:
                    this.setReadAddress(i);
                    break;
                // Set write address
                case 1:
                    this.setWriteAddress(i);
                    break;
                // Write register
                case 2:
                    this.setVDPWriteRegister(i);
                    break;
                // Color RAM (SMS only)
                case 3:
                    this.setVDPWriteCommand3(i);
                    break;
            }
            this.redrawRequired = true;
        }
        this.latch = !this.latch;
    }

    updateMode(reg0:number, reg1:number) {
        this.bitmapMode = (reg0 & 0x02) !== 0;
        this.textMode = (reg1 & 0x10) !== 0;
        // Check bitmap mode bit, not text or multicolor
        if (this.bitmapMode) {
            switch ((reg1 & 0x18) >> 3) {
                case 0:
                    // Bitmap mode
                    this.screenMode = TMS9918A_Mode.BITMAP;
                    break;
                case 1:
                    // Multicolor mode
                    this.screenMode = TMS9918A_Mode.BITMAP_MULTICOLOR;
                    break;
                case 2:
                    // Text mode
                    this.screenMode = TMS9918A_Mode.BITMAP_TEXT;
                    break;
                case 3:
                    // Illegal
                    this.screenMode = TMS9918A_Mode.ILLEGAL;
                    break;
            }
        } else {
            switch ((reg1 & 0x18) >> 3) {
                case 0:
                    // Graphics mode 0
                    this.screenMode = TMS9918A_Mode.GRAPHICS;
                    break;
                case 1:
                    // Multicolor mode
                    this.screenMode = TMS9918A_Mode.MULTICOLOR;
                    break;
                case 2:
                    // Text mode
                    this.screenMode = TMS9918A_Mode.TEXT;
                    break;
                case 3:
                    // Illegal
                    this.screenMode = TMS9918A_Mode.ILLEGAL;
                    break;
            }
        }
        if (this.bitmapMode) {
            this.colorTable = (this.registers[3] & 0x80) << 6;
            this.charPatternTable = (this.registers[4] & 0x4) << 11;
            this.updateTableMasks();
        } else {
            this.colorTable = this.registers[3] << 6;
            this.charPatternTable = (this.registers[4] & 0x7) << 11;
        }
        this.nameTable = (this.registers[2] & 0xf) << 10;
        this.spriteAttributeTable = (this.registers[5] & 0x7f) << 7;
        this.spritePatternTable = (this.registers[6] & 0x7) << 11;
    }

    updateTableMasks() {
        if (this.screenMode === TMS9918A_Mode.BITMAP) {
            this.colorTableMask = ((this.registers[3] & 0x7F) << 6) | 0x3F; // 000CCCCCCC111111
            this.patternTableMask  = ((this.registers[4] & 0x03) << 11) | (this.colorTableMask & 0x7FF); // 000PPCCCCC111111
            // this.log.info("colorTableMask:" + this.colorTableMask);
            // this.log.info("patternTableMask:" + this.patternTableMask);
        }
        else if (this.screenMode === TMS9918A_Mode.BITMAP_TEXT || this.screenMode === TMS9918A_Mode.BITMAP_MULTICOLOR) {
            this.colorTableMask = this.ramMask;
            this.patternTableMask  = ((this.registers[4] & 0x03) << 11) | 0x7FF; // 000PP11111111111
        }
        else {
            this.colorTableMask = this.ramMask;
            this.patternTableMask = this.ramMask;
        }
    }

    writeData(i:number) {
        this.ram[this.addressRegister++] = i;
        this.prefetchByte = i;
        this.addressRegister &= this.ramMask;
        this.latch = false;
        this.redrawRequired = true;
    }

    readStatus() : number {
        var i = this.statusRegister;
        this.statusRegister = 0x1F; // TODO: & 0x1f?
        if (this.interruptsOn) {
            this.cru.setVDPInterrupt(false);
        }
        this.latch = false;
        return i;
    }

    readData() : number {
        var i = this.prefetchByte;
        this.prefetchByte = this.ram[this.addressRegister++];
        this.addressRegister &= this.ramMask;
        this.latch = false;
        return i;
    }

    getRAM() : Uint8Array {
        return this.ram;
    }

    colorTableSize() : number {
        if (this.screenMode === TMS9918A_Mode.GRAPHICS) {
            return 0x20;
        }
        else if (this.screenMode === TMS9918A_Mode.BITMAP) {
            return Math.min(this.colorTableMask + 1, 0x1800);
        }
        else {
            return 0;
        }
    }

    patternTableSize() : number {
        if (this.bitmapMode) {
            return Math.min(this.patternTableMask + 1, 0x1800);
        }
        else {
            return 0x800;
        }
    }
    getDebugTables() {
        var tables : [string,number,number][] = [
            ["Pattern Table", this.charPatternTable, this.patternTableSize()],
            ["Name Table", this.nameTable, 0x300],
            ["Color Table", this.colorTable, this.colorTableSize()],
            ["Sprite Patterns", this.spritePatternTable, 64*32],
            ["Sprite Attributes", this.spriteAttributeTable, 4*32],
        ];
        return tables;
    }
    getRegsString() : string {
        const w = 20;
        var s = "Registers:";
        for (var i = 0; i < this.registers.length; i++) {
            s += " " + hex(this.registers[i],2);
        }
        s += "\n\n";
        var tables = this.getDebugTables();
        for (var row of tables) {
            if (row[2] > 0)
                s += lpad(row[0], w) + ": $" + hex(row[1],4) + " - $" + hex(row[1]+row[2]-1,4) + "\n";
        }
        s += lpad("Address Register",w) + ": $" + hex(this.addressRegister,4) + "\n";
        s += lpad("Status Register",w) + ": $" + hex(this.statusRegister,2) + "\n";
        s += lpad("Screen Mode",w) + ":  " + this.screenMode + "\n";
        s += lpad("Display On",w) + ":  " + this.displayOn + "\n";
        if (this.ramMask != 0x3fff)
            s += lpad("RAM Mask",w) + ": $" + hex(this.ramMask) + "\n";
        return s;
    }

    hexView(start:number, length:number, anchorAddr:number) : {text,lineCount,anchorLine} {
        var text = "";
        var anchorLine = null;
        var addr = start;
        var line = 0;
        for (var i = 0; i < length && addr < 0x4000; addr++, i++) {
            if ((i & 0x000F) === 0) {
                text += "\n" + hex(addr,4) + ":";
                line++;
            }
            text += " ";
            if (anchorAddr && anchorAddr === addr) {
                anchorLine = line;
            }
            var hx = this.ram[addr].toString(16).toUpperCase();
            if (hx.length === 1) {
                text += "0";
            }
            text += hx;
        }
        return {text: text.substr(1), lineCount: line, anchorLine: anchorLine - 1};
    }

    getWord(addr:number) : number {
        return addr < 0x4000 ? this.ram[addr] << 8 | this.ram[addr+1] : 0;
    }

    getCharAt(x:number, y:number) : number {
        x -= 24;
        y -= 24;
        if (!this.textMode) {
            return this.ram[this.nameTable + Math.floor(x / 8) + Math.floor(y / 8)  * 32];
        }
        else {
            return this.ram[this.nameTable + Math.floor((x - 8) / 6) + Math.floor(y / 8)  * 40];
        }
    }

    setFlicker(value:boolean) {
        this.flicker = value;
        this.enableFlicker = value;
    }

    getState() {
        return {
            ram: this.ram.slice(0),
            registers: this.registers.slice(0),
            addressRegister: this.addressRegister,
            statusRegister: this.statusRegister,
            latch: this.latch,
            prefetchByte: this.prefetchByte,
            displayOn: this.displayOn,
            interruptsOn: this.interruptsOn,
            screenMode: this.screenMode,
            bitmapMode: this.bitmapMode,
            textMode: this.textMode,
            colorTable: this.colorTable,
            nameTable: this.nameTable,
            charPatternTable: this.charPatternTable,
            spriteAttributeTable: this.spriteAttributeTable,
            spritePatternTable: this.spritePatternTable,
            colorTableMask: this.colorTableMask,
            patternTableMask: this.patternTableMask,
            ramMask: this.ramMask,
            fgColor: this.fgColor,
            bgColor: this.bgColor,
            flicker: this.flicker
        };
    }

    restoreState(state) {
        this.ram.set(state.ram);
        this.registers.set(state.registers);
        this.addressRegister = state.addressRegister;
        this.statusRegister = state.statusRegister;
        this.latch = state.latch;
        this.prefetchByte = state.prefetchByte;
        this.displayOn = state.displayOn;
        this.interruptsOn = state.interruptsOn;
        this.screenMode = state.screenMode;
        this.bitmapMode = state.bitmapMode;
        this.textMode = state.textMode;
        this.colorTable = state.colorTable;
        this.nameTable = state.nameTable;
        this.charPatternTable = state.charPatternTable;
        this.spriteAttributeTable = state.spriteAttributeTable;
        this.spritePatternTable = state.spritePatternTable;
        this.colorTableMask = state.colorTableMask;
        this.patternTableMask = state.patternTableMask;
        this.ramMask = state.ramMask;
        this.fgColor = state.fgColor;
        this.bgColor = state.bgColor;
        this.flicker = state.flicker;
        this.redrawRequired = true;
    }
};

export class SMSVDP extends TMS9918A {

    writeToCRAM : boolean;
    cram = new Uint8Array(32); // color RAM
    cpalette = new Uint32Array(32); // color RAM (RGBA)
    registers = new Uint8Array(16); // 8 more registers (actually only 5)
    vramUntwiddled = new Uint8Array(0x8000);
    numVisibleLines = 192;
    lineCounter = 0; // TODO: state
    lineInterruptPending = false; // TODO: state

    reset() {
        super.reset();
        this.writeToCRAM = false;
        this.cram.fill(0);
        this.cpalette.fill(0);
        this.vramUntwiddled.fill(0);        
    }
    readStatus() {
        this.lineInterruptPending = false;
        return super.readStatus();
    }
    updateMode(reg0:number, reg1:number) {
        if (reg0 & 0x04) {
            this.screenMode = TMS9918A_Mode.MODE4;
            this.nameTable = ((this.registers[2] & 0xf) << 10) & 0x3800;
            this.spriteAttributeTable = (this.registers[5] & 0x7e) << 7;
        } else {
            super.updateMode(reg0, reg1);
        }
    }
    setReadAddress(i:number) {
        super.setReadAddress(i);
        this.writeToCRAM = false;
    }
    setWriteAddress(i:number) {
        super.setWriteAddress(i);
        this.writeToCRAM = false;
    }
    setVDPWriteRegister(i:number) {
        super.setVDPWriteRegister(i);
        //this.writeToCRAM = false; // TODO?
        this.ramMask = 0x3fff;
    }
    setVDPWriteCommand3(i:number) {
        this.writeToCRAM = true;
        //this.addressRegister &= 0x1f; // TODO?
    }
    writeData(i:number) {
        if (this.writeToCRAM) {
            var palindex = this.addressRegister++ & (this.cram.length-1);
            this.cram[palindex] = i;
            this.cpalette[palindex] = RGBA((i&3)<<6, ((i>>2)&3)<<6, ((i>>4)&3)<<6);
            this.prefetchByte = i;
            this.addressRegister &= this.ramMask;
            this.redrawRequired = true;
        } else {
            var oldAddress = this.addressRegister;
            super.writeData(i);
            this.writeTwiddled(oldAddress, i);
        }
        this.latch = false;
    }
    writeTwiddled(vdp_addr:number, val:number) {
        var planarBase = vdp_addr & 0x3ffc;
        var twiddledBase = planarBase * 2;
        var val0 = this.ram[planarBase];
        var val1 = this.ram[planarBase + 1];
        var val2 = this.ram[planarBase + 2];
        var val3 = this.ram[planarBase + 3];
        for (var i = 0; i < 8; ++i) {
            var effectiveBit = 7 - i;
            var index = (((val0 >>> effectiveBit) & 1))
                | (((val1 >>> effectiveBit) & 1) << 1)
                | (((val2 >>> effectiveBit) & 1) << 2)
                | (((val3 >>> effectiveBit) & 1) << 3);
            this.vramUntwiddled[twiddledBase + i] = index;
        }
    }
    getState() {
        var state = super.getState();
        state['cram'] = this.cram.slice(0);
        return state;
    }
    restoreState(state) {
        super.restoreState(state);
        this.cram.set(state.cram);
    }
    drawScanline(y:number) {
        if (this.screenMode == TMS9918A_Mode.MODE4)
            this.rasterize_line(y);	// special mode 4
        else
            super.drawScanline(y);
    }

    findSprites(line:number) {
        var spriteInfo = this.spriteAttributeTable;
        var active = [];
        var spriteHeight = 8;
        var i;
        if (this.registers[1] & 2) {
            spriteHeight = 16;
        }
        for (i = 0; i < 64; i++) {
            var y = this.ram[spriteInfo + i];
            if (y === 208) {
                break;
            }
            if (y >= 240) y -= 256;
            if (line >= y && line < (y + spriteHeight)) {
                if (active.length === 8) {
                    this.statusRegister |= 0x40;  // Sprite overflow
                    break;
                }
                active.push([
                    this.ram[spriteInfo + 128 + i * 2],
                    this.ram[spriteInfo + 128 + i * 2 + 1],
                    y]);
            }
        }
        return active;
    }


    rasterize_background(lineAddr:number, pixelOffset:number, tileData:number, tileDef:number, transparent:boolean) {
        lineAddr = lineAddr | 0;
        pixelOffset = pixelOffset | 0;
        tileData = tileData | 0;
        tileDef = (tileDef | 0) * 2;
        var i, tileDefInc;
        if ((tileData & (1 << 9))) {
            tileDefInc = -1;
            tileDef += 7;
        } else {
            tileDefInc = 1;
        }
        const paletteOffset = (tileData & (1 << 11)) ? 16 : 0;
        var index;
        if (transparent && paletteOffset === 0) {
            for (i = 0; i < 8; i++) {
                index = this.vramUntwiddled[tileDef];
                tileDef += tileDefInc;
                if (index !== 0) this.fb32[lineAddr + pixelOffset] = this.cpalette[index];
                pixelOffset = (pixelOffset + 1) & 255;
            }
        } else {
            for (i = 0; i < 8; i++) {
                index = this.vramUntwiddled[tileDef] + paletteOffset;
                tileDef += tileDefInc;
                this.fb32[lineAddr + pixelOffset] = this.cpalette[index];
                pixelOffset = (pixelOffset + 1) & 255;
            }
        }
    }

    
    clear_background(lineAddr:number, pixelOffset:number) {
        lineAddr = lineAddr | 0;
        pixelOffset = pixelOffset | 0;
        var i;
        const rgb = this.cpalette[0];
        for (i = 0; i < 8; ++i) {
            this.fb32[lineAddr + pixelOffset] = rgb;
            pixelOffset = (pixelOffset + 1) & 255;
        }
    }

    rasterize_background_line(lineAddr:number, pixelOffset:number, nameAddr:number, yMod:number) {
        lineAddr = lineAddr | 0;
        pixelOffset = pixelOffset | 0;
        nameAddr = nameAddr | 0;
        const yOffset = (yMod | 0) * 4;
        for (var i = 0; i < 32; i++) {
            // TODO: static left-hand rows.
            var tileData = this.ram[nameAddr + i * 2] | (this.ram[nameAddr + i * 2 + 1] << 8);
            var tileNum = tileData & 511;
            var tileDef = 32 * tileNum;
            if (tileData & (1 << 10)) {
                tileDef += 28 - yOffset;
            } else {
                tileDef += yOffset;
            }
            if ((tileData & (1 << 12)) === 0) {
                this.rasterize_background(lineAddr, pixelOffset, tileData, tileDef, false);
            } else {
                this.clear_background(lineAddr, pixelOffset);
            }
            pixelOffset = (pixelOffset + 8) & 255;
        }
    }

    rasterize_foreground_line(lineAddr:number, pixelOffset:number, nameAddr:number, yMod:number) {
        lineAddr = lineAddr | 0;
        pixelOffset = pixelOffset | 0;
        nameAddr = nameAddr | 0;
        const yOffset = (yMod | 0) * 4;
        for (var i = 0; i < 32; i++) {
            // TODO: static left-hand rows.
            var tileData = this.ram[nameAddr + i * 2] | (this.ram[nameAddr + i * 2 + 1] << 8);
            if ((tileData & (1 << 12)) === 0) continue;
            var tileNum = tileData & 511;
            var tileDef = 32 * tileNum;
            if (tileData & (1 << 10)) {
                tileDef += 28 - yOffset;
            } else {
                tileDef += yOffset;
            }
            this.rasterize_background(lineAddr, ((i * 8) + pixelOffset & 0xff), tileData, tileDef, true);
        }
    }


    rasterize_sprites(line:number, lineAddr:number, pixelOffset:number, sprites) {
        lineAddr = lineAddr | 0;
        pixelOffset = pixelOffset | 0;
        const spriteBase = (this.registers[6] & 4) ? 0x2000 : 0;
        // TODO: sprite X-8 shift
        // TODO: sprite double size
        for (var i = 0; i < 256; ++i) {
            var xPos = i;//(i + this.registers[8]) & 0xff;
            var spriteFoundThisX = false;
            var writtenTo = false;
            var minDistToNext = 256;
            for (var k = 0; k < sprites.length; k++) {
                var sprite = sprites[k];
                var offset = xPos - sprite[0];
                // Sprite to the right of the current X?
                if (offset < 0) {
                    // Find out how far it would be to skip to this sprite
                    var distToSprite = -offset;
                    // Keep the minimum distance to the next sprite to the right.
                    if (distToSprite < minDistToNext) minDistToNext = distToSprite;
                    continue;
                }
                if (offset >= 8) continue;
                spriteFoundThisX = true;
                var spriteLine = line - sprite[2];
                var spriteAddr = spriteBase + sprite[1] * 32 + spriteLine * 4;
                var untwiddledAddr = spriteAddr * 2 + offset;
                var index = this.vramUntwiddled[untwiddledAddr];
                if (index === 0) {
                    continue;
                }
                if (writtenTo) {
                    // We have a collision!.
                    this.statusRegister |= 0x20;
                    break;
                }
                this.fb32[lineAddr + ((pixelOffset + i - this.registers[8]) & 0xff)] = this.cpalette[16 + index];
                writtenTo = true;
            }
            if (!spriteFoundThisX && minDistToNext > 1) {
                // If we didn't find a sprite on this X, then we can skip ahead by the minimum
                // dist to next (minus one to account for loop add)
                i += minDistToNext - 1;
            }
        }
    }

    border_clear(lineAddr:number, count:number) {
        lineAddr = lineAddr | 0;
        count = count | 0;
        const borderIndex = 16 + (this.registers[7] & 0xf);
        const borderRGB = this.cpalette[borderIndex];
        this.fb32.fill(borderRGB, lineAddr, lineAddr+count);
    }

    rasterize_line(line:number) {
        line |= 0;
        var vdp_regs = this.registers;
        var drawWidth = 256;
        var drawHeight = this.numVisibleLines; // TODO?
        var hBorder = (this.width - drawWidth) >> 1;
        var vBorder = (this.height - drawHeight) >> 1; // TODO?
        const startAddr = ((line + vBorder) * this.width) | 0;
        const lineAddr = (startAddr + hBorder) | 0;
        if (!this.displayOn || line < 0 || line >= drawHeight) {
            if (line < this.height)
                this.border_clear(startAddr, this.width);
            else if (line >= 262-vBorder)
                this.border_clear((line-262+vBorder)*this.width, this.width);
        } else {
            var effectiveLine = line + vdp_regs[9];
            if (effectiveLine >= 224) {
                effectiveLine -= 224;
            }
            const sprites = this.findSprites(line);
            const pixelOffset = ((vdp_regs[0] & 64) && line < 16) ? 0 : vdp_regs[8];
            const nameAddr = this.nameTable + (effectiveLine >>> 3) * 64;
            const yMod = effectiveLine & 7;

            this.rasterize_background_line(lineAddr, pixelOffset, nameAddr, yMod);
            this.rasterize_sprites(line, lineAddr, pixelOffset, sprites);
            this.rasterize_foreground_line(lineAddr, pixelOffset, nameAddr, yMod);

            this.border_clear(startAddr, hBorder);
            this.border_clear(lineAddr + 256, hBorder);
            if (vdp_regs[0] & (1 << 5)) {
                // Blank out left hand column.
                this.border_clear(lineAddr, 8);
            }
        }
        // frame interrupts
        if (line == drawHeight) {
            this.statusRegister |= 0x80;
            if (this.interruptsOn) {
                this.cru.setVDPInterrupt(true);
            }
        }
        // line interrupts
        if (line <= drawHeight) {
            if (this.lineCounter > 0) {
                this.lineCounter--;
            } else {
                this.lineCounter = this.registers[0xa];
                this.lineInterruptPending = true;
            }
        } else {
            this.lineCounter = this.registers[0xa];
        }
        if (this.lineInterruptPending) {
            if (this.registers[0] & 0x10) {
                // TODO this.cru.setVDPInterrupt(true);
            }
        }
    }

    getDebugTables() {
        if (this.screenMode == TMS9918A_Mode.MODE4) {
            var tables : [string,number,number][] = [
                ["Pattern Table", 0, 512*32],
                ["Name Table", this.nameTable, 32*32*2], // TODO: size
                ["Sprite Attributes", this.spriteAttributeTable, 256],
            ]
            return tables;
        } else {
            return super.getDebugTables();
        }
    }
};

