import ApplesoftCompiler from 'js/applesoft/compiler';
import RAM from 'js/ram';
import { Memory } from 'js/types';

// Zero page locations used by Applesoft. The names come from
// the commented decompilation produced by the Merlin Pro
// assembler, revision 4/27/84. There is evidence from
// https://www.pagetable.com/?p=774 that the original Microsoft
// BASIC source code used these names as well.
const TXTTAB = 0x67; // start of program, word
const VARTAB = 0x69; // start of variables, word
const ARYTAB = 0x6B; // start of arrays, word
const STREND = 0x6D; // end of strings, word
const PRGEND = 0xAF; // end of program, word

function compileToMemory(ram: Memory, program: string) {
    ApplesoftCompiler.compileToMemory(ram, program);
}

// Manual decompilation based on "Applesoft Internal Structure"
// by C.K. Mesztenyi/Washington Apple Pi, from Call—A.P.P.L.E.,
// January, 1982. Archived at:
// https://archive.org/details/DTCA2DOC-045_applesoft_internal
// Decompilation verified on the emulator by typing in the
// program, then:
//    ]CALL -151
//    *800.820
// and comparing the resulting bytes (starting at 801).

describe('ApplesoftCompiler', () => {
    it('compiles a one-line hello world', () => {
        const compiler = new ApplesoftCompiler();
        compiler.compile('10 PRINT "HELLO, WORLD!"');
        expect(compiler.program()).toEqual(new Uint8Array([
            0x16, 0x08, 0x0a, 0x00, 0xba, 0x22, 0x48, 0x45,
            0x4c, 0x4c, 0x4f, 0x2c, 0x20, 0x57, 0x4f, 0x52,
            0x4c, 0x44, 0x21, 0x22, 0x00, 0x00, 0x00
        ]));
    });

    it('compiles a one-line hello world into memory', () => {
        const ram = new RAM(0, 0xff); // 64K of RAM

        compileToMemory(ram, '10 PRINT "HELLO, WORLD!"');
        expect(ram.read(0x08, 0x01)).toBe(0x16); // pointer to next line low
        expect(ram.read(0x08, 0x02)).toBe(0x08); // pointer to next line high
        expect(ram.read(0x08, 0x03)).toBe(10);   // line number low
        expect(ram.read(0x08, 0x04)).toBe(0);    // line number high
        expect(ram.read(0x08, 0x05)).toBe(0xba); // PRINT
        expect(ram.read(0x08, 0x06)).toBe(0x22); // "
        expect(ram.read(0x08, 0x07)).toBe(0x48); // H
        expect(ram.read(0x08, 0x08)).toBe(0x45); // E
        expect(ram.read(0x08, 0x09)).toBe(0x4C); // L
        expect(ram.read(0x08, 0x0a)).toBe(0x4C); // L
        expect(ram.read(0x08, 0x0b)).toBe(0x4F); // O
        expect(ram.read(0x08, 0x0c)).toBe(0x2C); // ,
        expect(ram.read(0x08, 0x0d)).toBe(0x20); // space
        expect(ram.read(0x08, 0x0e)).toBe(0x57); // W
        expect(ram.read(0x08, 0x0f)).toBe(0x4F); // O
        expect(ram.read(0x08, 0x10)).toBe(0x52); // R
        expect(ram.read(0x08, 0x11)).toBe(0x4C); // L
        expect(ram.read(0x08, 0x12)).toBe(0x44); // D
        expect(ram.read(0x08, 0x13)).toBe(0x21); // !
        expect(ram.read(0x08, 0x14)).toBe(0x22); // "
        expect(ram.read(0x08, 0x15)).toBe(0x00); // end of line
        expect(ram.read(0x08, 0x16)).toBe(0x00); // end of program low
        expect(ram.read(0x08, 0x17)).toBe(0x00); // end of program high

        expect(ram.read(0x00, TXTTAB)).toBe(0x01);   // start of program low
        expect(ram.read(0x00, TXTTAB + 1)).toBe(0x08); // start of program high
        expect(ram.read(0x00, VARTAB)).toBe(0x19);   // start of variables low
        expect(ram.read(0x00, VARTAB + 1)).toBe(0x08); // start of variables high
        expect(ram.read(0x00, ARYTAB)).toBe(0x19);   // start of arrays low
        expect(ram.read(0x00, ARYTAB + 1)).toBe(0x08); // start of arrays high
        expect(ram.read(0x00, STREND)).toBe(0x19);   // end of strings low
        expect(ram.read(0x00, STREND + 1)).toBe(0x08); // end of strings high
        expect(ram.read(0x00, PRGEND)).toBe(0x19);   // end of program low
        expect(ram.read(0x00, PRGEND + 1)).toBe(0x08); // end of program high
    });

    it('uppercases normal-mode text, like variables', () => {
        const ram = new RAM(0, 0xff); // 64K of RAM

        compileToMemory(ram, '10 fori=xtoz');
        expect(ram.read(0x08, 0x03)).toBe(10);   // line number low
        expect(ram.read(0x08, 0x04)).toBe(0);    // line number high
        expect(ram.read(0x08, 0x05)).toBe(0x81); // FOR
        expect(ram.read(0x08, 0x06)).toBe(0x49); // I
        expect(ram.read(0x08, 0x07)).toBe(0xd0); // = (token)
        expect(ram.read(0x08, 0x08)).toBe(0x58); // X
        expect(ram.read(0x08, 0x09)).toBe(0xc1); // TO
        expect(ram.read(0x08, 0x0a)).toBe(0x5a); // Z
        expect(ram.read(0x08, 0x0b)).toBe(0x00); // end of line
    });

    it('allows lower-case characters in strings', () => {
        const ram = new RAM(0, 0xff); // 64K of RAM

        compileToMemory(ram, '10 PRINT "Hello!"');
        expect(ram.read(0x08, 0x03)).toBe(10);   // line number low
        expect(ram.read(0x08, 0x04)).toBe(0);    // line number high
        expect(ram.read(0x08, 0x05)).toBe(0xba); // PRINT
        expect(ram.read(0x08, 0x06)).toBe(0x22); // "
        expect(ram.read(0x08, 0x07)).toBe(0x48); // H
        expect(ram.read(0x08, 0x08)).toBe(0x65); // e
        expect(ram.read(0x08, 0x09)).toBe(0x6C); // l
        expect(ram.read(0x08, 0x0a)).toBe(0x6C); // l
        expect(ram.read(0x08, 0x0b)).toBe(0x6F); // o
    });

    it('allows lower-case characters in comments', () => {
        const ram = new RAM(0, 0xff); // 64K of RAM

        compileToMemory(ram, '10 REM Hello!');
        expect(ram.read(0x08, 0x03)).toBe(10);   // line number low
        expect(ram.read(0x08, 0x04)).toBe(0);    // line number high
        expect(ram.read(0x08, 0x05)).toBe(0xb2); // REM
        expect(ram.read(0x08, 0x06)).toBe(0x20); // space
        expect(ram.read(0x08, 0x07)).toBe(0x48); // H
        expect(ram.read(0x08, 0x08)).toBe(0x65); // e
        expect(ram.read(0x08, 0x09)).toBe(0x6C); // l
        expect(ram.read(0x08, 0x0a)).toBe(0x6C); // l
        expect(ram.read(0x08, 0x0b)).toBe(0x6F); // o
    });

    it('allows lower-case tokens', () => {
        const ram = new RAM(0, 0xff); // 64K of RAM

        compileToMemory(ram, '10 print "Hello!"');
        expect(ram.read(0x08, 0x03)).toBe(10);   // line number low
        expect(ram.read(0x08, 0x04)).toBe(0);    // line number high
        expect(ram.read(0x08, 0x05)).toBe(0xba); // PRINT
    });

    it('accepts out-of-order lines', () => {
        const ram = new RAM(0, 0xff); // 64K of RAM

        compileToMemory(ram, '20 GOTO 10\n10 PRINT "HELLO');
        expect(ram.read(0x08, 0x01)).toBe(0x0d); // pointer to next line low
        expect(ram.read(0x08, 0x02)).toBe(0x08); // pointer to next line high
        expect(ram.read(0x08, 0x03)).toBe(10);   // line number low
        expect(ram.read(0x08, 0x04)).toBe(0);    // line number high
        expect(ram.read(0x08, 0x05)).toBe(0xba); // PRINT
        expect(ram.read(0x08, 0x06)).toBe(0x22); // "
        expect(ram.read(0x08, 0x07)).toBe(0x48); // H
        expect(ram.read(0x08, 0x08)).toBe(0x45); // E
        expect(ram.read(0x08, 0x09)).toBe(0x4C); // L
        expect(ram.read(0x08, 0x0a)).toBe(0x4C); // L
        expect(ram.read(0x08, 0x0b)).toBe(0x4F); // O
        expect(ram.read(0x08, 0x0c)).toBe(0x00); // end of line
        expect(ram.read(0x08, 0x0d)).toBe(0x15); // pointer to next line low
        expect(ram.read(0x08, 0x0e)).toBe(0x08); // pointer to next line high
        expect(ram.read(0x08, 0x0f)).toBe(0x14); // line number low
        expect(ram.read(0x08, 0x10)).toBe(0x00); // line number high
        expect(ram.read(0x08, 0x11)).toBe(0xab); // GOTO
        expect(ram.read(0x08, 0x12)).toBe(0x31); // 1
        expect(ram.read(0x08, 0x13)).toBe(0x30); // 0
        expect(ram.read(0x08, 0x14)).toBe(0x00); // end of line
        expect(ram.read(0x08, 0x15)).toBe(0x00); // end of program low
        expect(ram.read(0x08, 0x16)).toBe(0x00); // end of program high
    });

    it('prefers ATN to AT', () => {
        const ram = new RAM(0, 0xff); // 64K of RAM

        compileToMemory(ram, '10 X = ATN(20)');
        expect(ram.read(0x08, 0x03)).toBe(10);   // line number low
        expect(ram.read(0x08, 0x04)).toBe(0);    // line number high
        expect(ram.read(0x08, 0x05)).toBe(0x58); // X
        expect(ram.read(0x08, 0x06)).toBe(0xd0); // = (token)
        expect(ram.read(0x08, 0x07)).toBe(0xe1); // ATN
        expect(ram.read(0x08, 0x08)).toBe(0x28); // (
        expect(ram.read(0x08, 0x09)).toBe(0x32); // 2
        expect(ram.read(0x08, 0x0a)).toBe(0x30); // 0
        expect(ram.read(0x08, 0x0b)).toBe(0x29); // )
        expect(ram.read(0x08, 0x0c)).toBe(0x00); // end of line
    });

    it('prefers TO to AT', () => {
        const ram = new RAM(0, 0xff); // 64K of RAM

        compileToMemory(ram, '10 FORI=ATOZ');
        expect(ram.read(0x08, 0x03)).toBe(10);   // line number low
        expect(ram.read(0x08, 0x04)).toBe(0);    // line number high
        expect(ram.read(0x08, 0x05)).toBe(0x81); // FOR
        expect(ram.read(0x08, 0x06)).toBe(0x49); // I
        expect(ram.read(0x08, 0x07)).toBe(0xd0); // = (token)
        expect(ram.read(0x08, 0x08)).toBe(0x41); // A
        expect(ram.read(0x08, 0x09)).toBe(0xc1); // TO
        expect(ram.read(0x08, 0x0a)).toBe(0x5a); // Z
        expect(ram.read(0x08, 0x0b)).toBe(0x00); // end of line
    });

    it('parses DATA statements that start with space', () => {
        const ram = new RAM(0, 0xff); // 64K of RAM

        compileToMemory(ram, '10 DATA 1,2,3');
        expect(ram.read(0x08, 0x03)).toBe(10);   // line number low
        expect(ram.read(0x08, 0x04)).toBe(0);    // line number high
        expect(ram.read(0x08, 0x05)).toBe(0x83); // DATA
        expect(ram.read(0x08, 0x06)).toBe(0x20); // space
        expect(ram.read(0x08, 0x07)).toBe(0x31); // 1
        expect(ram.read(0x08, 0x08)).toBe(0x2c); // ,
        expect(ram.read(0x08, 0x09)).toBe(0x32); // 2
        expect(ram.read(0x08, 0x0a)).toBe(0x2c); // ,
        expect(ram.read(0x08, 0x0b)).toBe(0x33); // 3
        expect(ram.read(0x08, 0x0c)).toBe(0x00); // end of line
    });

    it('parses DATA statements with numbers', () => {
        const ram = new RAM(0, 0xff); // 64K of RAM

        compileToMemory(ram, '10 DATA1,2,3');
        expect(ram.read(0x08, 0x03)).toBe(10);   // line number low
        expect(ram.read(0x08, 0x04)).toBe(0);    // line number high
        expect(ram.read(0x08, 0x05)).toBe(0x83); // DATA
        expect(ram.read(0x08, 0x06)).toBe(0x31); // 1
        expect(ram.read(0x08, 0x07)).toBe(0x2c); // ,
        expect(ram.read(0x08, 0x08)).toBe(0x32); // 2
        expect(ram.read(0x08, 0x09)).toBe(0x2c); // ,
        expect(ram.read(0x08, 0x0a)).toBe(0x33); // 3
        expect(ram.read(0x08, 0x0b)).toBe(0x00); // end of line
    });

    it('parses DATA statements with strings including lower-case', () => {
        const ram = new RAM(0, 0xff); // 64K of RAM

        compileToMemory(ram, '10 DATA"abc"');
        expect(ram.read(0x08, 0x03)).toBe(10);   // line number low
        expect(ram.read(0x08, 0x04)).toBe(0);    // line number high
        expect(ram.read(0x08, 0x05)).toBe(0x83); // DATA
        expect(ram.read(0x08, 0x06)).toBe(0x22); // "
        expect(ram.read(0x08, 0x07)).toBe(0x61); // a
        expect(ram.read(0x08, 0x08)).toBe(0x62); // b
        expect(ram.read(0x08, 0x09)).toBe(0x63); // c
        expect(ram.read(0x08, 0x0a)).toBe(0x22); // "
        expect(ram.read(0x08, 0x0b)).toBe(0x00); // end of line
    });

    it('parses DATA statements with literals', () => {
        const ram = new RAM(0, 0xff); // 64K of RAM

        compileToMemory(ram, '10 DATAHELLO');
        expect(ram.read(0x08, 0x03)).toBe(10);   // line number low
        expect(ram.read(0x08, 0x04)).toBe(0);    // line number high
        expect(ram.read(0x08, 0x05)).toBe(0x83); // DATA
        expect(ram.read(0x08, 0x06)).toBe(0x48); // H
        expect(ram.read(0x08, 0x07)).toBe(0x45); // E
        expect(ram.read(0x08, 0x08)).toBe(0x4C); // L
        expect(ram.read(0x08, 0x09)).toBe(0x4C); // L
        expect(ram.read(0x08, 0x0a)).toBe(0x4F); // O
        expect(ram.read(0x08, 0x0b)).toBe(0x00); // end of line
    });

    it('parses DATA statements with literals including lower-case', () => {
        const ram = new RAM(0, 0xff); // 64K of RAM

        compileToMemory(ram, '10 DATAHello');
        expect(ram.read(0x08, 0x03)).toBe(10);   // line number low
        expect(ram.read(0x08, 0x04)).toBe(0);    // line number high
        expect(ram.read(0x08, 0x05)).toBe(0x83); // DATA
        expect(ram.read(0x08, 0x06)).toBe(0x48); // H
        expect(ram.read(0x08, 0x07)).toBe(0x65); // e
        expect(ram.read(0x08, 0x08)).toBe(0x6C); // l
        expect(ram.read(0x08, 0x09)).toBe(0x6C); // l
        expect(ram.read(0x08, 0x0a)).toBe(0x6F); // o
        expect(ram.read(0x08, 0x0b)).toBe(0x00); // end of line
    });

    it('parses DATA statements with literals including quotes', () => {
        const ram = new RAM(0, 0xff); // 64K of RAM

        compileToMemory(ram, '10 DATAAA"B');
        expect(ram.read(0x08, 0x03)).toBe(10);   // line number low
        expect(ram.read(0x08, 0x04)).toBe(0);    // line number high
        expect(ram.read(0x08, 0x05)).toBe(0x83); // DATA
        expect(ram.read(0x08, 0x06)).toBe(0x41); // A
        expect(ram.read(0x08, 0x07)).toBe(0x41); // A
        expect(ram.read(0x08, 0x08)).toBe(0x22); // "
        expect(ram.read(0x08, 0x09)).toBe(0x42); // B
        expect(ram.read(0x08, 0x0a)).toBe(0x00); // end of line
    });

    it('parses DATA statements with literals including spaces', () => {
        const ram = new RAM(0, 0xff); // 64K of RAM

        compileToMemory(ram, '10 DATAA  B');
        expect(ram.read(0x08, 0x03)).toBe(10);   // line number low
        expect(ram.read(0x08, 0x04)).toBe(0);    // line number high
        expect(ram.read(0x08, 0x05)).toBe(0x83); // DATA
        expect(ram.read(0x08, 0x06)).toBe(0x41); // A
        expect(ram.read(0x08, 0x07)).toBe(0x20); // space
        expect(ram.read(0x08, 0x08)).toBe(0x20); // space
        expect(ram.read(0x08, 0x09)).toBe(0x42); // B
        expect(ram.read(0x08, 0x0a)).toBe(0x00); // end of line
    });

    it('terminates DATA statements at colons', () => {
        const ram = new RAM(0, 0xff); // 64K of RAM

        compileToMemory(ram, '10 DATAAA:FORI=1TO1');
        expect(ram.read(0x08, 0x03)).toBe(10);   // line number low
        expect(ram.read(0x08, 0x04)).toBe(0);    // line number high
        expect(ram.read(0x08, 0x05)).toBe(0x83); // DATA
        expect(ram.read(0x08, 0x06)).toBe(0x41); // A
        expect(ram.read(0x08, 0x07)).toBe(0x41); // A
        expect(ram.read(0x08, 0x08)).toBe(0x3a); // :
        expect(ram.read(0x08, 0x09)).toBe(0x81); // FOR
        expect(ram.read(0x08, 0x0a)).toBe(0x49); // I
        expect(ram.read(0x08, 0x0b)).toBe(0xd0); // = (token)
        expect(ram.read(0x08, 0x0c)).toBe(0x31); // 1
        expect(ram.read(0x08, 0x0d)).toBe(0xc1); // TO
        expect(ram.read(0x08, 0x0e)).toBe(0x31); // 1
        expect(ram.read(0x08, 0x0f)).toBe(0x00); // end of line
    });

    it('does not terminate DATA statements with a literal with a quote at colon', () => {
        const ram = new RAM(0, 0xff); // 64K of RAM

        compileToMemory(ram, '10 DATAA":FORI=1TO1');
        expect(ram.read(0x08, 0x03)).toBe(10);   // line number low
        expect(ram.read(0x08, 0x04)).toBe(0);    // line number high
        expect(ram.read(0x08, 0x05)).toBe(0x83); // DATA
        expect(ram.read(0x08, 0x06)).toBe(0x41); // A
        expect(ram.read(0x08, 0x07)).toBe(0x22); // "
        expect(ram.read(0x08, 0x08)).toBe(0x3a); // :
        expect(ram.read(0x08, 0x09)).toBe(0x46); // F
        expect(ram.read(0x08, 0x0a)).toBe(0x4F); // O
        expect(ram.read(0x08, 0x0b)).toBe(0x52); // R
        expect(ram.read(0x08, 0x0c)).toBe(0x49); // I
        expect(ram.read(0x08, 0x0d)).toBe(0x3D); // =
        expect(ram.read(0x08, 0x0e)).toBe(0x31); // 1
        expect(ram.read(0x08, 0x0f)).toBe(0x54); // T
        expect(ram.read(0x08, 0x10)).toBe(0x4F); // O
        expect(ram.read(0x08, 0x11)).toBe(0x31); // 1
        expect(ram.read(0x08, 0x12)).toBe(0x00); // end of line
    });

    it('does terminate DATA statements with a literal with two quotes at colon', () => {
        const ram = new RAM(0, 0xff); // 64K of RAM

        compileToMemory(ram, '10 DATAA"":FORI=1TO1');
        expect(ram.read(0x08, 0x03)).toBe(10);   // line number low
        expect(ram.read(0x08, 0x04)).toBe(0);    // line number high
        expect(ram.read(0x08, 0x05)).toBe(0x83); // DATA
        expect(ram.read(0x08, 0x06)).toBe(0x41); // A
        expect(ram.read(0x08, 0x07)).toBe(0x22); // "
        expect(ram.read(0x08, 0x08)).toBe(0x22); // "
        expect(ram.read(0x08, 0x09)).toBe(0x3a); // :
        expect(ram.read(0x08, 0x0a)).toBe(0x81); // FOR
        expect(ram.read(0x08, 0x0b)).toBe(0x49); // I
        expect(ram.read(0x08, 0x0c)).toBe(0xd0); // = (token)
        expect(ram.read(0x08, 0x0d)).toBe(0x31); // 1
        expect(ram.read(0x08, 0x0e)).toBe(0xc1); // TO
        expect(ram.read(0x08, 0x0f)).toBe(0x31); // 1
        expect(ram.read(0x08, 0x10)).toBe(0x00); // end of line
    });

    it('does not require a space after line number', () => {
        const compiler = new ApplesoftCompiler();
        compiler.compile('10PRINT"HELLO, WORLD!"');
        expect(compiler.program()).toEqual(new Uint8Array([
            0x16, 0x08, 0x0a, 0x00, 0xba, 0x22, 0x48, 0x45,
            0x4c, 0x4c, 0x4f, 0x2c, 0x20, 0x57, 0x4f, 0x52,
            0x4c, 0x44, 0x21, 0x22, 0x00, 0x00, 0x00
        ]));
    });

    it('parses ? as PRINT', () => {
        const compiler = new ApplesoftCompiler();
        compiler.compile('10 ?"HELLO, WORLD!"');
        expect(compiler.program()).toEqual(new Uint8Array([
            0x16, 0x08, 0x0a, 0x00, 0xba, 0x22, 0x48, 0x45,
            0x4c, 0x4c, 0x4f, 0x2c, 0x20, 0x57, 0x4f, 0x52,
            0x4c, 0x44, 0x21, 0x22, 0x00, 0x00, 0x00
        ]));
    });

    it('skips spaces when reading tokens', () => {
        const compiler = new ApplesoftCompiler();
        compiler.compile('10 T H E N');
        expect(compiler.program()).toEqual(new Uint8Array([
            0x07, 0x08, 0x0a, 0x00, 0xc4, 0x00, 0x00, 0x00,
        ]));
    });

    it('skips spaces and ignores case when reading tokens', () => {
        const compiler = new ApplesoftCompiler();
        compiler.compile('10 T h E n');
        expect(compiler.program()).toEqual(new Uint8Array([
            0x07, 0x08, 0x0a, 0x00, 0xc4, 0x00, 0x00, 0x00,
        ]));
    });

    it('smashes tokens together', () => {
        const compiler = new ApplesoftCompiler();
        compiler.compile('10 NOT RACE A THEN B');
        expect(compiler.program()).toEqual(new Uint8Array([
            0x0c, 0x08, 0x0a, 0x00, 0x9c, 0xc5, 0x48, 0x45,
            0x4e, 0x42, 0x00, 0x00, 0x00,
        ]));
    });

    it('parses 10ATOZ correctly', () => {
        const compiler = new ApplesoftCompiler();
        compiler.compile('10ATOZ');
        expect(compiler.program()).toEqual(new Uint8Array([
            0x09, 0x08, 0x0a, 0x00, 0x41, 0xc1, 0x5a, 0x00,
            0x00, 0x00,
        ]));
    });

    it('parses a bunch of crazy correctly', () => {
        const compiler = new ApplesoftCompiler();
        compiler.compile([
            '10 A THEN B',
            '30 A TO Z',
            '40 AT N',
            '50 A TN',
            '60 N O T R A C E',
            '70 NOT RACE'].join('\n'));
        expect(compiler.program()).toEqual(new Uint8Array([
            0x0b, 0x08, 0x0a, 0x00, 0xc5, 0x48, 0x45,
            0x4e, 0x42, 0x00, 0x13, 0x08, 0x1e, 0x00, 0x41,
            0xc1, 0x5a, 0x00, 0x1a, 0x08, 0x28, 0x00, 0xc5,
            0x4e, 0x00, 0x20, 0x08, 0x32, 0x00, 0xe1, 0x00,
            0x26, 0x08, 0x3c, 0x00, 0x9c, 0x00, 0x2c, 0x08,
            0x46, 0x00, 0x9c, 0x00, 0x00, 0x00,
        ]));
    });
});
