package prog8tests.compiler

import io.kotest.assertions.withClue
import io.kotest.core.spec.style.FunSpec
import io.kotest.engine.spec.tempdir
import io.kotest.matchers.shouldBe
import io.kotest.matchers.shouldNotBe
import io.kotest.matchers.string.shouldContain
import io.kotest.matchers.types.instanceOf
import io.kotest.matchers.types.shouldBeSameInstanceAs
import prog8.ast.GlobalNamespace
import prog8.ast.ParentSentinel
import prog8.ast.expressions.NumericLiteral
import prog8.ast.statements.*
import prog8.code.target.C64Target
import prog8tests.helpers.ErrorReporterForTests
import prog8tests.helpers.compileText


class TestScoping: FunSpec({

    val outputDir = tempdir().toPath()
    
    test("modules parent is global namespace") {
        val src = """
            main {
                sub start() {
                }
            }
        """

        val result = compileText(C64Target(), false, src, outputDir, writeAssembly = false)!!
        val module = result.compilerAst.toplevelModule
        module.parent shouldBe instanceOf<GlobalNamespace>()
        module.program shouldBeSameInstanceAs result.compilerAst
        module.parent.parent shouldBe instanceOf<ParentSentinel>()
    }

    test("anon scope vars moved into subroutine scope") {
        val src = """
            main {
                sub start() {
                    repeat 10 {
                        ubyte xx = 99
                        rol(xx)
                    }
                }
            }
        """

        val result = compileText(C64Target(), false, src, outputDir, writeAssembly = false)!!
        val mainBlock = result.compilerAst.entrypoint.definingBlock
        val start = mainBlock.statements.single() as Subroutine
        val repeatbody = start.statements.filterIsInstance<RepeatLoop>().single().body
        withClue("no vars moved to main block") {
            mainBlock.statements.any { it is VarDecl } shouldBe false
        }
        val subroutineVars = start.statements.filterIsInstance<VarDecl>()
        withClue("var from repeat anonscope must be moved up to subroutine") {
            subroutineVars.size shouldBe 1
        }
        subroutineVars[0].name shouldBe "xx"
        withClue("var should have been removed from repeat anonscope") {
            repeatbody.statements.any { it is VarDecl } shouldBe false
        }
        val initassign = repeatbody.statements[0] as? Assignment
        withClue("vardecl in repeat should be replaced by init assignment") {
            initassign?.target?.identifier?.nameInSource shouldBe listOf("xx")
        }
        withClue("vardecl in repeat should be replaced by init assignment") {
            (initassign?.value as? NumericLiteral)?.number?.toInt() shouldBe 99
        }
        repeatbody.statements[1] shouldBe instanceOf<FunctionCallStatement>()
    }

    test("labels with anon scopes") {
        val src = """
            main {
                sub start() {
                    uword addr
                    goto labeloutside
        
                    if true {
                        if true {
                            addr = &iflabel
                            addr = &labelinside
                            addr = &labeloutside
                            addr = &main.start.nested.nestedlabel
                            goto labeloutside
                            goto iflabel
                            goto main.start.nested.nestedlabel
                        }
            iflabel:
                    }
        
                    repeat 10 {
                        addr = &iflabel
                        addr = &labelinside
                        addr = &labeloutside
                        addr = &main.start.nested.nestedlabel
                        goto iflabel
                        goto labelinside
                        goto main.start.nested.nestedlabel
            labelinside:
                    }

                    sub nested () {
            nestedlabel:
                        addr = &nestedlabel
                        goto nestedlabel
                        goto main.start.nested.nestedlabel
                    }

            labeloutside:
                    addr = &iflabel
                    addr = &labelinside
                    addr = &labeloutside
                    addr = &main.start.nested.nestedlabel
                    goto main.start.nested.nestedlabel
                }
            }
        """

        val result = compileText(C64Target(), false, src, outputDir, writeAssembly = true)!!
        val mainBlock = result.compilerAst.entrypoint.definingBlock
        val start = mainBlock.statements.single() as Subroutine
        val labels = start.statements.filterIsInstance<Label>()
        withClue("only one label in subroutine scope") {
            labels.size shouldBe 1
        }
    }

    test("good subroutine call without qualified names") {
        val text="""
            main {
                sub start() {
                    routine() 
                    routine2()
                    
                    sub routine2() {
                    }
                }
                sub routine() {
                    start()
                }
            }
        """
        compileText(C64Target(), false, text, outputDir, writeAssembly = false) shouldNotBe null
    }

    test("wrong subroutine call without qualified names") {
        val text="""
            main {
                sub start() {
                    sub routine2() {
                    }
                }
                sub routine() {
                    routine2()
                }
            }
        """
        val errors= ErrorReporterForTests()
        compileText(C64Target(), false, text, outputDir, writeAssembly = false, errors = errors) shouldBe null
        errors.errors.size shouldBe 1
        errors.errors[0] shouldContain "undefined"
        errors.errors[0] shouldContain "routine2"
    }

    test("good subroutine calls with qualified names (from root)") {
        val text="""
            main {
                sub start() {
                    main.routine() 
                    main.start.routine2()
                    
                    sub routine2() {
                    }
                }
                sub routine() {
                    main.start.routine2()
                }
            }
        """
        compileText(C64Target(), false, text, outputDir, writeAssembly = false) shouldNotBe null
    }

    test("wrong subroutine calls with qualified names (not from root)") {
        val text="""
            main {
                sub start() {
                    start.routine2()     
                    wrong.start.routine2()
                    sub routine2() {
                    }
                }
                sub routine() {
                    start.routine2()
                    wrong.start.routine2()
                }
            }
        """
        val errors= ErrorReporterForTests()
        compileText(C64Target(), false, text, outputDir, writeAssembly = false, errors=errors) shouldBe null
        errors.errors.size shouldBe 4
        errors.errors[0] shouldContain "undefined"
        errors.errors[0] shouldContain "start.routine2"
        errors.errors[1] shouldContain "undefined"
        errors.errors[1] shouldContain "wrong.start.routine2"
        errors.errors[2] shouldContain "undefined"
        errors.errors[2] shouldContain "start.routine2"
        errors.errors[3] shouldContain "undefined"
        errors.errors[3] shouldContain "wrong.start.routine2"
    }

    test("good variables without qualified names") {
        val text="""
            main {
                ubyte v1
                
                sub start() {
                    ubyte v2
                    v1=1
                    v2=2
                    
                    sub routine2() {
                        ubyte v3
                        v1=1
                        v2=2
                        v3=3
                    }
                }
                sub routine() {
                    ubyte v4
                    v1=1
                    v4=4
                }
            }
        """
        compileText(C64Target(), false, text, outputDir, writeAssembly = false) shouldNotBe null
    }

    test("wrong variables without qualified names") {
        val text="""
            main {
                ubyte v1
                
                sub start() {
                    ubyte v2
                    v1=1
                    v2=2
                    v3=3    ; can't access
                    v4=4    ; can't access
                    sub routine2() {
                        ubyte v3
                        v1=1
                        v2=2
                        v3=3
                        v4=3    ;can't access
                    }
                }
                sub routine() {
                    ubyte v4
                    v1=1
                    v2=2    ; can't access
                    v3=3    ; can't access
                    v4=4
                }
            }
        """
        val errors= ErrorReporterForTests()
        compileText(C64Target(), false, text, outputDir, writeAssembly = false, errors=errors) shouldBe null
        errors.errors.size shouldBe 5
        errors.errors[0] shouldContain "undefined symbol: v3"
        errors.errors[1] shouldContain "undefined symbol: v4"
        errors.errors[2] shouldContain "undefined symbol: v4"
        errors.errors[3] shouldContain "undefined symbol: v2"
        errors.errors[4] shouldContain "undefined symbol: v3"
    }

    test("good variable refs with qualified names (from root)") {
        val text="""
            main {
                sub start() {
                    uword xx 
                    xx = &main.routine
                    main.routine(5)
                    main.routine.value = 5
                    main.routine.arg = 5
                    xx = &main.routine.nested
                    main.routine.nested(5)
                    main.routine.nested.nestedvalue = 5
                    main.routine.nested.arg2 = 5
                }
                
                sub routine(ubyte arg) {
                    ubyte value
                    
                    sub nested(ubyte arg2) {
                        ubyte nestedvalue
                    }
                }
            }
        """
        compileText(C64Target(), false, text, outputDir, writeAssembly = false) shouldNotBe null
    }

    test("wrong variable refs with qualified names 1 (not from root)") {
        val text="""
            main {
                sub start() {
                    uword xx 
                    xx = &routine
                    routine(5)
                    routine.value = 5
                    routine.arg = 5
                    routine.nested.arg2 = 5
                    routine.nested.nestedvalue = 5
                    nested.nestedvalue = 5
                }
                
                sub routine(ubyte arg) {
                    ubyte value
                    
                    sub nested(ubyte arg2) {
                        ubyte nestedvalue
                    }
                }
            }
        """
        val errors= ErrorReporterForTests()
        compileText(C64Target(), false, text, outputDir, writeAssembly = false, errors=errors) shouldBe null
        errors.errors.size shouldBe 5
        errors.errors[0] shouldContain "undefined symbol: routine.value"
        errors.errors[1] shouldContain "undefined symbol: routine.arg"
        errors.errors[2] shouldContain "undefined symbol: routine.nested.arg2"
        errors.errors[3] shouldContain "undefined symbol: routine.nested.nestedvalue"
        errors.errors[4] shouldContain "undefined symbol: nested.nestedvalue"
    }

    test("various good goto targets") {
        val text="""
            main {
                sub start() {
                    uword address = $4000
                    
                    goto ${'$'}c000
                    goto address        ; indirect jump
                    goto main.routine
                    goto main.jumplabel
                    
                    if_cc
                        goto ${'$'}c000
                    if_cc
                        goto address        ; indirect jump
                    if_cc
                        goto main.routine
                    if_cc
                        goto main.jumplabel
                }

            jumplabel:
                %asm {{
                    rts
                }}
                sub routine() {
                }
            }
        """
        compileText(C64Target(), false, text, outputDir, writeAssembly = true) shouldNotBe null
    }

    test("various wrong goto targets") {
        val text = """
            main {
                sub start() {
                    byte wrongaddress = 100
                    
                    goto wrongaddress   ; must be uword
                    goto main.routine   ; can't take args
                }

                sub routine(ubyte arg) {
                }
            }
        """
        val errors = ErrorReporterForTests()
        compileText(C64Target(), false, text, outputDir, writeAssembly = false, errors = errors) shouldBe null
        errors.errors.size shouldBe 2
        errors.errors[0] shouldContain "wrong address"
        errors.errors[1] shouldContain "takes parameters"
    }

    test("name shadowing and redefinition errors") {
        val text = """
            main {
                ubyte var1Warn
                
                sub start() {
                    ubyte var1Warn
                    ubyte var1Warn
                    ubyte main 
                    ubyte start
                    ubyte outer         ; is ok
                    ubyte internalOk
                    ubyte internalOk    ; double defined
                }
                
                sub outer() {
                    ubyte var1Warn
                    ubyte internalOk
                }
            }
        """
        val errors = ErrorReporterForTests()
        compileText(C64Target(), false, text, outputDir, writeAssembly = false, errors = errors) shouldBe null
        /*
There are 4 errors and 3 warnings.
ERROR name conflict 'start', also defined...
ERROR name conflict 'var1Warn', also defined...
ERROR name conflict 'main', also defined...
ERROR name conflict 'internalOk', also defined...
WARN name 'var1Warn' shadows occurrence at...
WARN name 'var1Warn' shadows occurrence at...
WARN name 'var1Warn' shadows occurrence at...
         */
        errors.warnings.size shouldBe 3
        errors.warnings[0] shouldContain "var1Warn"
        errors.warnings[0] shouldContain "shadows"
        errors.warnings[0] shouldContain "line 3"
        errors.warnings[1] shouldContain "var1Warn"
        errors.warnings[1] shouldContain "shadows"
        errors.warnings[1] shouldContain "line 3"
        errors.warnings[2] shouldContain "var1Warn"
        errors.warnings[2] shouldContain "shadows"
        errors.warnings[2] shouldContain "line 3"
        errors.errors.size shouldBe 4
        errors.errors[0] shouldContain "name conflict"
        errors.errors[0] shouldContain "start"
        errors.errors[0] shouldContain "line 5"
        errors.errors[1] shouldContain "name conflict"
        errors.errors[1] shouldContain "var1Warn"
        errors.errors[1] shouldContain "line 6"
        errors.errors[2] shouldContain "name conflict"
        errors.errors[2] shouldContain "main"
        errors.errors[2] shouldContain "line 2"
        errors.errors[3] shouldContain "name conflict"
        errors.errors[3] shouldContain "internalOk"
        errors.errors[3] shouldContain "line 11"
    }

    test("ast node linkage and lookups ok even with no symbol prefixing") {
        val src="""
main {
    sub start() {
        thing.routine()
    }
}

thing {
    %option no_symbol_prefixing

    sub routine() {
        other.something()
        other.counter++
        other.asmsomething()
    }
}

other {
    sub something() {
    }

    asmsub asmsomething() {
        %asm {{
            nop
            rts
        }}
    }

    uword @shared counter
}

"""

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

})
