//
// Note that this is supposed to run from command line.
// Do not use anything besides basic.pause, control.inBackground, console.log
//

//% shim=pxtrt::panic
function panic(code2: number): void { }

function msg(s: string): void {
    //console.log(s)
    //basic.pause(50);
}

function assert(cond: boolean, msg_: string) {
    if (!cond) {
        console.log("ASSERT: " + msg_);
        panic(45);
    }
}

//
// start tests
//

var glb1: number;
var s2: string;
var x: number;
var action: Action;
var tot: string;
var lazyAcc: number;
var sum: number;

var xyz = 12;

console.log("Starting...")

//lib.print_17(3);
basic.showNumber(0);
//assert(lib3.getX() == 17 * 3, "");

testNums();
testStrings();
testNumCollection();
testStringCollection();
testStringOps();
testReccoll();
inBg();
testAction(1);
testAction(7);
testIter();
testActionSave();
testLazyOps();
testRefLocals();
testByRefParams();
testFunDecl();
testDefaultArgs();
testMemoryFree();
testMemoryFreeHOF();
postPreFix()
eqOp()
testEnums()
testBuffer()

// test some top-level code
let xsum = 0;
for (let i = 0; i < 11; ++i) {
    xsum = xsum + i;
}
assert(xsum == 55, "mainfor")

control.inBackground(() => {
    xsum = xsum + 10;
})

basic.pause(20)
assert(xsum == 65, "mainforBg")

assert(xyz == 12, "init")

function incrXyz() {
    xyz++;
    return 0;
}
var unusedInit = incrXyz();

assert(xyz == 13, "init2")


testClass()

basic.showNumber(1)


console.log("ALL TESTS OK")


function defaultArgs(x: number, y = 3, z = 7) {
    return x + y + z;
}

function testDefaultArgs() {
    msg("testDefaultArgs");
    assert(defaultArgs(1) == 11, "defl0")
    assert(defaultArgs(1, 4) == 12, "defl1")
    assert(defaultArgs(1, 4, 8) == 13, "defl2")

    assert(optargs(1) == 1, "opt0");
    assert(optargs(1, 2) == 3, "opt1");
    assert(optargs(1, 2, 3) == 3, "opt2");

    assert(optstring(3) == 6, "os0")
    assert(optstring(3, "7") == 10, "os1")
    assert(optstring2(3) == 6, "os0")
    assert(optstring2(3, "7") == 10, "os1")
}

function optargs(x: number, y ?: number, z ?: number) {
    return x + y;
}

function optstring(x: number, s ?: string) {
    if (s != null) {
        return parseInt(s) + x;
    }
    return x * 2;
}

function optstring2(x: number, s: string = null) {
    if (s != null) {
        return parseInt(s) + x;
    }
    return x * 2;
}

function testNums(): void {
    let x = 40 + 2;
    assert(x == 42, "add");
    x = 40 / 2;
    assert(x == 20, "div");
    let r = fib(15);
    msg("FIB" + r);
    assert(r == 987, "fib");
    let x3 = doStuff(x, 2);
    assert(x3 == 10, "call order");
    glb1 = 5;
    incrBy_2();
    assert(glb1 == 7, "glb1");
    incrBy_2();
    assert(glb1 == 9, "glb2");
    assert(Math.abs(-42) == 42, "abs");
    assert(Math.abs(42) == 42, "abs");
    assert(Math.sign(42) == 1, "abs");
    testIf();

    assert((3 & 6) == 2, "&")
    assert((3 | 6) == 7, "|")
    assert((3 ^ 6) == 5, "^")
    assert((-10 >> 2) == -3, ">>")
    assert((-10 >>> 20) == 4095, ">>>")
    assert((-10 << 2) == -40, "<<")
    assert((10 << 2) == 40, "<<+")
    assert((10 >> 2) == 2, ">>+")
    assert((10 >>> 2) == 2, ">>>+")
    assert(1000000 * 1000000 == -727379968, "*")
    assert(100000001 * 100000001 == 2074919425, "*2")

    assert(105 % 100 == 5, "perc")
}



function fib(p: number): number {
    if (p <= 2) {
        return p;
    }
    let p2 = p - 1;
    return fib(p2) + fib(p - 2);
}

function doStuff(x: number, x2: number): number {
    let x3 = x / x2;
    return x3;
}


function testIf(): void {
    let b = false;
    if (!b) {
        glb1 = 7;
    } else {
        assert(false, "b0");
    }
    assert(glb1 == 7, "glb3");
    if (b) {
        assert(false, "b1");
    } else {
        glb1 = 8;
    }
    assert(glb1 == 8, "glb3");
}


function incrBy_2(): void {
    glb1 = glb1 + 2;
}

function testStrings(): void {
    assert((42).toString() == "42", "42");

    let s = "live";
    assert(s == "live", "hello eq");
    s = s + "4OK";
    s2 = s;
    assert(s.charCodeAt(4) == 52, "hello eq2");
    assert(s.charAt(4) == "4", "hello eq2X");
    assert(s[4] == "4", "hello eq2X");
    assert(s.length == 7, "len7");
    s = "";
    for (let i = 0; i < 10; i++) {
        s = s + i;
    }
    assert(s == "0123456789", "for");
    let x = 10;
    s = "";
    while (x >= 0) {
        s = s + x;
        x = x - 1;
    }
    assert(s == "109876543210", "while");
    msg(s);
    msg(s2);

    s2 = "";
    // don't leak ref

    x = 21
    s = "foo"
    s = `a${ x * 2 }X${ s }X${ s }Z`
    assert(s == "a42XfooXfoo" + "Z", "`")

    assert("X" + true == "Xt" + "rue", "boolStr")
}


function testNumCollection(): void {
    let collXYZ: number[] =[];
    assert(collXYZ.length == 0, "");
    collXYZ.push(42);
    assert(collXYZ.length == 1, "");
    collXYZ.push(22);
    assert(collXYZ[1] == 22, "");
    collXYZ.splice(0, 1);
    assert(collXYZ[0] == 22, "");
    collXYZ.removeElement(22);
    assert(collXYZ.length == 0, "");
    for (let i = 0; i < 100; i++) {
        collXYZ.push(i);
    }
    assert(collXYZ.length == 100, "");

    collXYZ =[1, 2, 3];
    assert(collXYZ.length == 3, "cons");
    assert(collXYZ[0] == 1, "cons0");
    assert(collXYZ[1] == 2, "cons1");
    assert(collXYZ[2] == 3, "cons2");
}

function testStringCollection(): void {
    let coll = (< string[] >[]);
    coll.push("foobar");
    coll.push((12).toString());
    coll.push(coll[0] + "xx");
    assert(coll.indexOf("12") == 1, "idx");
    coll =[
        "a" + "b",
        coll[2],
    ]
    assert(coll[0] == "ab", "")
    assert(coll[1] == "foob" + "arxx", "")
    assert(coll.length == 2, "")
}

function testStringOps(): void {
    assert("foo".concat("bar") == "foobar", "concat");
    assert("xAb".charCodeAt(1) == 65, "code at");
    assert("B".charCodeAt(0) == 66, "tcc");
    assert(parseInt("-123") == -123, "tonum");
    assert("fo"[1] == "o", "at");
    assert("fo".length == 2, "count");
    assert("fo".charCodeAt(17) == 0, "ct oor");
}

class Testrec {
    str: string;
    num: number;
    bool: boolean;
    str2: string;
}

function recordId(x: Testrec) {
    lazyAcc++
    return x
}

function postPreFix() {
    msg("postPref")
    let x = new Testrec()
    lazyAcc = 0
    recordId(x).num = 12
    assert(x.num == 12 && lazyAcc == 1, "X0")
    let y = recordId(x).num++
    assert(x.num == 13 && lazyAcc == 2, "X1")
    assert(y == 12, "X2")
    y = ++recordId(x).num
    assert(y == 14 && x.num == 14 && lazyAcc == 3, "X2")

    recordId(x).num >>= 1
    assert(x.num == 7, "X3")
    assert(lazyAcc == 4, "X4")
}

function eqOp() {
    msg("eqOp")
    let x = 12
    assert((x += 10) == 22, "Y0")
    assert(x == 22, "Y1")
    x /= 2
    assert(x == 11, "Y2")

    let s = ("fo" + 1)
    let t = ("ba" + 2)
    s += t
    assert(s == "fo1b" + "a2", "fb")
}

function testRec0(): Testrec {
    let testrec = new Testrec();
    testrec.str2 = "Hello" + " world";
    testrec.str = testrec.str2;
    testrec.num = 42;
    assert(testrec.str == "Hello world", "recstr");
    assert(testrec.num == 42, "recnum");
    msg(testrec.str2);
    let testrec2 = < Testrec > null;
    assert(testrec2 == null, "isinv");
    assert(testrec == testrec, "eq");
    assert(testrec != null, "non inv");
    return testrec;
}

function testReccoll(): void {
    let coll: Testrec[] =[];
    let item = testRec0();
    msg("in reccoll");
    coll.push(item);
}

function inBg() {
    let k = 7
    let q = 14
    let rec = new Testrec();
    glb1 = 0
    control.inBackground(() => {
        glb1 = glb1 + 10 + (q - k)
        rec.str = "foo"
    })
    control.inBackground(() => {
        glb1 = glb1 + 1
    })
    basic.pause(50)
    assert(glb1 == 18, "inbg0")
    assert(rec.str == "foo", "inbg1")
}

function runTwice(fn: Action): void {
    msg("r2 start");
    fn();
    fn();
    msg("r2 stop");
}

function iter(max: number, fn: (v: number) => void) {
    for (var i = 0; i < max; ++i) {
        fn(i);
    }
}

function testIter() {
    x = 0
    iter(10, v => {
        x = x + (v + 1)
    })
    assert(x == 55, "55")
}

function testAction(p: number): void {
    let s = "hello" + "1";
    let coll =[] as number[];
    let p2 = p * 2;
    x = 42;
    runTwice(() => {
        x = x + p + p2;
        coll.push(x);
        msg(s + x);
    });
    assert(x == 42 + p * 6, "run2");
    assert(coll.length == 2, "run2");
}

function add7() {
    sum = sum + 7;
}

function testFunDecl() {
    msg("testFunDecl");
    let x = 12;
    sum = 0;
    function addX() {
        sum = sum + x;
    }
    function add10() {
        sum = sum + 10;
    }
    runTwice(addX)
    assert(sum == 24, "cap")
    msg("testAdd10");
    runTwice(add10);
    msg("end-testAdd10");
    assert(sum == 44, "nocap");
    runTwice(add7);
    assert(sum == 44 + 14, "glb")
    addX();
    add10();
    assert(sum == 44 + 14 + x + 10, "direct");
}

function saveAction(fn: Action): void {
    action = fn;
}

function saveGlobalAction(): void {
    let s = "foo" + "42";
    tot = "";
    saveAction(() => {
        tot = tot + s;
    });
}

function testActionSave(): void {
    saveGlobalAction();
    runTwice(action);
    msg(tot);
    assert(tot == "foo42foo42", "");
    tot = "";
    action = null;
}

function testLazyOps(): void {
    lazyAcc = 0;
    if (incrLazyAcc(10, false) && incrLazyAcc(1, true)) {
        assert(false, "");
    } else {
        assert(lazyAcc == 10, "lazy1");
    }
    assert(lazyAcc == 10, "lazy2");
    if (incrLazyAcc(100, true) && incrLazyAcc(1, false)) {
        assert(false, "");
    } else {
        assert(lazyAcc == 111, "lazy4");
    }
    lazyAcc = 0;
    if (incrLazyAcc(100, true) && incrLazyAcc(8, true)) {
        assert(lazyAcc == 108, "lazy5");
    } else {
        assert(false, "");
    }
    lazyAcc = 0;
    if (incrLazyAcc(10, true) || incrLazyAcc(1, true)) {
        assert(lazyAcc == 10, "lazy1b");
    } else {
        assert(false, "");
    }
    assert(lazyAcc == 10, "lazy2xx");
    if (incrLazyAcc(100, false) || incrLazyAcc(1, false)) {
        assert(false, "");
    } else {
        assert(lazyAcc == 111, "lazy4x");
    }
    lazyAcc = 0;
    if (incrLazyAcc(100, false) || incrLazyAcc(8, true)) {
        assert(lazyAcc == 108, "lazy5");
    } else {
        assert(false, "");
    }
    lazyAcc = 0;
    if (incrLazyAcc(10, true) && incrLazyAcc(1, true) && incrLazyAcc(100, false)) {
        assert(false, "");
    } else {
        assert(lazyAcc == 111, "lazy10");
    }
    lazyAcc = 0;
    if (incrLazyAcc(10, true) && incrLazyAcc(1, true) || incrLazyAcc(100, false)) {
        assert(lazyAcc == 11, "lazy101");
    } else {
        assert(false, "");
    }

    lazyAcc = 0;
    assert((true ? incrLazyNum(1, 42): incrLazyNum(10, 36)) == 42, "?:")
    assert(lazyAcc == 1, "?:0");
    assert((false ? incrLazyNum(1, 42): incrLazyNum(10, 36)) == 36, "?:1")
    assert(lazyAcc == 11, "?:2");
}

function incrLazyAcc(delta: number, res: boolean): boolean {
    lazyAcc = lazyAcc + delta;
    return res;
}

function incrLazyNum(delta: number, res: number) {
    lazyAcc = lazyAcc + delta;
    return res;
}


function testRefLocals(): void {
    msg("start test ref locals");
    let s = "";
    // For 4 or more it runs out of memory
    for (let i = 0; i < 3; i++) {
        msg(i + "");
        let copy = i;
        control.inBackground(() => {
            basic.pause(10 * i);
            copy = copy + 10;
        });
        control.inBackground(() => {
            basic.pause(20 * i);
            s = s + copy;
        });
    }
    basic.pause(200);
    assert(s == "101112", "reflocals");
}

function byRefParam_0(p: number): void {
    control.inBackground(() => {
        basic.pause(1);
        sum = sum + p;
    });
    p = p + 1;
}

function byRefParam_2(pxx: number): void {
    pxx = pxx + 1;
    control.inBackground(() => {
        basic.pause(1);
        sum = sum + pxx;
    });
}

function testByRefParams(): void {
    msg("testByRefParams");
    refparamWrite("a" + "b");
    refparamWrite2(new Testrec());
    refparamWrite3(new Testrec());
    sum = 0;
    let x = 1;
    control.inBackground(() => {
        basic.pause(1);
        sum = sum + x;
    });
    x = 2;
    byRefParam_0(4);
    byRefParam_2(10);
    basic.pause(30);
    assert(sum == 18, "by ref");
}

function refparamWrite(s: string): void {
    s = s + "c";
    assert(s == "abc", "abc");
}

function refparamWrite2(testrec: Testrec): void {
    testrec = new Testrec();
    assert(testrec.bool == false, "");
}

function refparamWrite3(testrecX: Testrec): void {
    control.inBackground(() => {
        basic.pause(1);
        assert(testrecX.str == "foo", "ff");
        testrecX.str = testrecX.str + "x";
    });
    testrecX = new Testrec();
    testrecX.str = "foo";
    basic.pause(30);
    assert(testrecX.str == "foox", "ff2");
}

function testMemoryFree(): void {
    msg("testMemoryFree");
    for (let i = 0; i < 1000; i++) {
        allocImage();
    }
}

function runOnce(fn: Action): void {
    fn();
}

function createObj() {
    return new Testrec();
}

function testMemoryFreeHOF(): void {
    msg("testMemoryFreeHOF");
    for (let i = 0; i < 1000; i++) {
        runOnce(() => {
            let tmp = createObj();
        });
    }
}

function allocImage(): void {
    let tmp = createObj();
}

class Foo {
    pin: number;
    buf: number[];

    constructor(k: number, l: number) {
        this.pin = k - l
    }

    setPin(p: number) {
        this.pin = p
    }

    getPin() {
        return this.pin
    }

    init() {
        this.buf =[1, 2]
    }
}

function testClass() {
    let f = new Foo(272, 100);
    assert(f.getPin() == 172, "ctor")
    f.setPin(42)
    assert(f.getPin() == 42, "getpin")
}

enum En {
    A,
    B,
    C,
    D = 4200,
    E,
}

enum En2 {
    D0 = En.D,
    D1,
    D2 = 1,
}


function testEnums() {
    msg("enums")

    let k = En.C as number
    assert(k == 2, "e0")
    k = En.D as number
    assert(k == 4200, "e1")
    k = En.E as number
    assert(k == 4201, "e43")

    k = En2.D0 as number
    assert(k == 4200, "eX0")
    k = En2.D1 as number
    assert(k == 4201, "eX1")

    msg("enums0")
    assert(switchA(En.A) == 7, "s1")
    assert(switchA(En.B) == 7, "s2")
    assert(switchA(En.C) == 12, "s3")
    assert(switchA(En.D) == 13, "s4")
    assert(switchA(En.E) == 12, "s5")
    assert(switchA(-3 as En) == 12, "s6")

    msg("enums1")
    assert(switchB(En.A) == 7, "x1")
    assert(switchB(En.B) == 7, "x2")
    assert(switchB(En.C) == 17, "x3")
    assert(switchB(En.D) == 13, "x4")
    assert(switchB(En.E) == 14, "x5")
}


function switchA(e: En) {
    let r = 12;
    switch (e) {
        case En.A:
        case En.B: return 7;
        case En.D: r = 13; break;
    }
    return r
}

function switchB(e: En) {
    let r = 33;
    switch (e) {
        case En.A:
        case En.B: return 7;
        case En.D: r = 13; break;
        case En.E: r = 14; break;
        default: return 17;
    }
    return r;
}

function bufferIs(b:Buffer, a:number[]) {
    assert(b.length == a.length, "bis-len")
    for (let i = 0; i < a.length; ++i) {
        if (a[i] != b[i]) {
            assert(false, `bufferIs: buf[${i}]:${b[i]} != ${a[i]}`)
        }
    }
}

function testBuffer() {
    let b = pins.createBuffer(3);
    assert(b[0] == 0, "buf0");
    assert(b[1] == 0, "buf0");
    assert(b[2] == 0, "buf0");
    assert(b[-100000] == 0, "bufM");
    assert(b[100000] == 0, "bufM");

    b[0] = 42;
    bufferIs(b, [42, 0, 0]);
    b[2] = 41;
    bufferIs(b, [42, 0, 41]);

    b.rotate(1)
    bufferIs(b, [0, 41, 42]);
    b.rotate(-2)
    bufferIs(b, [41, 42, 0]);
    b.shift(1)
    bufferIs(b, [42, 0, 0]);
    b.rotate(9)
    bufferIs(b, [42, 0, 0]);
    b.rotate(-9)
    bufferIs(b, [42, 0, 0]);

    b.fill(4);
    bufferIs(b, [4, 4, 4]);

    b.fill(12, 1, 1);
    bufferIs(b, [4, 12, 4]);

    b.fill(13, 1, -1);
    bufferIs(b, [4, 13, 13]);

    b.fill(100, -1, -1);
    bufferIs(b, [4, 13, 13]);

    b.shift(-1)
    bufferIs(b, [0, 4, 13]);
}