# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.
"""Lowest level testing VM. Test execbuilder and execution."""

import numpy as np
import pytest
import tvm_ffi

import tvm
from tvm import relax
from tvm.relax.testing.vm import check_saved_func
from tvm.script import relax as R


def test_vm_execute():
    ib = relax.ExecBuilder()
    with ib.function("func0", num_inputs=2):
        ib.emit_call("test.vm.add", args=[ib.r(0), ib.r(1)], dst=ib.r(2))
        ib.emit_ret(ib.r(2))
    ex = ib.get()
    vm = relax.VirtualMachine(ex, tvm.cpu())
    a = tvm.runtime.tensor(
        np.random.rand(
            4,
        )
    )
    b = tvm.runtime.tensor(
        np.random.rand(
            4,
        )
    )

    add_res = check_saved_func(vm, "func0", a, b)
    tvm.testing.assert_allclose(add_res.numpy(), a.numpy() + b.numpy(), rtol=1e-7, atol=1e-7)


def test_vm_multiple_func():
    ib = relax.ExecBuilder()
    with ib.function("func0", num_inputs=2):
        ib.emit_call("test.vm.add", args=[ib.r(0), ib.r(1)], dst=ib.r(2))
        ib.emit_ret(ib.r(2))
    with ib.function("func1", num_inputs=2):
        ib.emit_call("test.vm.mul", args=[ib.r(0), ib.r(1)], dst=ib.r(2))
        ib.emit_ret(ib.r(2))
    ex = ib.get()
    vm = relax.VirtualMachine(ex, tvm.cpu())
    a = tvm.runtime.tensor(
        np.random.rand(
            4,
        )
    )
    b = tvm.runtime.tensor(
        np.random.rand(
            4,
        )
    )
    mul_res = check_saved_func(vm, "func1", a, b)
    add_res = check_saved_func(vm, "func0", a, b)
    tvm.testing.assert_allclose(add_res.numpy(), a.numpy() + b.numpy(), rtol=1e-7, atol=1e-7)
    tvm.testing.assert_allclose(mul_res.numpy(), a.numpy() * b.numpy(), rtol=1e-7, atol=1e-7)


def test_vm_checker():
    ib = relax.ExecBuilder()
    with pytest.raises(RuntimeError):
        with ib.function("func0", num_inputs=2):
            ib.emit_call("test.vm.add", args=[ib.r(0), ib.r(2)], dst=ib.r(2))
            ib.emit_ret(ib.r(2))
        ib.get()


def test_neg_imm():
    ib = relax.ExecBuilder()

    with ib.function("func0", num_inputs=1):
        ib.emit_call("test.vm.add_scalar", args=[ib.imm(-3), ib.r(0)], dst=ib.r(1))
        ib.emit_ret(ib.r(1))
    ib.get()

    ex = ib.get()
    vm = relax.VirtualMachine(ex, tvm.cpu())
    assert vm["func0"](1) == -2
    assert vm["func0"](-3) == -6


def test_emit_cache():
    ib = relax.ExecBuilder()

    with ib.function("func0", num_inputs=1):
        x0 = ib.convert_constant("str0")
        x1 = ib.convert_constant("str0")
        # cache constant str
        assert x0 == x1
        s0 = ib.convert_constant(tvm_ffi.Shape([1, 2]))
        s1 = ib.convert_constant(tvm_ffi.Shape([1, 2]))
        s2 = ib.convert_constant(tvm_ffi.Shape([1, 3]))
        assert s0 == s1
        assert s1 != s2
        y0 = ib.convert_constant(tvm.runtime.tensor(np.array([1, 2, 3]).astype("int32")))
        y1 = ib.convert_constant(tvm.runtime.tensor(np.array([1, 2, 3]).astype("int32")))
        assert y0 == y1
        ib.emit_ret(ib.r(0))


def test_vm_formalize():
    ib0 = relax.ExecBuilder()
    ib1 = relax.ExecBuilder()
    with ib0.function("func0", num_inputs=2):
        ib0.emit_call("test.vm.add", args=[ib0.r(0), ib0.r(1)], dst=ib0.r(100))
        ib0.emit_call("test.vm.mul", args=[ib0.r(1), ib0.r(100)], dst=ib0.r(50))
        ib0.emit_ret(ib0.r(50))
    with ib1.function("func0", num_inputs=2):
        ib1.emit_call("test.vm.add", args=[ib1.r(0), ib1.r(1)], dst=ib1.r(2))
        ib1.emit_call("test.vm.mul", args=[ib1.r(1), ib1.r(2)], dst=ib1.r(3))
        ib1.emit_ret(ib1.r(3))
    exec0 = ib0.get()
    exec1 = ib1.get()
    assert exec0.as_text() == exec1.as_text()


def test_vm_operand():
    ib0 = relax.ExecBuilder()
    with ib0.function("func0", num_inputs=2):
        ib0.emit_call("test.vm.add_scalar", args=[ib0.r(0), ib0.r(1)], dst=ib0.r(2))
        ib0.emit_ret(ib0.r(2))
    exec0 = ib0.get()
    vm = relax.VirtualMachine(exec0, tvm.cpu())
    res = vm["func0"](2, 3)
    assert res == 5

    ib1 = relax.ExecBuilder()
    with ib1.function("func1", num_inputs=1):
        ib1.emit_call("test.vm.get_device_id", args=[ib1.r(0)], dst=ib1.r(1))
        ib1.emit_ret(ib1.r(1))
    exec1 = ib1.get()
    vm = relax.VirtualMachine(exec1, tvm.cpu())
    res = vm["func1"](tvm.cpu(3))
    assert res == 3


def test_vm_shapeof():
    ib = relax.ExecBuilder()
    shape = (32, 16)
    arr = tvm.runtime.tensor(np.random.rand(*shape))
    with ib.function("main", num_inputs=0):
        ib.emit_call("vm.builtin.shape_of", args=[arr], dst=ib.r(0))
        ib.emit_ret(ib.r(0))
    ex = ib.get()
    vm = relax.VirtualMachine(ex, tvm.cpu())
    res = vm["main"]()
    for i, s in enumerate(res):
        assert s == shape[i]


def test_vm_storage():
    dtype = tvm.DataType("float32")
    shape = (4, 6)
    ib = relax.ExecBuilder()
    with ib.function("main", num_inputs=0):
        ib.emit_call(
            "vm.builtin.alloc_storage",
            args=[
                ib.vm_state(),
                (24,),
                ib.convert_constant(0),
                dtype,
                ib.convert_constant("global"),
            ],
            dst=ib.r(1),
        )
        ib.emit_call(
            "vm.builtin.alloc_tensor", args=[ib.r(1), ib.imm(0), shape, dtype], dst=ib.r(2)
        )
        ib.emit_ret(ib.r(2))
    ex = ib.get()
    vm = relax.VirtualMachine(ex, tvm.cpu())
    res = vm["main"]()
    assert res.device == tvm.cpu()
    assert res.shape == shape


def test_vm_goto():
    ib = relax.ExecBuilder()
    with ib.function("main", num_inputs=2):
        ib.emit_call("test.vm.add", args=[ib.r(0), ib.r(1)], dst=ib.r(2))
        ib.emit_goto(2)
        ib.emit_call("test.vm.mul", args=[ib.r(2), ib.r(1)], dst=ib.r(2))
        ib.emit_ret(ib.r(2))
    ex = ib.get()
    vm = relax.VirtualMachine(ex, tvm.cpu())
    a = tvm.runtime.tensor(
        np.random.rand(
            4,
        )
    )
    b = tvm.runtime.tensor(
        np.random.rand(
            4,
        )
    )
    res = check_saved_func(vm, "main", a, b)
    tvm.testing.assert_allclose(res.numpy(), a.numpy() + b.numpy(), rtol=1e-7, atol=1e-7)


def test_vm_if():
    ib = relax.ExecBuilder()
    with ib.function("main", num_inputs=3):
        ib.emit_if(ib.r(0), 3)
        ib.emit_call("test.vm.add", args=[ib.r(1), ib.r(2)], dst=ib.r(3))
        ib.emit_goto(2)
        ib.emit_call("test.vm.mul", args=[ib.r(1), ib.r(2)], dst=ib.r(3))
        ib.emit_ret(ib.r(3))
    ex = ib.get()
    vm = relax.VirtualMachine(ex, tvm.cpu())
    a = tvm.runtime.tensor(
        np.random.rand(
            4,
        )
    )
    b = tvm.runtime.tensor(
        np.random.rand(
            4,
        )
    )
    res = vm["main"](0, a, b)
    tvm.testing.assert_allclose(res.numpy(), a.numpy() * b.numpy(), rtol=1e-7, atol=1e-7)
    res = vm["main"](1, a, b)
    tvm.testing.assert_allclose(res.numpy(), a.numpy() + b.numpy(), rtol=1e-7, atol=1e-7)


def test_vm_invoke_closure():
    ib = relax.ExecBuilder()
    with ib.function("lifted_func_1", num_inputs=4):
        ib.emit_call("test.vm.add", args=[ib.r(0), ib.r(1)], dst=ib.r(4))
        ib.emit_call("test.vm.add", args=[ib.r(2), ib.r(4)], dst=ib.r(5))
        ib.emit_call("test.vm.add", args=[ib.r(3), ib.r(5)], dst=ib.r(6))
        ib.emit_ret(ib.r(6))
    with ib.function("main", num_inputs=2):
        ib.emit_call(
            "vm.builtin.make_closure", args=[ib.f("lifted_func_1"), ib.r(0), ib.r(1)], dst=ib.r(2)
        )
        ib.emit_ret(ib.r(2))

    ex = ib.get()
    vm = relax.VirtualMachine(ex, tvm.cpu())
    w_inp = tvm.runtime.tensor(np.random.rand(2, 3))
    x_inp = tvm.runtime.tensor(np.random.rand(2, 3))
    y_inp = tvm.runtime.tensor([[3.1, 4.0, 5.0], [6.0, 7.1, 9.0]])
    z_inp = tvm.runtime.tensor(np.random.rand(2, 3))
    clo = vm["main"](w_inp, x_inp)
    res = vm.invoke_closure(clo, y_inp, z_inp)
    tvm.testing.assert_allclose(
        res.numpy(), w_inp.numpy() + x_inp.numpy() + y_inp.numpy() + z_inp.numpy()
    )


def test_vm_stack_restore_after_failure():
    @tvm.script.ir_module
    class Module:
        @R.function
        def main(inp: R.Tensor((10, 10), dtype="float32")) -> R.Tensor((10, 10), dtype="float32"):
            with R.dataflow():
                lv: R.Tensor((10, 10), dtype="float32") = R.multiply(inp, R.const(2, "float32"))
                gv: R.Tensor((10, 10), dtype="float32") = lv
                R.output(gv)
            return gv

    ex = tvm.compile(Module, "llvm")
    vm = relax.VirtualMachine(ex, tvm.cpu())

    correct_input = tvm.runtime.tensor(np.random.normal(size=(10, 10)).astype("float32"))
    incorrect_input = tvm.runtime.tensor(np.random.normal(size=(12, 10)).astype("float32"))

    try:
        vm["main"](incorrect_input)
    except RuntimeError:
        pass

    # VM should executes correctly after encountered incorrect shape in previous invocation
    vm["main"](correct_input)


if __name__ == "__main__":
    tvm.testing.main()
