package prog8tests.codegeneration

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 prog8.code.ast.*
import prog8.code.target.C64Target
import prog8.code.target.VMTarget
import prog8tests.helpers.ErrorReporterForTests
import prog8tests.helpers.compileText
import kotlin.io.path.readText

class TestArrayThings: FunSpec({
    val outputDir = tempdir().toPath()
    
    test("assign prefix var to array should compile fine and is not split into inplace array modification") {
        val text = """
            main {
                sub start() {
                    byte[5] array
                    byte bb
                    array[1] = -bb
                }
            }
        """
        compileText(C64Target(), false, text, outputDir, writeAssembly = true) shouldNotBe null
        compileText(VMTarget(), false, text, outputDir, writeAssembly = true) shouldNotBe null
    }

    test("array in-place negation (integer types)") {
        val text = """
main {
  byte[10] foo
  ubyte[10] foou
  word[10] foow
  uword[10] foowu

  sub start() {
    foo[1] = 42
    foo[1] = -foo[1]

    foow[1] = 4242
    foow[1] = -foow[1]
  }
}"""
        compileText(C64Target(), false, text, outputDir, writeAssembly = true) shouldNotBe null
        compileText(VMTarget(), false, text, outputDir, writeAssembly = true) shouldNotBe null
    }

    test("array in-place negation (float type) vm target") {
        val text = """
%import floats

main {
  float[10] flt

  sub start() {
    flt[1] = 42.42
    flt[1] = -flt[1]
  }
}"""
        compileText(VMTarget(), false, text, outputDir, writeAssembly = true) shouldNotBe null
    }

    test("array in-place negation (float type) 6502 target") {
        val text = """
%import floats

main {
  float[10] flt

  sub start() {
    flt[1] = 42.42
    flt[1] = -flt[1]
  }
}"""
        compileText(C64Target(), false, text, outputDir, writeAssembly = true) shouldNotBe null
    }

    test("array in-place invert") {
        val text = """
main {
  ubyte[10] foo
  uword[10] foow

  sub start() {
    foo[1] = 42
    foo[1] = ~foo[1]

    foow[1] = 4242
    foow[1] = ~foow[1]
  }
}"""
        compileText(C64Target(), false, text, outputDir, writeAssembly = true) shouldNotBe null
        compileText(VMTarget(), false, text, outputDir, writeAssembly = true) shouldNotBe null
    }

    test("split word arrays in asm as lsb/msb, nosplit as single linear") {
        val text = """
main {
  uword[10] @shared uw
  word[10] @shared sw
  uword[10] @shared @nosplit normal

  sub start() {
    %asm {{
        lda  p8v_normal
        lda  p8v_uw_lsb
        lda  p8v_uw_msb
        lda  p8v_sw_lsb
        lda  p8v_sw_msb
    }}
  }
}"""
        compileText(C64Target(), false, text, outputDir, writeAssembly = true) shouldNotBe null
        compileText(VMTarget(), false, text, outputDir, writeAssembly = true) shouldNotBe null
    }

    test("array target with expression for index") {
        val text = """
main {
    sub start() {
        ubyte[] array = [1,2,3]
        array[cx16.r0L+1] += 42
        cx16.r0L = array[cx16.r0L+1]
    } 
}"""
        compileText(VMTarget(), false, text, outputDir, writeAssembly = true) shouldNotBe null
        compileText(C64Target(), false, text, outputDir, writeAssembly = true) shouldNotBe null
    }

    test("split array in zeropage is okay") {
        val text = """
main {
    sub start() {
        uword[3] @zp @shared thearray
        uword[3] @zp @nosplit @shared thearray2
    } 
}"""
        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 "thearray_lsb"
        assembly shouldContain "thearray_msb"
        assembly shouldContain "thearray2"
    }

    test("indexing str or pointervar with expression") {
        val text = """
main {
    sub start() {
        str name = "thing"
        modify(name)

        sub modify(str arg) {
            ubyte n=1
            uword pointervar
            arg[n+1] = arg[1]
            pointervar[n+1] = pointervar[1]
        }
    }
}"""
        compileText(C64Target(), false, text, outputDir, writeAssembly = true) shouldNotBe null
    }

    test("address of a uword pointer array expression") {
        val src="""
main {
    sub start() {
        set_state(12345, 1)
    }
    sub set_state(uword buffer, ubyte i) {
        uword addr = &buffer[i]
        addr++
    }
}"""
        compileText(C64Target(), false, src, outputDir, writeAssembly = true) shouldNotBe null
    }

    test("negative array index variables are not allowed, but ptr indexing is allowed") {
        val src="""
main {
    sub start() {
        uword @shared pointer
        ubyte[10] array
        str name = "hello"
        
        byte sindex
        
        array[sindex] = 10
        cx16.r0L = array[sindex]
        name[sindex] = 0
        cx16.r0L = name[sindex]
        pointer[sindex] = 10
        cx16.r0L=pointer[sindex]
    }
}"""
        val errors = ErrorReporterForTests()
        compileText(VMTarget(), false, src, outputDir, writeAssembly = false, errors = errors) shouldBe null
        errors.errors.size shouldBe 4
        errors.errors[0] shouldContain "signed variables"
        errors.errors[1] shouldContain "signed variables"
        errors.errors[2] shouldContain "signed variables"
        errors.errors[3] shouldContain "signed variables"
    }

    test("bounds checking for both positive and negative indexes, correct cases") {
        val src="""
main {
    sub start() {
        ubyte[10] array
        array[0] = 0
        array[9] = 0
        array[-1] = 0
        array[-10] = 0
    }
}"""
        compileText(VMTarget(), false, src, outputDir, writeAssembly = false) shouldNotBe null
    }

    test("bounds checking on strings, correct cases") {
        val src="""
main {
    sub start() {
        str name = "1234567890"
        name[0] = 0
        name[9] = 0
    }
}"""
        compileText(VMTarget(), false, src, outputDir, writeAssembly = false) shouldNotBe null
    }

    test("bounds checking for positive indexes, invalid case") {
        val src="""
main {
    sub start() {
        ubyte[10] array
        array[10] = 0
    }
}"""
        val errors = ErrorReporterForTests()
        compileText(VMTarget(), false, src, outputDir, writeAssembly = false, errors = errors) shouldBe null
        errors.errors.size shouldBe 1
        errors.errors[0] shouldContain "out of bounds"
    }

    test("bounds checking for negative indexes, invalid case") {
        val src="""
main {
    sub start() {
        ubyte[10] array
        array[-11] = 0
    }
}"""
        val errors = ErrorReporterForTests()
        compileText(VMTarget(), false, src, outputDir, writeAssembly = false, errors = errors) shouldBe null
        errors.errors.size shouldBe 1
        errors.errors[0] shouldContain "out of bounds"
    }

    test("bounds checking on strings invalid cases") {
        val src="""
main {
    sub start() {
        str name = "1234567890"
        name[10] = 0
        name[-1] = 0
        name[-11] = 0
    }
}"""
        val errors = ErrorReporterForTests()
        compileText(VMTarget(), false, src, outputDir, writeAssembly = false, errors = errors) shouldBe null
        errors.errors.size shouldBe 3
        errors.errors[0] shouldContain "out of bounds"
        errors.errors[1] shouldContain "out of bounds"
        errors.errors[2] shouldContain "out of bounds"
    }

    test("array and string initializer with multiplication") {
        val src="""
%option enable_floats

main {
    sub start() {
        str name = "xyz" * 3
        bool[3] boolarray   = [true] * 3
        ubyte[3] bytearray  = [42] * 3
        uword[3] wordarray  = [5555] * 3
        float[3] floatarray = [123.45] * 3
    }
}"""
        val result = compileText(C64Target(), false, src, outputDir, writeAssembly = true)!!
        val x = result.codegenAst!!.entrypoint()!!
        x.children.size shouldBe 7
        ((x.children[1] as PtVariable).value as PtString).value shouldBe "xyzxyzxyz"
        val array1 = (x.children[2] as PtVariable).value as PtArray
        val array2 = (x.children[3] as PtVariable).value as PtArray
        val array3 = (x.children[4] as PtVariable).value as PtArray
        val array4 = (x.children[5] as PtVariable).value as PtArray
        array1.children.map { (it as PtBool).value } shouldBe listOf(true, true, true)
        array2.children.map { (it as PtNumber).number } shouldBe listOf(42, 42, 42)
        array3.children.map { (it as PtNumber).number } shouldBe listOf(5555, 5555, 5555)
        array4.children.map { (it as PtNumber).number } shouldBe listOf(123.45, 123.45, 123.45)
    }

    test("array initializer with range") {
        val src="""
%option enable_floats

main {
    sub start() {
        ubyte[3] bytearray2 = 10 to 12
        uword[3] wordarray2 = 5000 to 5002
        float[3] floatarray2 = 100 to 102
    }
}"""
        val result = compileText(C64Target(), false, src, outputDir, writeAssembly = true)!!
        val x = result.codegenAst!!.entrypoint()!!
        x.children.size shouldBe 5
        val array1 = (x.children[1] as PtVariable).value as PtArray
        val array2 = (x.children[2] as PtVariable).value as PtArray
        val array3 = (x.children[3] as PtVariable).value as PtArray
        array1.children.map { (it as PtNumber).number } shouldBe listOf(10, 11, 12)
        array2.children.map { (it as PtNumber).number } shouldBe listOf(5000, 5001, 5002)
        array3.children.map { (it as PtNumber).number } shouldBe listOf(100, 101, 102)
    }

    test("identifiers in array literals getting implicit address-of") {
        val src="""
main {
    sub start() {
label:
        str @shared name = "name"
        uword[] @shared array1 = [name, label, start, main]
        uword[] @shared array2 = [&name, &label, &start, &main]
    }
}"""
        val result = compileText(C64Target(), false, src, outputDir, writeAssembly = true)!!
        val x = result.codegenAst!!.entrypoint()!!
        x.children.size shouldBe 6
        val array1 = (x.children[2] as PtVariable).value as PtArray
        val array2 = (x.children[3] as PtVariable).value as PtArray
        array1.children.forEach {
            it shouldBe instanceOf<PtAddressOf>()
        }
        array2.children.forEach {
            it shouldBe instanceOf<PtAddressOf>()
        }
    }

    test("variable identifiers in array literals not getting implicit address-of") {
        val src="""
main {
    sub start() {
label:
        str @shared name = "name"
        ubyte @shared bytevar
        uword[] @shared array1 = [cx16.r0]  ; error, is variables
        uword[] @shared array2 = [bytevar]  ; error, is variables
    }
}"""
        val errors = ErrorReporterForTests()
        compileText(C64Target(), false, src, outputDir, writeAssembly = true, errors=errors) shouldBe null
        errors.errors.size shouldBe 2
        errors.errors[0] shouldContain "contains non-constant"
        errors.errors[1] shouldContain "contains non-constant"
    }

    test("memsizing in codegen of array return values") {
        val src="""
main {
    sub start() {
        cx16.r1 = give_array1()
        cx16.r2 = give_array2()
    }

    sub give_array1() -> uword {
        return [1,2,3,4]
    }
    sub give_array2() -> uword {
        return [1000,2000,3000,4000]
    }
}"""
        compileText(VMTarget(), false, src, outputDir, writeAssembly = true) shouldNotBe null
        compileText(C64Target(), false, src, outputDir, writeAssembly = true) shouldNotBe null
    }

    test("taking address of split arrays works") {
        val src="""
main {
    sub start() {
        cx16.r0L=0
        if cx16.r0L==0 {
            uword[] addresses = [scores2, start]
            uword[] scores1 = [10, 25, 50, 100]
            uword[] scores2 = [100, 250, 500, 1000]

            cx16.r0 = &scores1
            cx16.r1 = &scores2
            cx16.r2 = &addresses
        }
    }
}"""
        val errors = ErrorReporterForTests(keepMessagesAfterReporting = true)
        compileText(C64Target(), optimize=false, src, outputDir, writeAssembly=true, errors=errors) shouldNotBe null
        errors.errors.size shouldBe 0
        errors.warnings.size shouldBe 0
    }

    test("various array indexed assignment scenarios") {
        val src="""
%import floats
%import textio
%zeropage basicsafe

main {
    bool[10] barray
    uword[10] @nosplit warrayns
    uword[10] warray
    float[10] farray

    sub start() {
        dump()

        ; ALL OK
        barray[2] = true
        warrayns[2] = 1234
        warray[2] = 5678
        farray[2] = 3.1415
        dump()

        ; ALL OK
        cx16.r0L=2
        barray[cx16.r0L] = false
        warrayns[cx16.r0L] = 0
        warray[cx16.r0L] = 0
        farray[cx16.r0L] = 0
        dump()

        ; ALL OK
        cx16.r0L=2
        barray[cx16.r0L] = true
        warrayns[cx16.r0L] = 1234
        warray[cx16.r0L] = 5678
        farray[cx16.r0L] = 3.1415
        dump()

        ; ALL OK
        barray[2] = false
        warrayns[2] = 0
        warray[2] = 0
        farray[2] = 0.0
        dump()

        sub dump() {
            txt.print_bool(barray[2])
            txt.spc()
            txt.print_uw(warrayns[2])
            txt.spc()
            txt.print_uw(warray[2])
            txt.spc()
            txt.print_f(farray[2])
            txt.nl()
        }
    }
}"""
        compileText(C64Target(), optimize=true, src, outputDir) shouldNotBe null
        compileText(VMTarget(), optimize=true, src, outputDir) shouldNotBe null
        compileText(C64Target(), optimize=false, src, outputDir) shouldNotBe null
        compileText(VMTarget(), optimize=false, src, outputDir) shouldNotBe null
    }
})

