package prog8tests.codegeneration

import io.kotest.core.spec.style.FunSpec
import io.kotest.engine.spec.tempdir
import io.kotest.matchers.ints.shouldBeGreaterThan
import io.kotest.matchers.shouldBe
import io.kotest.matchers.shouldNotBe
import io.kotest.matchers.string.shouldContain
import io.kotest.matchers.string.shouldStartWith
import io.kotest.matchers.types.instanceOf
import prog8.code.ast.*
import prog8.code.core.BaseDataType
import prog8.code.core.DataType
import prog8.code.target.*
import prog8tests.helpers.ErrorReporterForTests
import prog8tests.helpers.compileText
import kotlin.io.path.readText

class TestVariousCodeGen: FunSpec({

    val outputDir = tempdir().toPath()

    test("nested scoping") {
        val text="""
main {
    sub start() {
        testscope.duplicate()
        cx16.r0L = testscope.duplicate2()
    }
}

testscope {

    sub sub1() {
        ubyte @shared duplicate
        ubyte @shared duplicate2
    }

    sub duplicate() {
        ; do nothing
    }

    sub duplicate2() -> ubyte {
        return cx16.r0L
    }
}"""
        compileText(C64Target(), false, text, outputDir, writeAssembly = true) shouldNotBe null
    }

    test("word array indexing") {
        val text="""
main {
    sub start() {
        uword[3] seed
        cx16.r0 = seed[0] + seed[1] + seed[2]
    }
}"""
        compileText(C64Target(), false, text, outputDir, writeAssembly = true) shouldNotBe null
    }

    test("ast result from compileText") {
        val text="""
main {
    sub start() {
        uword[3] seed
        cx16.r0 = seed[0] + seed[1] + seed[2]
    }
}"""
        val result = compileText(C64Target(), false, text, outputDir, writeAssembly = true)!!
        result.compilerAst.name shouldStartWith  "on_the_fly"
        val ast = result.codegenAst!!
        ast.name shouldBe result.compilerAst.name
        ast.children.size shouldBeGreaterThan 2
        val start = ast.entrypoint()!!
        start.name shouldBe "p8s_start"
        start.children.size shouldBeGreaterThan 2
        val seed = start.children[1] as PtVariable
        seed.name shouldBe "p8v_seed"
        seed.value shouldBe null
        seed.type shouldBe DataType.arrayFor(BaseDataType.UWORD)
        val assign = start.children[2] as PtAssignment
        assign.target.identifier!!.name shouldBe "cx16.r0"
        assign.value shouldBe instanceOf<PtBinaryExpression>()
    }

    test("peek and poke argument types") {
        val text="""
main {
    sub start() {
        uword[3] arr
        ubyte i = 42
        uword ww = peekw(arr[i])
        ubyte xx = peek(arr[i])
        xx = @(arr[i])
        
        @(arr[i]) = 42
        poke(arr[i], 42)
        pokew(arr[i], 4242)
    }
}"""
        compileText(C64Target(), false, text, outputDir, writeAssembly = true) shouldNotBe null
    }

    test("assigning memory byte into arrays works") {
        val text="""
main {
    sub start() {
        uword factor1
        ubyte[3] bytearray
        uword[3] wordarray
        @(factor1) = bytearray[0]
        bytearray[0] = @(factor1)
        @(factor1) = lsb(wordarray[0])
        wordarray[0] = @(factor1)
        @(5000) = bytearray[0]
        @(5000) = lsb(wordarray[0])
        bytearray[0] = @(5000)
        wordarray[0] = @(5000)
    }
}"""
        compileText(C64Target(), false, text, outputDir, writeAssembly = true) shouldNotBe null
    }

    test("reading memory from unknown var gives proper error") {
        val text="""
main {
    sub start() {
        cx16.r0L = @(doesnotexist)
    }
}"""
        val errors = ErrorReporterForTests()
        compileText(C64Target(), false, text, outputDir, writeAssembly = true, errors = errors)
        errors.errors.size shouldBe 2
        errors.errors[0] shouldContain "invalid address type"
        errors.errors[1] shouldContain "undefined symbol: doesnotexist"
    }

    test("shifting by word value is ok") {
        val text="""
main {
    sub start() {
        ubyte @shared c = 1
        @(15000 + c<<${'$'}0003) = 42
        @(15000 + (c<<${'$'}0003)) = 42
        @(15000 + c*${'$'}0008) = 42       ; *8 becomes a shift after opt

        uword @shared qq = 15000 + c<<${'$'}0003
        qq = 15000 + (c<<${'$'}0003)
        qq = 16000 + c*${'$'}0008
    }
}"""
        compileText(C64Target(), true, text, outputDir, writeAssembly = true) shouldNotBe null
        compileText(VMTarget(), true, text, outputDir, writeAssembly = true) shouldNotBe null
    }

    test("builtin func in float expression") {
        val src="""
%import floats
main {
    sub start() {
        float @shared fl =25.1
        fl = abs(fl)+0.5
        fl = sqrt(fl)+0.5
    }
}"""
        compileText(C64Target(), false, src, outputDir, writeAssembly = true) shouldNotBe null
    }

    test("string vars in inlined subroutines are ok") {
        val src="""
main {
    sub start() {
        void block2.curdir()
        void block2.other()
    }
}

block2 {
    str result="zzzz"
    sub curdir() -> str {
        return result
    }

    sub other() -> str {
        return "other"
    }
}"""

        compileText(C64Target(), true, src, outputDir, writeAssembly = true) shouldNotBe null
        compileText(VMTarget(), true, src, outputDir, writeAssembly = true) shouldNotBe null
    }

    test("array with pointers") {
        val src = """
main {
    sub start() {
        str localstr = "hello"
        ubyte[] otherarray = [1,2,3]
        uword[] words = [1111,2222,"three",&localstr,&otherarray]
        uword @shared zz = &words
        bool @shared result = 2222 in words
        zz = words[2]
        zz++
        zz = words[3]
    }
}"""
        val othertarget = Cx16Target()
        compileText(othertarget, true, src, outputDir, writeAssembly = true) shouldNotBe null
    }

    test("case sensitive symbols") {
        val src = """
main {
    sub start() {
        ubyte bytevar = 11      ; var at 0
        ubyte byteVAR = 22      ; var at 1
        ubyte ByteVar = 33      ; var at 2
        ubyte @shared total = bytevar+byteVAR+ByteVar   ; var at 3
        goto skipLABEL
SkipLabel:
        return
skipLABEL:
        bytevar = 42
    }
}"""
        val target = Cx16Target()
        compileText(target, true, src, outputDir, writeAssembly = true) shouldNotBe null
    }

    test("addresses from labels/subroutines") {
        val src = """
main {
    sub start() {

mylabel:
        ubyte @shared variable
        uword @shared pointer1 = &main.start
        uword @shared pointer2 = &start
        uword @shared pointer3 = &main.start.mylabel
        uword @shared pointer4 = &mylabel
        uword[] @shared ptrs = [&variable, &start, &main.start, &mylabel, &main.start.mylabel]
    }
}

"""
        compileText(Cx16Target(), true, src, outputDir, writeAssembly = true) shouldNotBe null
    }

    test("duplicate symbols okay other block and variable") {
        val src = """
main {
    ubyte derp
    sub start() {
        derp++
        foo.bar()
    }
}

foo {
    sub bar() {
        derp.print()
    }
}

derp {
    sub print() {
        cx16.r0++
        cx16.r1++
    }
}"""

        compileText(VMTarget(), false, src, outputDir, writeAssembly = true) shouldNotBe null
        compileText(Cx16Target(), false, src, outputDir, writeAssembly = true) shouldNotBe null
    }

    test("ambiguous symbol name variable vs block") {
        val src = """
main {
    sub start() {
        uword module
        module++
        module.test++
    }
}

module {
    ubyte @shared test
}
"""
        val errors=ErrorReporterForTests()
        compileText(VMTarget(), false, src, outputDir, writeAssembly = false, errors = errors) shouldBe null
        errors.errors.size shouldBe 1
        errors.errors[0] shouldContain "ambiguous symbol"
    }

    test("prefix expressions with typecasting") {
        val src="""
main
{
    sub start()
    {
        uword uw = 54321
        ubyte ub = 123
        word sw = -12345
        byte sb = -123

        func_uw(~ub as uword) 
        func_ub(~uw as ubyte) 
        func_uw(~sb as uword) 
        func_ub(~sw as ubyte) 
        func_w(-sb as word)   
        func_b(-sw as byte)   
    }
    
    sub func_uw(uword arg) {
    }
    sub func_w(word arg) {
    }
    sub func_ub(ubyte arg) {
    }
    sub func_b(byte arg) {
    }
}"""

        compileText(VMTarget(), false, src, outputDir, writeAssembly = true) shouldNotBe null
        compileText(Cx16Target(), false, src, outputDir, writeAssembly = true) shouldNotBe null
    }

    test("inlining sub with 2 statements") {
        val src="""
main {
    sub start() {
        init()
    }

    sub init() {
        init_handler()
        return
    }

    sub init_handler() {
        cx16.r0++
    }
}"""
        compileText(VMTarget(), true, src, outputDir, writeAssembly = false) shouldNotBe null
        compileText(Cx16Target(), true, src, outputDir, writeAssembly = false) shouldNotBe null
    }

    test("push pop are inlined also with noopt") {
        val text = """
main {
    sub start() {
        sys.push(11)
        sys.pushw(2222)
        sys.push_returnaddress(3333)
        cx16.r2++
        cx16.r1 = sys.popw()
        cx16.r0L = sys.pop()
    } 
}"""
        val result = compileText(C64Target(), false, text, outputDir, writeAssembly = true)!!
        val assemblyFile = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".asm")
        val assembly = assemblyFile.readText()
        assembly shouldContain "inlined routine follows: push"
        assembly shouldContain "inlined routine follows: pushw"
        assembly shouldContain "inlined routine follows: push_returnaddress"
        assembly shouldContain "inlined routine follows: pop"
        assembly shouldContain "inlined routine follows: popw"
    }

    test("syslib correctly available for raw outputs") {
        val text = """
%output raw
%launcher none
%address ${'$'}2000

main {
    sub start() {
        cx16.r0++
        sys.clear_carry()
    }
}
"""
        compileText(Cx16Target(), false, text, outputDir, writeAssembly = true) shouldNotBe null
        compileText(C64Target(), false, text, outputDir, writeAssembly = true) shouldNotBe null
        compileText(C128Target(), false, text, outputDir, writeAssembly = true) shouldNotBe null
        compileText(PETTarget(), false, text, outputDir, writeAssembly = true) shouldNotBe null
        compileText(VMTarget(), false, text, outputDir, writeAssembly = true) shouldNotBe null
    }

    test("block start address must be greater than program load address") {
        val src = """
%output raw
%launcher none
%address ${'$'}2000

main $2000 {
    sub start() {
        sys.clear_carry()
    }
}

otherblock ${'$'}2013 {
    %option force_output
}

thirdblock ${'$'}2014 {
    %option force_output
}"""
        val errors = ErrorReporterForTests()
        compileText(C64Target(), false, src, outputDir, writeAssembly = false, errors = errors) shouldBe null
        errors.errors.size shouldBe 2
        errors.errors[0] shouldContain "6:1: block address must be at least program load address + 20"
        errors.errors[1] shouldContain "12:1: block address must be at least program load address + 20"
    }

    test("for loops with just 1 iteration") {
        val src="""
main {
    sub start() {
        for cx16.r0L in 100 downto 100 {
            cx16.r1++
        }
        for cx16.r0L in 100 to 100 {
            cx16.r1++
        }
        for cx16.r0 in 2222 downto 2222 {
            cx16.r1++
        }
        for cx16.r0 in 2222 to 2222 {
            cx16.r1++
        }

        for cx16.r0L in 100 downto 100 step -5 {
            cx16.r1++
        }
        for cx16.r0L in 100 to 100 step 5 {
            cx16.r1++
        }
        for cx16.r0 in 2222 downto 2222 step -5 {
            cx16.r1++
        }
        for cx16.r0 in 2222 to 2222 step 5 {
            cx16.r1++
        }
    }
}"""
        compileText(VMTarget(), true, src, outputDir, writeAssembly = true) shouldNotBe null
        compileText(VMTarget(), false, src, outputDir, writeAssembly = true) shouldNotBe null
        compileText(Cx16Target(), true, src, outputDir, writeAssembly = true) shouldNotBe null
        compileText(Cx16Target(), false, src, outputDir, writeAssembly = true) shouldNotBe null
    }

    test("multiple status flags return values from asmsub") {
        val src="""
main {
    extsub 5000 = carryAndNegativeAndByteAndWord() -> bool @Pc, bool @Pn, ubyte @X, uword @AY

    sub start() {
        ubyte @shared x
        uword @shared w
        bool @shared flag1
        bool @shared flag2

        flag1, flag2, x, w = carryAndNegativeAndByteAndWord()
        flag1, void, void, w = carryAndNegativeAndByteAndWord()
        void, void, void, void = carryAndNegativeAndByteAndWord()
        void carryAndNegativeAndByteAndWord()
    }    
}"""
        compileText(Cx16Target(), false, src, outputDir, writeAssembly = true) shouldNotBe null
    }

    test("missing rts in asmsub") {
        val src="""
main {
    sub start() {
        test()
        test2()
    }

    asmsub test() {
        %asm {{
            nop
            nop
        }}
    }

    inline asmsub test2() {
        %asm {{
            nop
            nop
        }}
    }
}"""

        val errors = ErrorReporterForTests()
        compileText(C64Target(), false, src, outputDir, writeAssembly = true, errors = errors) shouldBe null
        errors.errors.size shouldBe 1
        errors.errors[0] shouldContain "asmsub seems to never return"
    }

    test("missing rts in asmsub suppressed") {
        val src="""
main {
    sub start() {
        test()
    }

    asmsub test() {
        %asm {{
            nop
            nop
            ; !notreached!
        }}
    }
}"""

        compileText(C64Target(), false, src, outputDir, writeAssembly = true) shouldNotBe null
    }

    test("if not without else is not swapped") {
        val src="""
main {
    sub start() {
        if not thing()
            cx16.r0++
    }

    sub thing() -> bool {
        cx16.r0++
        return false
    }
}"""
        compileText(C64Target(), false, src, outputDir, writeAssembly = true) shouldNotBe null
        val result = compileText(VMTarget(), false, src, outputDir, writeAssembly = true)!!
        val st = result.codegenAst!!.entrypoint()!!.children
        st.size shouldBe 3
        val ifelse = st[1] as PtIfElse
        ifelse.hasElse() shouldBe false
        (ifelse.condition as PtPrefix).operator shouldBe "not"
    }

    test("if not with else is swapped") {
        val src="""
main {
    sub start() {
        if not thing()
            cx16.r0++
        else
            cx16.r1++
    }

    sub thing() -> bool {
        cx16.r0++
        return false
    }
}"""
        compileText(C64Target(), false, src, outputDir, writeAssembly = true) shouldNotBe null
        val result = compileText(VMTarget(), false, src, outputDir, writeAssembly = true)!!
        val st = result.codegenAst!!.entrypoint()!!.children
        st.size shouldBe 3
        val ifelse = st[1] as PtIfElse
        ifelse.hasElse() shouldBe true
        ifelse.condition shouldBe instanceOf<PtFunctionCall>()
    }

    test("bit instruction is used for testing bits 6 and 7 of a byte") {
        val text = """
main {
    sub start() {   
        if cx16.r0L & ${'$'}80 != 0
            return
        if cx16.r1L & ${'$'}80 == 0
            return
        if cx16.r2L & ${'$'}40 != 0
            return
        if cx16.r3L & ${'$'}40 == 0
            return
        cx16.r9L = if cx16.r4L & ${'$'}80 != 0  11 else 22
        cx16.r10L = if cx16.r5L & ${'$'}40 == 0  11 else 22
    } 
}"""
        val result = compileText(C64Target(), true, text, outputDir, writeAssembly = true)!!
        val assemblyFile = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".asm")
        val assembly = assemblyFile.readText()
        assembly shouldContain "bit  cx16.r0L"
        assembly shouldContain "bit  cx16.r1L"
        assembly shouldContain "bit  cx16.r2L"
        assembly shouldContain "bit  cx16.r3L"
        assembly shouldContain "bit  cx16.r4L"
        assembly shouldContain "bit  cx16.r5L"

        val resultIR = compileText(VMTarget(), true, text, outputDir, writeAssembly = true)!!
        val irFile = resultIR.compilationOptions.outputDir.resolve(result.compilerAst.name + ".p8ir")
        val ir = irFile.readText()
        ir shouldContain "bit.b ${'$'}ff02"     // r0
        ir shouldContain "bit.b ${'$'}ff04"     // r1
        ir shouldContain "bit.b ${'$'}ff06"     // f2
        ir shouldContain "bit.b ${'$'}ff08"     // r3
        ir shouldContain "bit.b ${'$'}ff0a"     // r4
        ir shouldContain "bit.b ${'$'}ff0c"     // r5
    }

    test("strings in if expression") {
        val src="""
main {
    sub start() {
       str foo = "foo"
       str bar = "bar"
       bool flag = true
       uword @shared foobar = if flag foo else bar
    }
}"""
        compileText(C64Target(), false, src, outputDir, writeAssembly = true) shouldNotBe null
        compileText(VMTarget(), false, src, outputDir, writeAssembly = true) shouldNotBe null
    }

    test("word bitshift with byte operand") {
        val src="""
main{
    sub start() {
        cx16.r0 >>= 4
        cx16.r1 <<= 4
    }
}"""

        compileText(C64Target(), false, src, outputDir) shouldNotBe null
        compileText(VMTarget(), false, src, outputDir) shouldNotBe null
        compileText(C64Target(), true, src, outputDir) shouldNotBe null
        compileText(VMTarget(), true, src, outputDir) shouldNotBe null
    }
})