import re

import pytest
import tilelang
import tilelang.testing
import tvm
from tilelang import language as T
from tvm.target import Target


@tilelang.jit
def _tilelang_issue_layout_free_inference_choose_smallest_replication():
    @T.prim_func
    def main(A: T.Tensor((128, 4), T.float), B: T.Tensor((128, 4), T.float)):
        with T.Kernel(1, threads=128) as _:
            A_frag = T.alloc_fragment((128, 4), T.float)
            B_frag = T.alloc_fragment((128, 4), T.float)
            S_frag = T.alloc_fragment((4,), T.float)
            T.annotate_layout(
                {
                    A_frag: T.Fragment(A_frag.shape, lambda i, j: (i, j)),
                }
            )
            for i, j in T.Parallel(128, 4):
                A_frag[i, j] = S_frag[j]
            for i, j in T.Parallel(128, 4):
                B_frag[i, j] = S_frag[j]

    return main


def test_tilelang_issue_layout_free_inference_choose_smallest_replication():
    kernel = _tilelang_issue_layout_free_inference_choose_smallest_replication()
    source = kernel.get_kernel_source()
    assert "float S_frag[4];" in source, "S_frag is not in the source"
    assert "float B_frag[4];" in source, "B_frag is not in the source"
    assert "float A_frag[4];" in source, "A_frag is not in the source"


@tilelang.jit
def _invalid_fragment_write_owner_layout():
    def token_loop_layout_fn(i, j, rep):
        return i * 4 + rep % 4 + (rep // 4) * 32, j

    def scalar_buffer_layout_fn(i, j, rep):
        return i * 4 + j + rep * 32, 0

    loop_layout = T.Fragment((8, 4), forward_fn=token_loop_layout_fn, replicate=8)
    buffer_layout = T.Fragment((8, 4), forward_fn=scalar_buffer_layout_fn, replicate=2)

    @T.prim_func
    def main():
        with T.Kernel(1, threads=64):
            frag = T.alloc_fragment((8, 4), T.float32)
            T.annotate_layout({frag: buffer_layout})
            for i, j in T.Parallel(8, 4, loop_layout=loop_layout):
                frag[i, j] = T.float32(1.0)

    return main


def test_reject_fragment_write_from_non_owner_threads():
    with pytest.raises(Exception, match="Layout infer conflict"):
        _invalid_fragment_write_owner_layout()


@tilelang.jit
def _scalar_reduce_accumulator_owner_layout():
    @T.prim_func
    def main(out: T.Tensor((1,), T.float32)):
        with T.Kernel(1, threads=128):
            acc = T.alloc_fragment((128,), T.float32)
            total = T.alloc_fragment((1,), T.float32)
            total[0] = T.float32(0.0)
            for k in T.Parallel(128):
                acc[k] = T.float32(1.0)
            T.reduce_sum(acc, total, clear=False)
            if T.get_thread_binding() == 0:
                out[0] = total[0]

    return main


def test_scalar_reduce_accumulator_owner_layout():
    kernel = _scalar_reduce_accumulator_owner_layout()
    source = kernel.get_kernel_source()
    assert "AllReduce" in source


def _lower_fully_replicated_readback(use_copy: bool) -> str:
    @T.prim_func
    def main(inp: T.Tensor((128,), T.float32), out: T.Tensor((128,), T.float32)):
        with T.Kernel(1, threads=128):
            fragment = T.alloc_fragment((128,), T.float32)
            T.annotate_layout({fragment: tilelang.layout.make_fully_replicated_layout_fragment(fragment, 128)})

            for i in T.serial(128):
                fragment[i] = inp[i]

            if use_copy:
                T.copy(fragment, out)
            else:
                for i in T.Parallel(128):
                    out[i] = fragment[i]

    target = Target("cuda")
    pass_configs = {
        tilelang.PassConfigKey.TL_DISABLE_TMA_LOWER.value: True,
        tilelang.PassConfigKey.TL_DISABLE_WARP_SPECIALIZED.value: True,
        tilelang.PassConfigKey.TL_DISABLE_DATA_RACE_CHECK.value: True,
    }
    with tvm.transform.PassContext(config=pass_configs), target:
        artifact = tilelang.lower(main, target=target)
    return artifact.kernel_source


def _assert_single_thread_replicated_readback(src: str) -> None:
    assert not re.search(r"fragment\[[^\]\n]*threadIdx\.x", src), src
    assert not re.search(r"out\[[^\]\n]*threadIdx\.x", src), src
    assert "if (((int)threadIdx.x) == 0)" in src
    assert re.search(r"for \(int i(_\d+)? = 0; i(_\d+)? < 128; \+\+i(_\d+)?\)", src), src


@tilelang.testing.requires_cuda
def test_parallel_readback_from_fully_replicated_fragment_uses_rep_guard():
    src = _lower_fully_replicated_readback(use_copy=False)
    _assert_single_thread_replicated_readback(src)


@tilelang.testing.requires_cuda
def test_copy_readback_from_fully_replicated_fragment_uses_rep_guard():
    src = _lower_fully_replicated_readback(use_copy=True)
    _assert_single_thread_replicated_readback(src)


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