package prog8tests.compiler

import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.style.FunSpec
import io.kotest.engine.spec.tempdir
import io.kotest.matchers.doubles.plusOrMinus
import io.kotest.matchers.shouldBe
import io.kotest.matchers.shouldNotBe
import io.kotest.matchers.string.shouldContain
import prog8.code.core.InternalCompilerException
import prog8.code.core.toHex
import prog8.code.target.C64Target
import prog8.code.target.Mflpt5
import prog8tests.helpers.ErrorReporterForTests
import prog8tests.helpers.compileText


class TestNumbers: FunSpec({

    val outputDir = tempdir().toPath()

    test("testToHex") {
        0.toHex() shouldBe "0"
        1.toHex() shouldBe "1"
        1.234.toHex() shouldBe "1"
        10.toHex() shouldBe "10"
        10.99.toHex() shouldBe "10"
        15.toHex() shouldBe "15"
        16.toHex() shouldBe "\$10"
        255.toHex() shouldBe "\$ff"
        256.toHex() shouldBe "\$0100"
        20060.toHex() shouldBe "\$4e5c"
        50050.toHex() shouldBe "\$c382"
        65535.toHex() shouldBe "\$ffff"
        65535L.toHex() shouldBe "\$ffff"
        0.toHex() shouldBe "0"
        (-1).toHex() shouldBe "-1"
        (-1.234).toHex() shouldBe "-1"
        (-10).toHex() shouldBe "-10"
        (-10.99).toHex() shouldBe "-10"
        (-15).toHex() shouldBe "-15"
        (-16).toHex() shouldBe "-\$10"
        (-255).toHex() shouldBe "-\$ff"
        (-256).toHex() shouldBe "-\$0100"
        (-20060).toHex() shouldBe "-\$4e5c"
        (-50050).toHex() shouldBe "-\$c382"
        (-65535).toHex() shouldBe "-\$ffff"
        (-65535L).toHex() shouldBe "-\$ffff"
        (65536).toHex() shouldBe "\$00010000"
        (-65536).toHex() shouldBe "-\$00010000"
    }

    test("testFloatToMflpt5") {
        Mflpt5.fromNumber(0) shouldBe Mflpt5(0x00u, 0x00u, 0x00u, 0x00u, 0x00u)
        Mflpt5.fromNumber(3.141592653) shouldBe Mflpt5(0x82u, 0x49u, 0x0Fu, 0xDAu, 0xA1u)
        Mflpt5.fromNumber(3.141592653589793) shouldBe Mflpt5(0x82u, 0x49u, 0x0Fu, 0xDAu, 0xA2u)
        Mflpt5.fromNumber(32768) shouldBe Mflpt5(0x90u, 0x00u, 0x00u, 0x00u, 0x00u)
        Mflpt5.fromNumber(-32768) shouldBe Mflpt5(0x90u, 0x80u, 0x00u, 0x00u, 0x00u)
        Mflpt5.fromNumber(1) shouldBe Mflpt5(0x81u, 0x00u, 0x00u, 0x00u, 0x00u)
        Mflpt5.fromNumber(0.7071067812) shouldBe Mflpt5(0x80u, 0x35u, 0x04u, 0xF3u, 0x34u)
        Mflpt5.fromNumber(0.7071067811865476) shouldBe Mflpt5(0x80u, 0x35u, 0x04u, 0xF3u, 0x33u)
        Mflpt5.fromNumber(1.4142135624) shouldBe Mflpt5(0x81u, 0x35u, 0x04u, 0xF3u, 0x34u)
        Mflpt5.fromNumber(1.4142135623730951) shouldBe Mflpt5(0x81u, 0x35u, 0x04u, 0xF3u, 0x33u)
        Mflpt5.fromNumber(-.5) shouldBe Mflpt5(0x80u, 0x80u, 0x00u, 0x00u, 0x00u)
        Mflpt5.fromNumber(0.69314718061) shouldBe Mflpt5(0x80u, 0x31u, 0x72u, 0x17u, 0xF8u)
        Mflpt5.fromNumber(0.6931471805599453) shouldBe Mflpt5(0x80u, 0x31u, 0x72u, 0x17u, 0xF7u)
        Mflpt5.fromNumber(10) shouldBe Mflpt5(0x84u, 0x20u, 0x00u, 0x00u, 0x00u)
        Mflpt5.fromNumber(1000000000) shouldBe Mflpt5(0x9Eu, 0x6Eu, 0x6Bu, 0x28u, 0x00u)
        Mflpt5.fromNumber(.5) shouldBe Mflpt5(0x80u, 0x00u, 0x00u, 0x00u, 0x00u)
        Mflpt5.fromNumber(1.4426950408889634) shouldBe Mflpt5(0x81u, 0x38u, 0xAAu, 0x3Bu, 0x29u)
        Mflpt5.fromNumber(1.5707963267948966) shouldBe Mflpt5(0x81u, 0x49u, 0x0Fu, 0xDAu, 0xA2u)
        Mflpt5.fromNumber(6.283185307179586) shouldBe Mflpt5(0x83u, 0x49u, 0x0Fu, 0xDAu, 0xA2u)
        Mflpt5.fromNumber(.25) shouldBe Mflpt5(0x7Fu, 0x00u, 0x00u, 0x00u, 0x00u)
        Mflpt5.fromNumber(123.45678e22) shouldBe Mflpt5(0xd1u, 0x02u, 0xb7u, 0x06u, 0xfbu)
        Mflpt5.fromNumber(-123.45678e-22) shouldBe Mflpt5(0x3eu, 0xe9u, 0x34u, 0x09u, 0x1bu)
    }

    test("testFloatRange") {
        Mflpt5.fromNumber(Mflpt5.FLOAT_MAX_POSITIVE) shouldBe Mflpt5(0xffu, 0x7fu, 0xffu, 0xffu, 0xffu)
        Mflpt5.fromNumber(Mflpt5.FLOAT_MAX_NEGATIVE) shouldBe Mflpt5(0xffu, 0xffu, 0xffu, 0xffu, 0xffu)
        Mflpt5.fromNumber(1.7e-38) shouldBe Mflpt5(0x03u, 0x39u, 0x1du, 0x15u, 0x63u)
        Mflpt5.fromNumber(1.7e-39) shouldBe Mflpt5(0x00u, 0x00u, 0x00u, 0x00u, 0x00u)
        Mflpt5.fromNumber(-1.7e-38) shouldBe Mflpt5(0x03u, 0xb9u, 0x1du, 0x15u, 0x63u)
        Mflpt5.fromNumber(-1.7e-39) shouldBe Mflpt5(0x00u, 0x00u, 0x00u, 0x00u, 0x00u)
        shouldThrow<InternalCompilerException> { Mflpt5.fromNumber(1.7014118346e+38) }
        shouldThrow<InternalCompilerException> { Mflpt5.fromNumber(-1.7014118346e+38) }
        shouldThrow<InternalCompilerException> { Mflpt5.fromNumber(1.7014118347e+38) }
        shouldThrow<InternalCompilerException> { Mflpt5.fromNumber(-1.7014118347e+38) }
    }

    test("testMflpt5ToFloat") {
        val epsilon=0.000000001

        Mflpt5(0x00u, 0x00u, 0x00u, 0x00u, 0x00u).toDouble() shouldBe 0.0
        Mflpt5(0x82u, 0x49u, 0x0Fu, 0xDAu, 0xA1u).toDouble() shouldBe(3.141592653 plusOrMinus epsilon)
        Mflpt5(0x82u, 0x49u, 0x0Fu, 0xDAu, 0xA2u).toDouble() shouldBe(3.141592653589793 plusOrMinus epsilon)
        Mflpt5(0x90u, 0x00u, 0x00u, 0x00u, 0x00u).toDouble() shouldBe 32768.0
        Mflpt5(0x90u, 0x80u, 0x00u, 0x00u, 0x00u).toDouble() shouldBe -32768.0
        Mflpt5(0x81u, 0x00u, 0x00u, 0x00u, 0x00u).toDouble() shouldBe 1.0
        Mflpt5(0x80u, 0x35u, 0x04u, 0xF3u, 0x34u).toDouble() shouldBe(0.7071067812 plusOrMinus epsilon)
        Mflpt5(0x80u, 0x35u, 0x04u, 0xF3u, 0x33u).toDouble() shouldBe(0.7071067811865476 plusOrMinus epsilon)
        Mflpt5(0x81u, 0x35u, 0x04u, 0xF3u, 0x34u).toDouble() shouldBe(1.4142135624 plusOrMinus epsilon)
        Mflpt5(0x81u, 0x35u, 0x04u, 0xF3u, 0x33u).toDouble() shouldBe(1.4142135623730951 plusOrMinus epsilon)
        Mflpt5(0x80u, 0x80u, 0x00u, 0x00u, 0x00u).toDouble() shouldBe -.5
        Mflpt5(0x80u, 0x31u, 0x72u, 0x17u, 0xF8u).toDouble() shouldBe(0.69314718061 plusOrMinus epsilon)
        Mflpt5(0x80u, 0x31u, 0x72u, 0x17u, 0xF7u).toDouble() shouldBe(0.6931471805599453 plusOrMinus epsilon)
        Mflpt5(0x84u, 0x20u, 0x00u, 0x00u, 0x00u).toDouble() shouldBe 10.0
        Mflpt5(0x9Eu, 0x6Eu, 0x6Bu, 0x28u, 0x00u).toDouble() shouldBe 1000000000.0
        Mflpt5(0x80u, 0x00u, 0x00u, 0x00u, 0x00u).toDouble() shouldBe .5
        Mflpt5(0x81u, 0x38u, 0xAAu, 0x3Bu, 0x29u).toDouble() shouldBe(1.4426950408889634 plusOrMinus epsilon)
        Mflpt5(0x81u, 0x49u, 0x0Fu, 0xDAu, 0xA2u).toDouble() shouldBe(1.5707963267948966 plusOrMinus epsilon)
        Mflpt5(0x83u, 0x49u, 0x0Fu, 0xDAu, 0xA2u).toDouble() shouldBe(6.283185307179586 plusOrMinus epsilon)
        Mflpt5(0x7Fu, 0x00u, 0x00u, 0x00u, 0x00u).toDouble() shouldBe .25
        Mflpt5(0xd1u, 0x02u, 0xb7u, 0x06u, 0xfbu).toDouble() shouldBe(123.45678e22 plusOrMinus 1.0e15)
        Mflpt5(0x3eu, 0xe9u, 0x34u, 0x09u, 0x1bu).toDouble() shouldBe(-123.45678e-22 plusOrMinus epsilon)
    }

    test("implicit float conversion warning if enabled") {
        val src="""
            %option enable_floats
            main {
                sub start() {
                    uword xx = 10
                    if xx+99 == 1.23456
                        xx++
                    if xx+99 == 1234567.8
                        xx++
                }
            }
        """
        val errors = ErrorReporterForTests(keepMessagesAfterReporting = true)
        compileText(C64Target(), true, src, outputDir, writeAssembly = false, errors=errors) shouldNotBe null
        errors.errors.size shouldBe 0
        errors.infos.size shouldBe 2
        errors.infos[0] shouldContain "converted to float"
        errors.infos[1] shouldContain "converted to float"
    }

    test("implicit float conversion error if not enabled") {
        val src="""
            main {
                sub start() {
                    uword xx = 10
                    if xx+99 == 1.23456
                        xx++
                    if xx+99 == 1234567.8
                        xx++
                }
            }
        """
        val errors = ErrorReporterForTests()
        compileText(C64Target(), true, src, outputDir, writeAssembly = false, errors=errors) shouldBe null
        errors.errors.size shouldBe 2
        errors.warnings.size shouldBe 0
        errors.errors[0] shouldContain "converted to float"
        errors.errors[1] shouldContain "converted to float"
    }

    test("out of range number assignments") {
        val src="""
            main {
                sub start() {
                    uword @shared qq = ${'$'}2ff33
                    cx16.r0 = ${'$'}1fc0f
                    cx16.r0L = 1234
                }
            }
        """
        val errors = ErrorReporterForTests()
        compileText(C64Target(), true, src, outputDir, writeAssembly = false, errors=errors) shouldBe null
        errors.errors.size shouldBe 6
        errors.warnings.size shouldBe 0
        errors.errors[0] shouldContain "out of range"
        errors.errors[1] shouldContain "doesn't match"
        errors.errors[2] shouldContain "out of range"
        errors.errors[3] shouldContain "doesn't match"
        errors.errors[4] shouldContain "out of range"
        errors.errors[5] shouldContain "cannot assign word to byte"
    }

    test("large numeric literals still ok if actual value is small") {
        val src="""
            main {
                sub start() {
                    cx16.r1L = %000000000001
                    cx16.r2L = ${'$'}000000000001
                }
            }
        """
        val errors = ErrorReporterForTests()
        compileText(C64Target(), true, src, outputDir, writeAssembly = false, errors=errors) shouldNotBe null
    }

    test("big numbers okay in const expressions if result fits") {
        val src="""
            main {
                sub start() {
                    uword @shared qq = ${'$'}2ff33 >> 4
                    cx16.r0 = ${'$'}1fc0f >> 4
                }
            }
        """
        compileText(C64Target(), true, src, outputDir, writeAssembly = false) shouldNotBe null
    }

    test("signed negative numbers not implicitly cast to unsigned") {
        val src="""
            main {
                sub start() {
                    uword uw1 = -32768
                    uword uw = -1
                    ubyte ub = -1
                    uw = -2 as uword
                    ub = -2 as ubyte 
                }
            }
        """
        val errors = ErrorReporterForTests()
        compileText(C64Target(), false, src, outputDir, writeAssembly = false, errors=errors) shouldBe null
        errors.errors.size shouldBe 6
        errors.errors[0] shouldContain "out of range"
        errors.errors[1] shouldContain "word doesn't match"
        errors.errors[2] shouldContain "out of range"
        errors.errors[3] shouldContain "byte doesn't match"
        errors.errors[4] shouldContain "out of range"
        errors.errors[5] shouldContain "byte doesn't match"
    }
})
