# 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.

# 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 tempfile

import tvm_ffi

import tvm
import tvm.s_tir.meta_schedule as ms
import tvm.testing
from tvm import relax
from tvm.ir import transform
from tvm.ir.module import IRModule
from tvm.ir.transform import PassContext
from tvm.script import relax as R
from tvm.script import tirx as T

target = tvm.target.Target({"kind": "llvm", "num-cores": 16})


@tvm.script.ir_module
class InputModule:
    @T.prim_func(s_tir=True)
    def tir_matmul(x: T.handle, y: T.handle, z: T.handle) -> None:
        T.func_attr({"global_symbol": "tir_matmul"})
        k = T.int32()
        A = T.match_buffer(x, (32, 32))
        B = T.match_buffer(y, (32, 32))
        C = T.match_buffer(z, (32, 32))

        for i0, j0, k0 in T.grid(32, 32, 32):
            with T.sblock():
                i, j, k = T.axis.remap("SSR", [i0, j0, k0])
                with T.init():
                    C[i, j] = 0.0
                C[i, j] += A[i, k] * B[j, k]

    @T.prim_func(s_tir=True)
    def tir_relu(x: T.handle, y: T.handle):
        T.func_attr({"global_symbol": "tir_relu"})
        A = T.match_buffer(x, (32, 32))
        B = T.match_buffer(y, (32, 32))
        for i, j in T.grid(32, 32):
            with T.sblock():
                vi, vj = T.axis.remap("SS", [i, j])
                B[vi, vj] = T.max(A[vi, vj], 0.0)

    @R.function
    def main(x: R.Tensor((32, 32), "float32"), w: R.Tensor((32, 32), "float32")) -> R.Tensor:
        cls = InputModule
        with R.dataflow():
            lv0 = R.call_tir(cls.tir_matmul, (x, w), R.Tensor((32, 32), dtype="float32"))
            lv1 = R.call_tir(cls.tir_relu, (lv0), R.Tensor((32, 32), dtype="float32"))
            R.output(lv1)
        return lv1


# TODO(@sunggg): determine how to pass MS database object across different passes.
# PassContext might be an option, but we already have TuningAPI database.
# (MS database and TuningAPI database will be unified in the future)
# For now, we only support default JSON database config.
def test_ms_tuning_irmodule():
    mod = InputModule
    assert isinstance(mod, IRModule)

    with tempfile.TemporaryDirectory() as work_dir:
        """
        # TODO(@sunggg): revisit when ready
        with target, PassContext(trace=Trace(mod), opt_level=0):
            tuning_pass = relax.transform.MetaScheduleTuneIRMod(
                params={}, work_dir=work_dir, max_trials_global=4
            )
            out_mod = tuning_pass(mod)
            assert PassContext.current().get_trace_stack_size() == 1
            assert PassContext.current().get_current_trace().size == 1
            tvm.ir.assert_structural_equal(mod, out_mod)
        """

        with target, PassContext(opt_level=0):
            tuning_pass = relax.transform.MetaScheduleTuneIRMod(
                params={}, work_dir=work_dir, max_trials_global=4
            )
            out_mod = tuning_pass(mod)

            application_pass = relax.transform.MetaScheduleApplyDatabase(work_dir)

            out_mod = application_pass(mod)
            assert not tvm_ffi.structural_equal(mod, out_mod)


def test_ms_tuning_primfunc():
    mod = InputModule
    assert isinstance(mod, IRModule)
    with tempfile.TemporaryDirectory() as work_dir:
        """
        # TODO(@sunggg): revisit when ready
        with target, PassContext(trace=Trace(mod), opt_level=0):
            tuning_pass = relax.transform.MetaScheduleTuneTIR(
                work_dir=work_dir, max_trials_global=4
            )
            out_mod = tuning_pass(mod)
            assert PassContext.current().get_trace_stack_size() == 1
            # TODO (@sunggg): Need to determine how to track subgraph-level tuning traces.
            # Currently, we don't track this so the trace size. Revisit this later.
            tvm.ir.assert_structural_equal(mod, out_mod)
        """
        with target, PassContext(opt_level=0):
            tuning_pass = relax.transform.MetaScheduleTuneIRMod(
                params={}, work_dir=work_dir, max_trials_global=4
            )
            out_mod = tuning_pass(mod)

            application_pass = relax.transform.MetaScheduleApplyDatabase(work_dir)
            out_mod = application_pass(mod)
            assert not tvm_ffi.structural_equal(mod, out_mod)

    with tempfile.TemporaryDirectory() as work_dir:
        with target, PassContext(opt_level=0):
            tuning_pass = relax.transform.MetaScheduleTuneIRMod(
                params={},
                work_dir=work_dir,
                max_trials_global=4,
                max_trials_per_task=2,
                op_names=["matmul"],
            )
            tuning_pass(mod)

            db = ms.database.JSONDatabase(
                work_dir + "/database_workload.json", work_dir + "/database_tuning_record.json"
            )

            assert len(db.get_all_tuning_records()) == 2

            for rec in db.get_all_tuning_records():
                assert rec.workload.mod["main"].attrs["global_symbol"] == "tir_matmul"


@tvm.script.ir_module
class DefaultScheduledModule:
    @T.prim_func(s_tir=True)
    def tir_matmul(
        A: T.Buffer((32, 32), "float32"),
        B: T.Buffer((32, 32), "float32"),
        C: T.Buffer((32, 32), "float32"),
    ):
        T.func_attr({"global_symbol": "tir_matmul", "tirx.is_scheduled": True})
        # with T.sblock("root"):
        for i0_j0_fused_0 in T.thread_binding(1, thread="blockIdx.x"):
            for i0_j0_fused_1 in T.thread_binding(1024, thread="threadIdx.x"):
                for k0 in range(32):
                    with T.sblock(""):
                        i = T.axis.spatial(32, (i0_j0_fused_0 * 1024 + i0_j0_fused_1) // 32)
                        j = T.axis.spatial(32, (i0_j0_fused_0 * 1024 + i0_j0_fused_1) % 32)
                        k = T.axis.reduce(32, k0)
                        T.reads(A[i, k], B[j, k])
                        T.writes(C[i, j])
                        with T.init():
                            C[i, j] = T.float32(0)
                        C[i, j] = C[i, j] + A[i, k] * B[j, k]

    @T.prim_func(s_tir=True)
    def tir_relu(A: T.Buffer((32, 32), "float32"), B: T.Buffer((32, 32), "float32")):
        T.func_attr({"global_symbol": "tir_relu", "tirx.is_scheduled": True})
        # with T.sblock("root"):
        for i_j_fused_0 in T.thread_binding(1, thread="blockIdx.x"):
            for i_j_fused_1 in T.thread_binding(1024, thread="threadIdx.x"):
                with T.sblock(""):
                    vi = T.axis.spatial(32, (i_j_fused_0 * 1024 + i_j_fused_1) // 32)
                    vj = T.axis.spatial(32, (i_j_fused_0 * 1024 + i_j_fused_1) % 32)
                    T.reads(A[vi, vj])
                    T.writes(B[vi, vj])
                    B[vi, vj] = T.max(A[vi, vj], T.float32(0))

    @R.function
    def main(
        x: R.Tensor((32, 32), dtype="float32"), w: R.Tensor((32, 32), dtype="float32")
    ) -> R.Tensor((32, 32), dtype="float32"):
        with R.dataflow():
            lv0 = R.call_tir(
                DefaultScheduledModule.tir_matmul,
                (x, w),
                out_ty=R.Tensor((32, 32), dtype="float32"),
            )
            lv1 = R.call_tir(
                DefaultScheduledModule.tir_relu,
                (lv0,),
                out_ty=R.Tensor((32, 32), dtype="float32"),
            )
            R.output(lv1)
        return lv1


def test_ms_database_apply_fallback():
    mod = InputModule
    target_cuda = tvm.target.Target("nvidia/geforce-rtx-3090-ti")
    assert isinstance(mod, IRModule)
    with tempfile.TemporaryDirectory() as work_dir:
        """
        with target_cuda, PassContext(trace=Trace(mod), opt_level=0):
            tuning_pass = relax.transform.MetaScheduleTuneTIR(
                work_dir=work_dir, max_trials_global=0
            )
            out_mod = tuning_pass(mod)
            tvm.ir.assert_structural_equal(mod, out_mod)
        """
        with target_cuda, PassContext(opt_level=0):
            tuning_pass = relax.transform.MetaScheduleTuneTIR(
                work_dir=work_dir, max_trials_global=0
            )
            out_mod = tuning_pass(mod)
            default_pass = tvm.s_tir.transform.DefaultGPUSchedule()
            out_mod = default_pass(mod)
            tvm.ir.assert_structural_equal(out_mod, DefaultScheduledModule)


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