
import { SourceLocation } from "../workertypes";
import { DataValue, ECSError } from "./ecs";

export interface DecoderResult {
    properties: {[name: string] : DataValue}
}

abstract class LineDecoder {
    curline: number = 0; // for debugging, zero-indexed
    lines : string[][]; // array of token arrays

    constructor(
        text: string
    ) {
        // split the text into lines and into tokens
        this.lines = text.split('\n').map(s => s.trim()).filter(s => !!s).map(s => s.split(/\s+/));
    }

    decodeBits(s: string, n: number, msbfirst: boolean) {
        if (s.length != n) throw new ECSError(`Expected ${n} characters`);
        let b = 0;
        for (let i=0; i<n; i++) {
            let bit;
            let ch = s.charAt(i);
            if (ch == 'x' || ch == 'X' || ch == '1') bit = 1;
            else if (ch == '.' || ch == '0') bit = 0;
            else throw new ECSError('need x or . (or 0 or 1)');
            if (bit) {
                if (msbfirst) b |= 1 << (n-1-i);
                else b |= 1 << i;
            }
        }
        return b;
    }

    assertTokens(toks: string[], count: number) {
        if (toks.length != count) throw new ECSError(`Expected ${count} tokens on line.`);
    }

    hex(s: string) {
        let v = parseInt(s, 16);
        if (isNaN(v)) throw new ECSError(`Invalid hex value: ${s}`)
        return v;
    }

    getErrorLocation($loc: SourceLocation): SourceLocation {
        // TODO: blank lines mess this up
        $loc.line += this.curline + 1;
        return $loc;
    }

    abstract parse() : DecoderResult;
}

export class VCSSpriteDecoder extends LineDecoder {
    parse() {
        let height = this.lines.length;
        let bitmapdata = new Uint8Array(height);
        let colormapdata = new Uint8Array(height);
        for (let i=0; i<height; i++) {
            this.curline = height - 1 - i;
            let toks = this.lines[this.curline];
            this.assertTokens(toks, 2);
            bitmapdata[i] = this.decodeBits(toks[0], 8, true);
            colormapdata[i] = this.hex(toks[1]);
        }
        return {
            properties: {
                bitmapdata, colormapdata, height: height-1
            }
        }
    }
}

export class VCSBitmapDecoder extends LineDecoder {
    parse() {
        let height = this.lines.length;
        let bitmapdata = new Uint8Array(height);
        for (let i=0; i<height; i++) {
            this.curline = height - 1 - i;
            let toks = this.lines[this.curline];
            this.assertTokens(toks, 1);
            bitmapdata[i] = this.decodeBits(toks[0], 8, true);
        }
        return {
            properties: {
                bitmapdata, height: height-1
            }
        }
    }
}

export class VCSPlayfieldDecoder extends LineDecoder {
    parse() {
        let height = this.lines.length;
        let pf = new Uint32Array(height);
        for (let i=0; i<height; i++) {
            this.curline = height - 1 - i;
            let toks = this.lines[this.curline];
            this.assertTokens(toks, 1);
            let pf0 = this.decodeBits(toks[0].substring(0,4), 4, false) << 4;
            let pf1 = this.decodeBits(toks[0].substring(4,12), 8, true);
            let pf2 = this.decodeBits(toks[0].substring(12,20), 8, false);
            pf[i] = (pf0 << 0) | (pf1 << 8) | (pf2 << 16);
        }
        return {
            properties: {
                pf
            }
        }
    }
}


export class VCSVersatilePlayfieldDecoder extends LineDecoder {
    parse() {
        let height = this.lines.length;
        let data = new Uint8Array(height * 2);
        data.fill(0x3f);
        // pf0 pf1 pf2 colupf colubk ctrlpf trash
        const regs = [0x0d, 0x0e, 0x0f, 0x08, 0x09, 0x0a, 0x3f];
        let prev = [0,0,0,0,0,0,0];
        let cur  = [0,0,0,0,0,0,0];
        for (let i=0; i<height; i++) {
            let dataofs = height*2 - i*2;
            this.curline = i;
            let toks = this.lines[this.curline];
            if (toks.length == 2) {
                data[dataofs - 1] = this.hex(toks[0]);
                data[dataofs - 2] = this.hex(toks[1]);
                continue;
            }
            this.assertTokens(toks, 4);
            cur[0] = this.decodeBits(toks[0].substring(0,4), 4, false) << 4;
            cur[1] = this.decodeBits(toks[0].substring(4,12), 8, true);
            cur[2] = this.decodeBits(toks[0].substring(12,20), 8, false);
            if (toks[1] != '..') cur[3] = this.hex(toks[1]);
            if (toks[2] != '..') cur[4] = this.hex(toks[2]);
            if (toks[3] != '..') cur[5] = this.hex(toks[3]);
            let changed = [];
            for (let j=0; j<cur.length; j++) {
                if (cur[j] != prev[j])
                    changed.push(j);
            }
            if (changed.length > 1) {
                console.log(changed, cur, prev);
                throw new ECSError(`More than one register change in line ${i+1}: [${changed}]`);
            }
            let chgidx = changed.length ? changed[0] : regs.length-1;
            data[dataofs - 1] = regs[chgidx];
            data[dataofs - 2] = cur[chgidx];
            prev[chgidx] = cur[chgidx];
        }
        return {
            properties: {
                data
            }
        }
    }
}

export class VCSBitmap48Decoder extends LineDecoder {
    parse() {
        let height = this.lines.length;
        let bitmap0 = new Uint8Array(height);
        let bitmap1 = new Uint8Array(height);
        let bitmap2 = new Uint8Array(height);
        let bitmap3 = new Uint8Array(height);
        let bitmap4 = new Uint8Array(height);
        let bitmap5 = new Uint8Array(height);
        for (let i=0; i<height; i++) {
            this.curline = height - 1 - i;
            let toks = this.lines[this.curline];
            this.assertTokens(toks, 1);
            bitmap0[i] = this.decodeBits(toks[0].slice(0,8), 8, true);
            bitmap1[i] = this.decodeBits(toks[0].slice(8,16), 8, true);
            bitmap2[i] = this.decodeBits(toks[0].slice(16,24), 8, true);
            bitmap3[i] = this.decodeBits(toks[0].slice(24,32), 8, true);
            bitmap4[i] = this.decodeBits(toks[0].slice(32,40), 8, true);
            bitmap5[i] = this.decodeBits(toks[0].slice(40,48), 8, true);
        }
        return {
            properties: {
                bitmap0, bitmap1, bitmap2, bitmap3, bitmap4, bitmap5,
                height: height-1
            }
        }
    }
}

export function newDecoder(name: string, text: string) : LineDecoder | undefined {
    let cons = (DECODERS as any)[name];
    if (cons) return new cons(text);
}

const DECODERS = {
    'vcs_sprite': VCSSpriteDecoder,
    'vcs_bitmap': VCSBitmapDecoder,
    'vcs_playfield': VCSPlayfieldDecoder,
    'vcs_versatile': VCSVersatilePlayfieldDecoder,
    'vcs_bitmap48': VCSBitmap48Decoder,
}
