namespace pxsim {

    function OFF(x: number, y: number) {
        return x + y * visuals.SCREEN_WIDTH
    }


    export class EV3ScreenState {
        changed: boolean = true;
        points: Uint8Array;
        constructor() {
            this.points = new Uint8Array(visuals.SCREEN_WIDTH * visuals.SCREEN_HEIGHT)
        }

        applyMode(off: number, v: number) {
            if (v & Draw.Clear)
                this.points[off] = 0;
            else if (v & Draw.Xor)
                this.points[off] = this.points[off] ? 0 : 255;
            else
                this.points[off] = 255;
        }

        setPixel(x: number, y: number, v: number) {
            this.applyMode(OFF(x, y), v)
            this.changed = true;
        }

        clear() {
            for (let i = 0; i < this.points.length; ++i)
                this.points[i] = 0;
            this.changed = true;
        }

        blitLineCore(x: number, y: number, w: number, buf: RefBuffer, mode: Draw, offset = 0) {
            if (y < 0 || y >= visuals.SCREEN_HEIGHT)
                return;
            if (x + w <= 0)
                return;
            if (x >= visuals.SCREEN_WIDTH)
                return;

            let off = OFF(x, y);
            const off0 = OFF(0, y);
            const off1 = OFF(visuals.SCREEN_WIDTH - 1, y);
            let mask = 0x01
            let dp = offset

            for (let i = 0; i < w; ++i) {
                if ((buf.data[dp] & mask) && off0 <= off && off <= off1) {
                    this.applyMode(off, mode);
                }
                off++
                mask <<= 1
                if (mask & 0x100) {
                    mask = 0x01
                    dp++
                }
            }

            this.changed = true;
        }

        clearLine(x: number, y: number, w: number) {
            let off = OFF(x, y);
            const off0 = OFF(0, y);
            const off1 = OFF(visuals.SCREEN_WIDTH - 1, y);
            for (let i = 0; i < w; ++i) {
                if (off0 <= off && off <= off1) {
                    this.points[off] = 0
                }
                off++
            }
        }

        didChange() {
            const res = this.changed;
            this.changed = false;
            return res;
        }
    }
}


namespace pxsim.screen {
    function XX(v: number) { return (v << 16) >> 16 }
    function YY(v: number) { return v >> 16 }

    export function _setPixel(x: number, y: number, mode: Draw) {
        const screenState = ev3board().screenState;
        screenState.setPixel(x, y, mode);
    }

    export function _blitLine(xw: number, y: number, buf: RefBuffer, mode: Draw) {
        const screenState = ev3board().screenState;
        screenState.blitLineCore(XX(xw), y, YY(xw), buf, mode)
    }

    export function isValidImage(buf: RefBuffer) {
        return buf.data.length >= 3 && buf.data[0] == 0xf0;
    }

    export function PIX2BYTES(x: number) {
        return ((x + 7) >> 3)
    }
    export function clear(): void {
        const screenState = ev3board().screenState;
        screenState.clear()
    }

    export function dump() {
        // No need for this one.
    }

    export function imageOf(buf: RefBuffer) {
        return incr(buf)
    }
}

namespace pxsim.screen {
    function DMESG(msg: string) {
        control.dmesg(msg)
    }
    const NULL: RefBuffer = null;
    function revbits(v: number) {
        v = (v & 0xf0) >> 4 | (v & 0x0f) << 4;
        v = (v & 0xcc) >> 2 | (v & 0x33) << 2;
        v = (v & 0xaa) >> 1 | (v & 0x55) << 1;
        return v;
    }

    export function unpackPNG(png: RefBuffer) {
        function memcmp(off: number, mark: string) {
            for (let i = 0; i < mark.length; ++i) {
                if (mark.charCodeAt(i) != png.data[off + i])
                    return 1
            }
            return 0
        }
        function readInt(off: number) {
            return ((png.data[off] << 24) | (png.data[off + 1] << 16) |
                (png.data[off + 2] << 8) | (png.data[off + 3])) >>> 0
        }
        if (!png) {
            DMESG("PNG: Missing image");
            return NULL;
        }
        if (png.data.length < 45) {
            DMESG("PNG: File too small");
            return NULL;
        }

        if (memcmp(0, "\x89PNG\r\n\x1A\n") != 0) {
            DMESG("PNG: Invalid header");
            return NULL;
        }

        if (memcmp(12, "IHDR") != 0) {
            DMESG("PNG: missing IHDR");
            return NULL;
        }

        const lenIHDR = readInt(8);
        const width = readInt(16);
        const height = readInt(20);
        const lenIDAT = readInt(33);
        const sizeOfHD = 41;

        if (lenIHDR != 13) {
            DMESG("PNG: bad IHDR len");
            return NULL;
        }
        if (memcmp(24, "\x01\x00\x00\x00\x00") != 0) {
            DMESG("PNG: not 1-bit grayscale");
            return NULL;
        }
        if (memcmp(37, "IDAT") != 0) {
            DMESG("PNG: missing IDAT");
            return NULL;
        }
        if (lenIDAT + sizeOfHD >= png.data.length) {
            DMESG("PNG: buffer too short");
            return NULL;
        }
        if (width > 300 || height > 300) {
            DMESG("PNG: too big");
            return NULL;
        }

        const byteW = (width + 7) >> 3;
        const sz = (byteW + 1) * height;
        const tmp = new Uint8Array(sz + 1);
        // uncompress doesn't take the zlib header, hence + 2
        const two = tinf.uncompress(png.data.slice(sizeOfHD + 2, sizeOfHD + lenIDAT), tmp);
        if (two.length != sz) {
            DMESG("PNG: invalid compressed size");
            return NULL;
        }

        const res = output.createBuffer(2 + byteW * height);
        res.data[0] = 0xf0;
        res.data[1] = width;
        let dst = 2
        let src = 0
        let lastMask = (1 << (width & 7)) - 1;
        if (lastMask == 0)
            lastMask = 0xff;
        for (let i = 0; i < height; ++i) {
            if (two[src++] != 0) {
                DMESG("PNG: unsupported filter");
                decr(res);
                return NULL;
            }
            for (let j = 0; j < byteW; ++j) {
                res.data[dst] = ~revbits(two[src++]);
                if (j == byteW - 1) {
                    res.data[dst] &= lastMask;
                }
                dst++;
            }
        }
        return res;
    }
}

namespace pxsim.ImageMethods {
    const bitdouble = [
        0x00, 0x03, 0x0c, 0x0f, 0x30, 0x33, 0x3c, 0x3f, 0xc0, 0xc3, 0xcc, 0xcf, 0xf0, 0xf3, 0xfc, 0xff,
    ]

    export function buffer(buf: RefBuffer) {
        return incr(buf)
    }

    export function width(buf: RefBuffer) {
        if (!screen.isValidImage(buf)) return 0
        return buf.data[1]
    }

    export function height(buf: RefBuffer) {
        if (!screen.isValidImage(buf)) return 0
        const bw = screen.PIX2BYTES(buf.data[1]);
        const h = ((buf.data.length - 2) / bw) | 0;
        return h
    }

    export function draw(buf: RefBuffer, x: number, y: number, mode: Draw): void {
        const screenState = ev3board().screenState;

        if (!screen.isValidImage(buf))
            return;

        if (mode & (Draw.Double | Draw.Quad)) {
            buf = doubled(buf);
            if (mode & Draw.Quad) {
                let pbuf = buf;
                buf = doubled(buf);
                decr(pbuf);
            }
        }

        let pixwidth = buf.data[1];
        let ptr = 2;
        const bytewidth = screen.PIX2BYTES(pixwidth);
        pixwidth = Math.min(pixwidth, visuals.SCREEN_WIDTH);
        while (ptr + bytewidth <= buf.data.length) {
            if (mode & (Draw.Clear | Draw.Xor | Draw.Transparent)) {
                // no erase of background
            } else {
                screenState.clearLine(x, y, pixwidth)
            }
            screenState.blitLineCore(x, y, pixwidth, buf, mode, ptr);
            y++;
            ptr += bytewidth;
        }

        if (mode & (Draw.Double | Draw.Quad))
            decr(buf);
    }

    export function doubled(buf: RefBuffer): RefBuffer {
        if (!screen.isValidImage(buf))
            return null;
        const w = buf.data[1];
        if (w > 126)
            return null;
        const bw = screen.PIX2BYTES(w);
        const h = ((buf.data.length - 2) / bw) | 0;
        const bw2 = screen.PIX2BYTES(w * 2);
        const out = pins.createBuffer(2 + bw2 * h * 2)
        out.data[0] = 0xf0;
        out.data[1] = w * 2;
        let src = 2
        let dst = 2
        for (let i = 0; i < h; ++i) {
            for (let jj = 0; jj < 2; ++jj) {
                let p = src;
                for (let j = 0; j < bw; ++j) {
                    const v = buf.data[p++]
                    out.data[dst++] = bitdouble[v & 0xf];
                    out.data[dst++] = bitdouble[v >> 4];
                }
            }
            src += bw;
        }
        return out;
    }


}