# 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: F401

import ctypes

import numpy as np
import pytest
import tvm_ffi

import tvm.testing
from tvm import relax
from tvm.ir import Op
from tvm.script import ir as I
from tvm.script import relax as R

# Parameterization for reading dtype of DLTensor.  Chosen to have
# multiple distinct type codes, number of lanes, and widths.
dtype = tvm.testing.parameter(
    "int32",
    "int64",
    "float32",
    "float32x4",
    "bfloat",
    "float8_e4m3fn",
)
shape = tvm.testing.parameter(
    [],
    [16],
    [128, 256],
    [1] * 64,
)

elem_offset = tvm.testing.parameter(0, 64, 128)


def inspect_tensor_field(op_name, *args):
    return relax.Call(Op.get(f"relax.inspect.{op_name}"), args)


def test_tensor_dtype_code(dtype):
    @I.ir_module
    class mod:
        @R.function
        def main(A: R.Tensor):
            return inspect_tensor_field("tensor_dtype_code", A)

    built = tvm.compile(mod)
    vm = relax.VirtualMachine(built, tvm.cpu())

    arg = tvm.runtime.empty([16], dtype)
    res = vm["main"](arg)

    expected_type_code = tvm.runtime.DataType(dtype).type_code
    assert res == expected_type_code


def test_tensor_dtype_bits(dtype):
    @I.ir_module
    class mod:
        @R.function
        def main(A: R.Tensor):
            return inspect_tensor_field("tensor_dtype_bits", A)

    built = tvm.compile(mod)
    vm = relax.VirtualMachine(built, tvm.cpu())

    arg = tvm.runtime.empty([16], dtype)
    res = vm["main"](arg)

    expected_type_bits = tvm.runtime.DataType(dtype).bits
    assert res == expected_type_bits


def test_tensor_dtype_lanes(dtype):
    @I.ir_module
    class mod:
        @R.function
        def main(A: R.Tensor):
            return inspect_tensor_field("tensor_dtype_lanes", A)

    built = tvm.compile(mod)
    vm = relax.VirtualMachine(built, tvm.cpu())

    arg = tvm.runtime.empty([16], dtype)
    res = vm["main"](arg)

    expected_type_lanes = tvm.runtime.DataType(dtype).lanes
    assert res == expected_type_lanes


def test_tensor_ndim(shape):
    @I.ir_module
    class mod:
        @R.function
        def main(A: R.Tensor):
            return inspect_tensor_field("tensor_ndim", A)

    built = tvm.compile(mod)
    vm = relax.VirtualMachine(built, tvm.cpu())

    arg = tvm.runtime.empty(shape, "int32")
    res = vm["main"](arg)

    assert res == len(shape)


def test_tensor_shape(shape):
    @I.ir_module
    class mod:
        @R.function
        def main(A: R.Tensor, axis: R.Prim("int64")):
            return inspect_tensor_field("tensor_shape_i", A, axis)

    built = tvm.compile(mod)
    vm = relax.VirtualMachine(built, tvm.cpu())

    arg = tvm.runtime.empty(shape, "int32")

    res = [vm["main"](arg, i) for i, _ in enumerate(shape)]

    tvm.ir.assert_structural_equal(res, shape)


def _get_compact_striding(shape):
    strides = []
    product = 1
    for dim in reversed(shape):
        strides.append(product)
        product *= dim
    return list(reversed(strides))


def test_strides_of_compact_tensor(shape):
    @I.ir_module
    class mod:
        @R.function
        def main(A: R.Tensor, axis: R.Prim("int64")):
            return inspect_tensor_field("tensor_stride_i", A, axis)

    built = tvm.compile(mod)
    vm = relax.VirtualMachine(built, tvm.cpu())

    arg = tvm.runtime.empty(shape, "int32")

    res = [vm["main"](arg, i) for i, _ in enumerate(shape)]
    expected = _get_compact_striding(shape)

    tvm.ir.assert_structural_equal(res, expected)


def test_strides_of_non_compact_tensor():
    @I.ir_module
    class mod:
        @R.function
        def main(A: R.Tensor, axis: R.Prim("int64")):
            return inspect_tensor_field("tensor_stride_i", A, axis)

    built = tvm.compile(mod)
    vm = relax.VirtualMachine(built, tvm.cpu())
    view_shape = [4, 4]
    expected_strides = [1, 4]
    # use transpose to make strides non-compact
    x = np.zeros([4, 4], "int32").T
    y = tvm_ffi.from_dlpack(x, require_alignment=4, require_contiguous=False)
    res = [vm["main"](y, i) for i, _ in enumerate(view_shape)]
    tvm.ir.assert_structural_equal(res, expected_strides)


def test_byte_offset(elem_offset):
    backing_shape = [64, 64]
    view_shape = [16, 16]
    byte_offset = elem_offset * 4

    @I.ir_module
    class mod:
        @R.function
        def main(A: R.Tensor):
            return inspect_tensor_field("tensor_byte_offset", A)

    built = tvm.compile(mod)
    vm = relax.VirtualMachine(built, tvm.cpu())
    dtype = "int32"
    backing_tensor = tvm.runtime.empty(backing_shape, dtype)
    view = backing_tensor._create_view(view_shape, dtype, relative_byte_offset=byte_offset)
    res = vm["main"](view)
    assert res == byte_offset


def test_elem_offset(elem_offset, dtype):
    tvm_dtype = tvm.runtime.DataType(dtype)

    backing_shape = [64, 64]
    view_shape = [16, 16]
    element_bytes = (tvm_dtype.bits * tvm_dtype.lanes) // 8
    byte_offset = elem_offset * element_bytes

    @I.ir_module
    class mod:
        @R.function
        def main(A: R.Tensor):
            return inspect_tensor_field("tensor_elem_offset", A)

    built = tvm.compile(mod)
    vm = relax.VirtualMachine(built, tvm.cpu())

    backing_tensor = tvm.runtime.empty(backing_shape, dtype)
    view = backing_tensor._create_view(view_shape, dtype, relative_byte_offset=byte_offset)
    res = vm["main"](view)

    assert res == elem_offset


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