# 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.
# ruff: noqa: E501, F841

import sys
import tempfile

import numpy as np
import pytest
import tvm_ffi

import tvm
import tvm.testing
from tvm import relax
from tvm.script import ir as I
from tvm.script import relax as R
from tvm.script import tirx as T

exec_mode = tvm.testing.parameter("bytecode", "compiled")


@tvm.script.ir_module
class InputModule:
    @R.function
    def foo(x: R.Tensor(("m", "n"), "int64")):
        y = R.unique(x, sorted=False)
        y_sorted = R.unique(x)
        return y, y_sorted


def run_cpu(mod, func_name, *args, exec_mode):
    if isinstance(mod, relax.Function):
        func = mod
        args = [func_name, *args]
        func_name = func.attrs["global_symbol"]
        mod = tvm.IRModule.from_expr(func)

    target = tvm.target.Target("llvm")
    ex = relax.build(mod, target, exec_mode=exec_mode)
    vm = relax.VirtualMachine(ex, tvm.cpu())

    return vm[func_name](*args)


def test_unique(exec_mode):
    # TODO(prakalp): also add test for compiling and running on CUDA device.
    data_numpy = np.random.randint(0, 16, (16, 16))
    data = tvm.runtime.tensor(data_numpy)
    result, result_sorted = run_cpu(InputModule, "foo", data, exec_mode=exec_mode)

    expected_output_sorted, indices = np.unique(data_numpy, return_index=True)
    expected_output = [data_numpy.flatten()[index] for index in sorted(indices)]

    np.testing.assert_array_equal(expected_output_sorted, result_sorted.numpy())
    np.testing.assert_array_equal(expected_output, result.numpy())


@tvm.script.ir_module
class PrintTest:
    @R.function(pure=False)
    def foo(x: R.Tensor((), "int32")):
        # results have to be bound, but we don't use them
        # TODO: We should allow calls whose results are not bound for side effects;
        #       it would be easy syntactic sugar to add.
        p1 = R.print(x)
        p2 = R.print(x, format="Number: {}")
        t = (x, x)
        p3 = R.print(t, format="Tuple: {}")
        p4 = R.print(x, t)
        p5 = R.print(x, x, format="Custom print: {} {}")
        p6 = R.print(x, t, format="Another print: {} {}")
        return x


def test_print(exec_mode):
    try:
        stdout = sys.stdout
        with tempfile.TemporaryFile(mode="w+") as test_out:
            sys.stdout = test_out
            run_cpu(
                PrintTest,
                "foo",
                tvm.runtime.tensor(np.array(1).astype("int32")),
                exec_mode=exec_mode,
            )
            test_out.seek(0)
            printed_text = str(test_out.read())
            expected = "1\nNumber: 1\nTuple: (1, 1)\n1 (1, 1)\nCustom print: 1 1\nAnother print: 1 (1, 1)\n"
            assert printed_text in expected, ("printed_text is ", printed_text)
    finally:
        sys.stdout = stdout


def test_assert_passes(exec_mode):
    @R.function(pure=False)
    def func(x: R.Tensor((), "int32")):
        _ = R.assert_op(relax.const(True))
        return x

    run_cpu(func, tvm.runtime.tensor(np.array(1).astype("int32")), exec_mode=exec_mode)


def test_assert_passes_with_format_args(exec_mode):
    @R.function(pure=False)
    def func(x: R.Tensor((), "int32")):
        _ = R.assert_op(relax.const(True), x, format="You won't see me")
        return x

    run_cpu(func, tvm.runtime.tensor(np.array(1).astype("int32")), exec_mode=exec_mode)


def test_assert_fails(exec_mode):
    @R.function(pure=False)
    def func(x: R.Tensor((), "int32")):
        _ = R.assert_op(relax.const(False))
        return x

    with pytest.raises(AssertionError, match="Assertion Failed"):
        run_cpu(func, tvm.runtime.tensor(np.array(1).astype("int32")), exec_mode=exec_mode)


def test_assert_fails_with_message(exec_mode):
    @R.function(pure=False)
    def func(x: R.Tensor((), "int32")):
        _ = R.assert_op(relax.const(False), format="I failed...")
        return x

    with pytest.raises(AssertionError, match="I failed..."):
        run_cpu(func, tvm.runtime.tensor(np.array(1).astype("int32")), exec_mode=exec_mode)


def test_assert_fails_with_args(exec_mode):
    @R.function(pure=False)
    def func(x: R.Tensor((), "int32")):
        _ = R.assert_op(relax.const(False), [x, x])
        return x

    with pytest.raises(AssertionError, match="5, 5"):
        run_cpu(func, tvm.runtime.tensor(np.array(5).astype("int32")), exec_mode=exec_mode)


def test_assert_fails_with_formatted_args(exec_mode):
    @R.function(pure=False)
    def func(x: R.Tensor((), "int32")):
        _ = R.assert_op(relax.const(False), x, format="Number: {}")
        return x

    with pytest.raises(AssertionError, match="Number: 6"):
        run_cpu(func, tvm.runtime.tensor(np.array(6).astype("int32")), exec_mode=exec_mode)


def test_assert_on_argument_passes(exec_mode):
    @R.function(pure=False)
    def func(condition: R.Tensor((), "bool"), x: R.Tensor((), "int32")):
        _ = R.assert_op(condition)
        return x

    condition = tvm.runtime.tensor(np.array(True))
    x = tvm.runtime.tensor(np.array(5).astype("int32"))
    run_cpu(func, condition, x, exec_mode=exec_mode)


def test_assert_on_argument_fails(exec_mode):
    @R.function(pure=False)
    def func(condition: R.Tensor((), "bool"), x: R.Tensor((), "int32")):
        _ = R.assert_op(condition)
        return x

    condition = tvm.runtime.tensor(np.array(False))
    x = tvm.runtime.tensor(np.array(5).astype("int32"))
    with pytest.raises(AssertionError):
        run_cpu(func, condition, x, exec_mode=exec_mode)


def test_assert_on_symbolic_var_passes(exec_mode):
    @R.function(pure=False)
    def func(x: R.Tensor(["N"], "int32")):
        N = T.int64()
        _ = R.assert_op(R.prim_value(N % 8 == 0))
        return x

    x = tvm.runtime.tensor(np.arange(8, dtype="int32"))
    run_cpu(func, x, exec_mode=exec_mode)


def test_assert_on_symbolic_var_fails(exec_mode):
    @R.function(pure=False)
    def func(x: R.Tensor(["N"], "int32")):
        N = T.int64()
        _ = R.assert_op(R.prim_value(N % 8 == 0))
        return x

    x = tvm.runtime.tensor(np.arange(10, dtype="int32"))
    with pytest.raises(AssertionError):
        run_cpu(func, x, exec_mode=exec_mode)


@tvm.script.ir_module
class ShapeOfTest:
    @R.function
    def get_shape(t: R.Tensor(ndim=-1, dtype="int32")) -> R.Shape(ndim=-1):
        return R.shape_of(t)

    @R.function
    def get_constrained_shape(t: R.Tensor(ndim=1, dtype="int32")) -> R.Shape(ndim=1):
        # require the input tensor to have rank 1
        return R.shape_of(t)

    @R.function
    def get_scalar_shape() -> R.Shape(()):
        x: R.Tensor((), "int32") = R.const(1, dtype="int32")
        return R.shape_of(x)

    @R.function
    def get_constant_shape() -> R.Shape((2, 2)):
        x: R.Tensor((2, 2), "int32") = R.const(
            np.array([[1, 2], [3, 4]], dtype="int32"), dtype="int32"
        )
        return R.shape_of(x)


def test_op_shape_of(exec_mode):
    unit_shape = run_cpu(ShapeOfTest, "get_scalar_shape", exec_mode=exec_mode)
    assert unit_shape == tvm_ffi.Shape([])

    const_shape = run_cpu(ShapeOfTest, "get_constant_shape", exec_mode=exec_mode)
    assert const_shape == tvm_ffi.Shape([2, 2])

    scalar_shape = run_cpu(
        ShapeOfTest,
        "get_shape",
        tvm.runtime.tensor(np.array(1, dtype="int32")),
        exec_mode=exec_mode,
    )
    assert scalar_shape == tvm_ffi.Shape([])

    tensor_shape = run_cpu(
        ShapeOfTest,
        "get_shape",
        tvm.runtime.tensor(np.zeros((1, 2, 3)).astype("int32")),
        exec_mode=exec_mode,
    )
    assert tensor_shape == tvm_ffi.Shape([1, 2, 3])

    constrained_shape = run_cpu(
        ShapeOfTest,
        "get_constrained_shape",
        tvm.runtime.tensor(np.zeros((1,)).astype("int32")),
        exec_mode=exec_mode,
    )
    assert constrained_shape == tvm_ffi.Shape([1])


@tvm.script.ir_module
class ShapeToTensorTest:
    @R.function
    def const_shape(shape: R.Shape(ndim=-1)) -> R.Tensor(ndim=-1):
        return R.shape_to_tensor(shape)

    @R.function
    def symbolic_shape(shape: R.Shape(("m", "n"))) -> R.Tensor(ndim=-1):
        m = T.int64()
        n = T.int64()
        return R.shape_to_tensor(shape)


def test_op_shape_to_tensor(exec_mode):
    # Check type
    isinstance(ShapeToTensorTest["const_shape"].body.ty, tvm.relax.TensorType)
    assert ShapeToTensorTest["const_shape"].body.ty.ndim == 1
    isinstance(ShapeToTensorTest["symbolic_shape"].body.ty, tvm.relax.TensorType)
    assert ShapeToTensorTest["symbolic_shape"].body.ty.ndim == 1

    # Check its functionality
    out2d = run_cpu(ShapeToTensorTest, "const_shape", tvm_ffi.Shape([3, 2]), exec_mode=exec_mode)
    assert isinstance(out2d, tvm.runtime.Tensor)
    assert np.array_equal(out2d.numpy(), np.array([3, 2]))

    out3d = run_cpu(ShapeToTensorTest, "const_shape", tvm_ffi.Shape([3, 3, 2]), exec_mode=exec_mode)
    assert isinstance(out3d, tvm.runtime.Tensor)
    assert np.array_equal(out3d.numpy(), np.array([3, 3, 2]))

    out4d = run_cpu(
        ShapeToTensorTest, "const_shape", tvm_ffi.Shape([3, 3, 2, 2]), exec_mode=exec_mode
    )
    assert isinstance(out4d, tvm.runtime.Tensor)
    assert np.array_equal(out4d.numpy(), np.array([3, 3, 2, 2]))

    outs = run_cpu(ShapeToTensorTest, "symbolic_shape", tvm_ffi.Shape([3, 2]), exec_mode=exec_mode)
    assert isinstance(outs, tvm.runtime.Tensor)
    assert np.array_equal(outs.numpy(), np.array([3, 2]))


def test_op_call_pure_packed(exec_mode):
    @tvm.script.ir_module
    class CallPureTest:
        @R.function
        def pure_copy(x: R.Tensor((3, 4), "float32")):
            z = R.call_pure_packed(
                "vm.builtin.copy", x, ty_args=(R.Tensor((3, 4), dtype="float32"))
            )
            return z

    np.random.seed(0)  # to avoid flakiness
    arr = np.random.rand(3, 4).astype("float32")
    copy_found = run_cpu(CallPureTest, "pure_copy", tvm.runtime.tensor(arr), exec_mode=exec_mode)
    assert (copy_found.numpy() == arr).all()


def test_op_call_inplace_packed(exec_mode):
    # in this case we can use the same test as above
    @tvm.script.ir_module
    class CallInplaceTest:
        @R.function
        def pure_copy(x: R.Tensor((3, 4), "float32")):
            z = R.call_inplace_packed(
                "vm.builtin.copy",
                x,
                inplace_indices=0,
                ty_args=(R.Tensor((3, 4), dtype="float32")),
            )
            return z

    @tvm.register_global_func("test.inplace.add", override=True)
    def inplace_add(a, b):
        arr_a = a.numpy()
        arr_b = b.numpy()
        for i in range(len(arr_a)):
            for j in range(len(arr_a[i])):
                arr_a[i][j] = arr_a[i][j] + arr_b[i][j]
        a.copyfrom(arr_a)
        return a

    @tvm.script.ir_module
    class CallInplaceAddTest:
        @R.function
        def inplace_add(x: R.Tensor((3, 4), "float32"), y: R.Tensor((3, 4), "float32")):
            z = R.call_inplace_packed(
                "test.inplace.add",
                x,
                y,
                inplace_indices=0,
                ty_args=(R.Tensor((3, 4), dtype="float32")),
            )
            return z

    np.random.seed(1)  # to avoid flakiness
    arr_a = np.random.rand(3, 4).astype("float32")
    arr_b = np.random.rand(3, 4).astype("float32")
    sum = arr_a + arr_b
    tvm_arr_a = tvm.runtime.tensor(arr_a)
    result = run_cpu(
        CallInplaceAddTest, "inplace_add", tvm_arr_a, tvm.runtime.tensor(arr_b), exec_mode=exec_mode
    )
    assert result == tvm_arr_a
    assert (result.numpy() == sum).all()

    @tvm.register_global_func("test.inplace.tuple_add", override=True)
    def inplace_tuple_add(a, b):
        arr_a = a.numpy()
        arr_b = b.numpy()
        c = tvm.runtime.tensor(arr_a + arr_b)
        for i in range(len(arr_a)):
            for j in range(len(arr_a[i])):
                arr_a[i][j] = arr_a[i][j] + arr_b[i][j]
        a.copyfrom(arr_a)
        return tvm.runtime.convert([a, c])

    @tvm.script.ir_module
    class CallInplaceTuple:
        @R.function
        def inplace_tuple(x: R.Tensor((3, 4), "float32"), y: R.Tensor((3, 4), "float32")):
            z = R.call_inplace_packed(
                "test.inplace.tuple_add",
                x,
                y,
                inplace_indices=[0, -1],
                ty_args=(R.Tensor((3, 4), dtype="float32"), R.Tensor((3, 4), dtype="float32")),
            )
            return z

    np.random.seed(2)  # to avoid flakiness
    arr_a = np.random.rand(3, 4).astype("float32")
    arr_b = np.random.rand(3, 4).astype("float32")
    sum = arr_a + arr_b
    tvm_arr_a = tvm.runtime.tensor(arr_a)
    tvm_arr_b = tvm.runtime.tensor(arr_b)
    result = run_cpu(CallInplaceTuple, "inplace_tuple", tvm_arr_a, tvm_arr_b, exec_mode=exec_mode)
    assert result[0] == tvm_arr_a
    assert (result[0].numpy() == sum).all()
    assert result[1] != tvm_arr_a and result[1] != tvm_arr_b
    assert (result[1].numpy() == sum).all()


def test_op_call_py_func(exec_mode):
    """Test R.call_py_func operator functionality."""
    import torch

    def torch_relu(x):
        if isinstance(x, tvm.runtime.Tensor):
            x_torch = torch.from_numpy(x.numpy())
        elif hasattr(x, "asnumpy"):
            x_torch = torch.from_numpy(x.asnumpy())
        else:
            x_np = np.array(x)
            if isinstance(x_np, tvm.runtime.Tensor):
                x_torch = torch.from_numpy(x_np.numpy())
            elif len(x_np) > 0 and isinstance(x_np[0], tvm.runtime.Tensor):
                x_torch = torch.from_numpy(np.array([t.numpy() for t in x_np]))
                if x_torch.ndim > 1:
                    x_torch = x_torch.flatten()
            else:
                x_torch = torch.from_numpy(x_np)
        result = torch.relu(x_torch)
        return tvm.runtime.tensor(result.numpy())

    def torch_sigmoid(x):
        if isinstance(x, tvm.runtime.Tensor):
            x_torch = torch.from_numpy(x.numpy())
        elif hasattr(x, "asnumpy"):
            x_torch = torch.from_numpy(x.asnumpy())
        else:
            x_np = np.array(x)
            if isinstance(x_np, tvm.runtime.Tensor):
                x_torch = torch.from_numpy(x_np.numpy())
            elif len(x_np) > 0 and isinstance(x_np[0], tvm.runtime.Tensor):
                x_torch = torch.from_numpy(np.array([t.numpy() for t in x_np]))
                if x_torch.ndim > 1:
                    x_torch = x_torch.flatten()
            else:
                x_torch = torch.from_numpy(x_np)
        result = torch.sigmoid(x_torch)
        return tvm.runtime.tensor(result.numpy())

    register_func = tvm.get_global_func("vm.builtin.register_py_func")
    register_func("torch_relu", torch_relu)
    register_func("torch_sigmoid", torch_sigmoid)

    @tvm.script.ir_module
    class CallPyFuncTest:
        @R.function
        def simple_call(x: R.Tensor((3,), "float32")):
            result = R.call_py_func(R.str("torch_relu"), (x,), out_ty=R.Tensor((3,), "float32"))
            return result

        @R.function
        def multiple_calls(x: R.Tensor((2,), "float32")):
            y = R.call_py_func(R.str("torch_relu"), (x,), out_ty=R.Tensor((2,), "float32"))
            z = R.call_py_func(R.str("torch_sigmoid"), (y,), out_ty=R.Tensor((2,), "float32"))
            return z

    np.random.seed(0)
    x_data = np.array([-1.0, 0.0, 1.0], dtype=np.float32)
    x_tvm = tvm.runtime.tensor(x_data)

    result = run_cpu(CallPyFuncTest, "simple_call", x_tvm, exec_mode=exec_mode)
    expected = np.maximum(x_data, 0.0)
    assert (result.numpy() == expected).all()

    y_data = np.array([-0.5, 0.5], dtype=np.float32)
    y_tvm = tvm.runtime.tensor(y_data)

    result2 = run_cpu(CallPyFuncTest, "multiple_calls", y_tvm, exec_mode=exec_mode)
    expected2 = 1.0 / (1.0 + np.exp(-np.maximum(y_data, 0.0)))
    assert (result2.numpy() == expected2).all()

    clear_func = tvm.get_global_func("vm.builtin.clear_py_func_registry")
    clear_func()


def test_op_to_device(exec_mode):
    @tvm.script.ir_module
    class CallToDevice:
        @R.function
        def to_dev(x: R.Tensor((3, 4), "float32")):
            z = R.call_pure_packed(
                "vm.builtin.to_device",
                x,
                1,
                0,
                ty_args=(R.Tensor((3, 4), dtype="float32")),
            )
            return z

    np.random.seed(0)  # to avoid flakiness
    arr = np.random.rand(3, 4).astype("float32")
    copy_found = run_cpu(CallToDevice, "to_dev", tvm.runtime.tensor(arr), exec_mode=exec_mode)
    assert (copy_found.numpy() == arr).all()


def test_op_to_vdevice(exec_mode):
    @tvm.script.ir_module
    class ToVDevice:
        I.module_global_infos({"vdevice": [I.vdevice("llvm")]})

        @R.function
        def to_vdev(x: R.Tensor((3, 4), "float32")):
            dst_vdev = tvm.ir.VDevice("llvm", 0, "global")
            ret = R.to_vdevice(x, "llvm")
            return ret

    np.random.seed(0)
    arr = np.random.rand(3, 4).astype("float32")
    copy_found = run_cpu(ToVDevice, "to_vdev", tvm.runtime.tensor(arr), exec_mode=exec_mode)
    assert (copy_found.numpy() == arr).all()


def test_scalar_tensor_as_branch_condition(exec_mode):
    """The condition of a branch may be a scalar tensor"""

    @R.function
    def func(condition: R.Tensor((), "bool")):
        if condition:
            out = R.prim_value(5)
        else:
            out = R.prim_value(10)
        return out

    res = run_cpu(func, tvm.runtime.tensor(np.array(True)), exec_mode=exec_mode)
    assert res == 5

    res = run_cpu(func, tvm.runtime.tensor(np.array(False)), exec_mode=exec_mode)
    assert res == 10


def test_prim_value_as_branch_condition(exec_mode):
    """The condition may be a Expr"""

    @R.function
    def func(condition: R.Prim("bool")):
        if condition:
            out = R.prim_value(5)
        else:
            out = R.prim_value(10)
        return out

    res = run_cpu(func, True, exec_mode=exec_mode)
    assert res == 5

    res = run_cpu(func, False, exec_mode=exec_mode)
    assert res == 10


def test_computed_prim_value_as_branch_condition(exec_mode):
    """The R.Prim condition may be computed within the function"""

    @R.function
    def func(x: R.Tensor(["N"], "int64")):
        N = T.int64()
        if R.prim_value(N % 16 == 0):
            out = R.prim_value(5)
        else:
            out = R.prim_value(10)
        return out

    res = run_cpu(func, tvm.runtime.tensor(np.arange(16)), exec_mode=exec_mode)
    assert res == 5

    res = run_cpu(func, tvm.runtime.tensor(np.arange(20)), exec_mode=exec_mode)
    assert res == 10


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