# 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.
# pylint: disable=missing-docstring,
from collections.abc import Sequence

import numpy as np
import pytest
from tvm_ffi import Shape

import tvm
import tvm.testing
from tvm import tirx
from tvm.s_tir import dlight as dl
from tvm.script import tirx as T
from tvm.testing import env

# pylint: disable=invalid-name

np_zero = np.full((16, 16), 0.0, "float16")
np_one = np.full((32, 32), 1.0, "float32")
np_two = np.full((16, 16), 2.0, "float16")
np_three = np.full((32, 32), 3.0, "float32")

reserved_nseq = 4
max_history = 4
num_layers = 1
# Note that kernels in this test file cannot support 1-dim states.
states = [((16, 16), "float16"), ((32, 32), "float32")]

f_clear = None
f_add_sequence = None
f_remove_sequence = None
f_fork_sequence = None
f_popn = None
f_begin_forward = None
f_end_forward = None
f_get = None
f_set = None
f_debug_get = None

f_tir_gets = []
f_tir_sets = []

# pylint: enable=invalid-name


def set_global_func():
    global f_clear, f_add_sequence, f_remove_sequence, f_fork_sequence, f_popn
    global f_begin_forward, f_end_forward, f_get, f_set, f_debug_get
    global f_tir_gets, f_tir_sets

    f_clear = tvm.get_global_func("vm.builtin.kv_state_clear")
    f_add_sequence = tvm.get_global_func("vm.builtin.kv_state_add_sequence")
    f_remove_sequence = tvm.get_global_func("vm.builtin.kv_state_remove_sequence")
    f_fork_sequence = tvm.get_global_func("vm.builtin.kv_state_fork_sequence")
    f_popn = tvm.get_global_func("vm.builtin.kv_state_popn")
    f_begin_forward = tvm.get_global_func("vm.builtin.kv_state_begin_forward")
    f_end_forward = tvm.get_global_func("vm.builtin.kv_state_end_forward")
    f_get = tvm.get_global_func("vm.builtin.rnn_state_get")
    f_set = tvm.get_global_func("vm.builtin.rnn_state_set")
    f_debug_get = tvm.get_global_func("vm.builtin.rnn_state_debug_get")

    target = tvm.target.Target("cuda")

    def _build(tir_func):
        mod = tvm.IRModule({"main": tir_func})
        with target:
            mod = dl.ApplyDefaultSchedule(dl.gpu.Fallback())(mod)  # pylint: disable=not-callable
        f = tvm.tirx.build(mod["main"], target=target)
        return f.main

    _f_tir_gets, _f_tir_sets = [], []
    for state in states:
        shape, dtype = state
        _f_tir_gets.append(_build(rnn_state_get(shape, dtype)))
        _f_tir_sets.append(_build(rnn_state_set(shape, dtype)))

    f_tir_gets = _f_tir_gets
    f_tir_sets = _f_tir_sets


def create_rnn_state(device):
    f_create = tvm.get_global_func("vm.builtin.rnn_state_create")
    init_values = [
        tvm.runtime.tensor(np_zero, device=device),
        tvm.runtime.tensor(np_one, device=device),
    ]
    return f_create(num_layers, reserved_nseq, max_history, f_tir_gets, f_tir_sets, init_values)


@pytest.fixture
def rnn_state():
    set_global_func()
    return create_rnn_state


def verify_state(state, seq_ids, expected_values):
    layer_id = 0
    for seq_id in seq_ids:
        for state_id, expected_value in enumerate(expected_values[seq_id]):
            state_value = f_debug_get(state, layer_id, state_id, seq_id)
            tvm.testing.assert_allclose(state_value.numpy(), expected_value)


@pytest.mark.gpu
@pytest.mark.skipif(not env.has_cuda(), reason="need cuda")
def test_rnn_state_get(rnn_state):  # pylint: disable=redefined-outer-name
    def run_and_check():
        device = tvm.cuda()
        state = rnn_state(device)
        f_clear(state)
        f_add_sequence(state, 0)
        f_begin_forward(state, Shape([0]), Shape([1]))
        tvm_nd_0 = tvm.runtime.tensor(np.empty((1, 16, 16), "float16"), device=device)
        tvm_nd_1 = tvm.runtime.tensor(np.empty((1, 32, 32), "float32"), device=device)
        f_get(state, 0, 0, tvm_nd_0)
        f_get(state, 0, 1, tvm_nd_1)
        f_end_forward(state)
        tvm.testing.assert_allclose(tvm_nd_0.numpy(), np.zeros((1, 16, 16), "float16"))
        tvm.testing.assert_allclose(tvm_nd_1.numpy(), np.ones((1, 32, 32), "float32"))

    tvm.testing.run_with_gpu_lock(run_and_check)


@pytest.mark.gpu
@pytest.mark.skipif(not env.has_cuda(), reason="need cuda")
def test_rnn_state_set(rnn_state):  # pylint: disable=redefined-outer-name
    def run_and_check():
        device = tvm.cuda()
        state = rnn_state(device)
        f_clear(state)
        for seq_id in range(3):
            f_add_sequence(state, seq_id)
        f_begin_forward(state, Shape([0, 2]), Shape([1, 1]))
        f_set(
            state,
            0,
            0,
            tvm.runtime.tensor(np.full((2, 16, 16), 2.0, "float16"), device=device),
        )
        f_set(
            state,
            0,
            1,
            tvm.runtime.tensor(np.full((2, 32, 32), 3.0, "float32"), device=device),
        )
        f_end_forward(state)
        expected_values = [[np_two, np_three], [np_zero, np_one], [np_two, np_three]]
        verify_state(state, [0, 1, 2], expected_values)

    tvm.testing.run_with_gpu_lock(run_and_check)


@pytest.mark.gpu
@pytest.mark.skipif(not env.has_cuda(), reason="need cuda")
def test_rnn_state_popn(rnn_state):  # pylint: disable=redefined-outer-name
    def run_and_check():
        device = tvm.cuda()
        state = rnn_state(device)
        f_clear(state)
        f_add_sequence(state, 0)
        f_begin_forward(state, Shape([0]), Shape([1]))
        f_set(state, 0, 0, tvm.runtime.tensor(np_two.reshape(1, 16, 16), device=device))
        f_set(state, 0, 1, tvm.runtime.tensor(np_three.reshape(1, 32, 32), device=device))
        f_end_forward(state)
        verify_state(state, [0], [[np_two, np_three]])
        f_popn(state, 0, 1)
        verify_state(state, [0], [[np_zero, np_one]])
        with pytest.raises(RuntimeError):
            f_popn(state, 0, 1)

    tvm.testing.run_with_gpu_lock(run_and_check)


@pytest.mark.gpu
@pytest.mark.skipif(not env.has_cuda(), reason="need cuda")
def test_rnn_state_fork_sequence(rnn_state):  # pylint: disable=redefined-outer-name
    def run_and_check():
        device = tvm.cuda()
        state = rnn_state(device)
        f_clear(state)
        f_add_sequence(state, 0)
        f_begin_forward(state, Shape([0]), Shape([1]))
        f_set(state, 0, 0, tvm.runtime.tensor(np_two.reshape(1, 16, 16), device=device))
        f_set(state, 0, 1, tvm.runtime.tensor(np_three.reshape(1, 32, 32), device=device))
        f_end_forward(state)
        f_fork_sequence(state, 0, 1, -1)
        verify_state(state, [0, 1], [[np_two, np_three], [np_two, np_three]])
        f_popn(state, 1, 1)
        verify_state(state, [0, 1], [[np_two, np_three], [np_zero, np_one]])

    tvm.testing.run_with_gpu_lock(run_and_check)


def rnn_state_get(
    shape: Sequence[int],
    dtype: str,
):
    # fmt: off
    @T.prim_func(s_tir=True)
    def _rnn_state_get(
        var_storage: T.handle,
        var_seq_slot_ids: T.handle,
        var_history_slot_ids: T.handle,
        var_output: T.handle,
    ):
        batch_size = T.int32()

        storage = T.match_buffer(var_storage, (reserved_nseq, max_history, *shape), dtype)
        seq_slot_ids = T.match_buffer(var_seq_slot_ids, (batch_size,), "int32")
        history_slot_ids = T.match_buffer(var_history_slot_ids, (batch_size,), "int32")
        output = T.match_buffer(var_output, (batch_size, *shape), dtype)

        for i in range(batch_size):
            for s in T.grid(*shape):
                with T.sblock("copy"):
                    vi, *vs = T.axis.remap("S" * (len(shape) + 1), [i, *s])
                    seq_id: T.let[T.int32] = seq_slot_ids[vi]
                    history_id: T.let[T.int32] = history_slot_ids[vi]
                    # The following line is equivalent to:
                    # `output[vi, *vs] = storage[seq_id, history_id, *vs]`
                    # However, unpacking operator in subscript requires Python 3.11 or newer
                    T.buffer_store(
                        output, T.BufferLoad(storage, [seq_id, history_id, *vs]), [vi, *vs]
                    )
    # fmt: on
    return _rnn_state_get


def rnn_state_set(
    shape: Sequence[int | tirx.Var],
    dtype: str,
):
    # fmt: off
    @T.prim_func(s_tir=True)
    def _rnn_state_set(
        var_storage: T.handle,
        var_seq_slot_ids: T.handle,
        var_history_slot_ids: T.handle,
        var_data: T.handle,
    ):
        batch_size = T.int32()

        storage = T.match_buffer(var_storage, (reserved_nseq, max_history, *shape), dtype)
        seq_slot_ids = T.match_buffer(var_seq_slot_ids, (batch_size,), "int32")
        history_slot_ids = T.match_buffer(var_history_slot_ids, (batch_size,), "int32")
        data = T.match_buffer(var_data, (batch_size, *shape), dtype)

        for i in range(batch_size):
            for s in T.grid(*shape):
                with T.sblock("copy"):
                    vi, *vs = T.axis.remap("S" * (len(shape) + 1), [i, *s])
                    seq_id: T.let[T.int32] = seq_slot_ids[vi]
                    history_id: T.let[T.int32] = (history_slot_ids[vi] + 1) % T.cast(
                        max_history, "int32"
                    )
                    # The following line is equivalent to:
                    # `storage[seq_id, history_id, *vs] = data[vi, *vs]`
                    # However, unpacking operator in subscript requires Python 3.11 or newer
                    T.buffer_store(
                        storage, T.BufferLoad(data, [vi, *vs]), [seq_id, history_id, *vs]
                    )

    # fmt: on

    return _rnn_state_set


if __name__ == "__main__":
    set_global_func()
    rnn_state = create_rnn_state
    test_rnn_state_get(rnn_state)
    test_rnn_state_set(rnn_state)
    test_rnn_state_popn(rnn_state)
    test_rnn_state_fork_sequence(rnn_state)
