# 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, F841

from typing import Union

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


def test_batch_norm_inference():
    @I.ir_module
    class Before:
        @R.function
        def main(
            x: R.Tensor((1, 64, 112, 112), "float32"),
            gamma: R.Tensor((64,), "float32"),
            beta: R.Tensor((64,), "float32"),
            moving_mean: R.Tensor((64,), "float32"),
            moving_var: R.Tensor((64,), "float32"),
        ):
            with R.dataflow():
                bn = R.nn.batch_norm(
                    x,
                    gamma,
                    beta,
                    moving_mean,
                    moving_var,
                    axis=1,
                    epsilon=1e-5,
                    center=True,
                    scale=True,
                )
                gv = bn[0]
                R.output(gv)
            return gv

    @I.ir_module
    class Expected:
        @R.function
        def main(
            x: R.Tensor((1, 64, 112, 112), dtype="float32"),
            gamma: R.Tensor((64,), dtype="float32"),
            beta: R.Tensor((64,), dtype="float32"),
            moving_mean: R.Tensor((64,), dtype="float32"),
            moving_var: R.Tensor((64,), dtype="float32"),
        ) -> R.Tensor((1, 64, 112, 112), dtype="float32"):
            with R.dataflow():
                lv: R.Tensor((1, 64, 1, 1), dtype="float32") = R.expand_dims(
                    moving_mean, axis=[0, 2, 3]
                )
                lv1: R.Tensor((1, 64, 112, 112), dtype="float32") = R.subtract(x, lv)
                lv2: R.Tensor((1, 64, 1, 1), dtype="float32") = R.expand_dims(
                    moving_var, axis=[0, 2, 3]
                )
                lv3: R.Tensor((1, 64, 1, 1), dtype="float32") = R.add(
                    lv2, R.const(9.9999997473787516e-06, "float32")
                )
                lv4: R.Tensor((1, 64, 1, 1), dtype="float32") = R.sqrt(lv3)
                lv5: R.Tensor((1, 64, 112, 112), dtype="float32") = R.divide(lv1, lv4)
                lv6: R.Tensor((1, 64, 1, 1), dtype="float32") = R.expand_dims(gamma, axis=[0, 2, 3])
                lv7: R.Tensor((1, 64, 112, 112), dtype="float32") = R.multiply(lv5, lv6)
                lv8: R.Tensor((1, 64, 1, 1), dtype="float32") = R.expand_dims(beta, axis=[0, 2, 3])
                lv9: R.Tensor((1, 64, 112, 112), dtype="float32") = R.add(lv7, lv8)
                bn: R.Tuple(
                    R.Tensor((1, 64, 112, 112), dtype="float32"),
                    R.Tensor((64,), dtype="float32"),
                    R.Tensor((64,), dtype="float32"),
                ) = (lv9, moving_mean, moving_var)
                gv: R.Tensor((1, 64, 112, 112), dtype="float32") = bn[0]
                R.output(gv)
            return gv

    After = relax.transform.DecomposeOpsForInference("main")(Before)
    tvm.ir.assert_structural_equal(Expected, After)


def test_batch_norm_training():
    @I.ir_module
    class Before:
        @R.function
        def main(
            x: R.Tensor((1, 64, 112, 112), "float32"),
            gamma: R.Tensor((64,), "float32"),
            beta: R.Tensor((64,), "float32"),
            moving_mean: R.Tensor((64,), "float32"),
            moving_var: R.Tensor((64,), "float32"),
        ):
            with R.dataflow():
                bn = R.nn.batch_norm(
                    x,
                    gamma,
                    beta,
                    moving_mean,
                    moving_var,
                    axis=1,
                    epsilon=1e-5,
                    center=True,
                    scale=True,
                    momentum=0.1,
                )
                gv0 = bn[0]
                gv1 = bn[1]
                gv2 = bn[2]
                R.output(gv0, gv1, gv2)
            return gv0, gv1, gv2

    @I.ir_module
    class Expected:
        @R.function
        def main(
            x: R.Tensor((1, 64, 112, 112), dtype="float32"),
            gamma: R.Tensor((64,), dtype="float32"),
            beta: R.Tensor((64,), dtype="float32"),
            moving_mean: R.Tensor((64,), dtype="float32"),
            moving_var: R.Tensor((64,), dtype="float32"),
        ) -> R.Tuple(
            R.Tensor((1, 64, 112, 112), dtype="float32"),
            R.Tensor((64,), dtype="float32"),
            R.Tensor((64,), dtype="float32"),
        ):
            with R.dataflow():
                # This portion is training-specific, computing the
                # mean/variance of the dataset.
                lv = R.mean(x, axis=[0, 2, 3], keepdims=False)
                lv3 = R.variance(x, axis=[0, 2, 3], keepdims=False)

                # This portion is identical to the batch_norm run during inference
                lv1 = R.expand_dims(lv, axis=[0, 2, 3])
                lv2 = R.subtract(x, lv1)
                lv4 = R.expand_dims(lv3, axis=[0, 2, 3])
                lv5 = R.add(lv4, R.const(9.9999997473787516e-06, "float32"))
                lv6 = R.sqrt(lv5)
                lv7 = R.divide(lv2, lv6)
                lv8 = R.expand_dims(gamma, axis=[0, 2, 3])
                lv9 = R.multiply(lv7, lv8)
                lv10 = R.expand_dims(beta, axis=[0, 2, 3])
                lv11 = R.add(lv9, lv10)
                inner_tuple = (lv11, lv, lv3)
                # This is the result that would be returned from a
                # batch_norm at inference.

                # However, at training we need to update the moving
                # mean/variance, and to return those updated values.
                inner_res = inner_tuple[0]
                lv12 = R.multiply(R.const(0.89999997615814209, "float32"), moving_mean)
                lv13 = R.multiply(R.const(0.10000000149011612, "float32"), lv)
                lv14 = R.add(lv12, lv13)
                lv15 = R.multiply(R.const(0.89999997615814209, "float32"), moving_var)
                lv16 = R.multiply(R.const(0.10000000149011612, "float32"), lv3)
                lv17 = R.add(lv15, lv16)
                bn = (inner_res, lv14, lv17)
                gv0 = bn[0]
                gv1 = bn[1]
                gv2 = bn[2]
                R.output(gv0, gv1, gv2)
            return (gv0, gv1, gv2)

    After = relax.transform.DecomposeOpsForTraining("main")(Before)
    tvm.ir.assert_structural_equal(Expected, After)


def test_batch_norm_multiple_functions():
    @I.ir_module
    class Before:
        @R.function
        def main(
            x: R.Tensor((1, 64, 112, 112), "float32"),
            gamma: R.Tensor((64,), "float32"),
            beta: R.Tensor((64,), "float32"),
            moving_mean: R.Tensor((64,), "float32"),
            moving_var: R.Tensor((64,), "float32"),
        ):
            with R.dataflow():
                bn = R.nn.batch_norm(
                    x,
                    gamma,
                    beta,
                    moving_mean,
                    moving_var,
                    axis=1,
                    epsilon=1e-5,
                    center=True,
                    scale=True,
                )
                gv = bn[0]
                R.output(gv)
            return gv

        @R.function
        def main1(
            x: R.Tensor((1, 64, 112, 112), "float32"),
            gamma: R.Tensor((64,), "float32"),
            beta: R.Tensor((64,), "float32"),
            moving_mean: R.Tensor((64,), "float32"),
            moving_var: R.Tensor((64,), "float32"),
        ):
            with R.dataflow():
                bn = R.nn.batch_norm(
                    x,
                    gamma,
                    beta,
                    moving_mean,
                    moving_var,
                    axis=1,
                    epsilon=1e-5,
                    center=True,
                    scale=True,
                )
                gv = bn[0]
                R.output(gv)
            return gv

    @I.ir_module
    class Expected:
        @R.function
        def main1(
            x: R.Tensor((1, 64, 112, 112), dtype="float32"),
            gamma: R.Tensor((64,), dtype="float32"),
            beta: R.Tensor((64,), dtype="float32"),
            moving_mean: R.Tensor((64,), dtype="float32"),
            moving_var: R.Tensor((64,), dtype="float32"),
        ) -> R.Tensor((1, 64, 112, 112), dtype="float32"):
            with R.dataflow():
                lv10: R.Tensor((1, 64, 1, 1), dtype="float32") = R.expand_dims(
                    moving_mean, axis=[0, 2, 3]
                )
                lv11: R.Tensor((1, 64, 112, 112), dtype="float32") = R.subtract(x, lv10)
                lv12: R.Tensor((1, 64, 1, 1), dtype="float32") = R.expand_dims(
                    moving_var, axis=[0, 2, 3]
                )
                lv13: R.Tensor((1, 64, 1, 1), dtype="float32") = R.add(
                    lv12, R.const(9.9999997473787516e-06, "float32")
                )
                lv14: R.Tensor((1, 64, 1, 1), dtype="float32") = R.sqrt(lv13)
                lv15: R.Tensor((1, 64, 112, 112), dtype="float32") = R.divide(lv11, lv14)
                lv16: R.Tensor((1, 64, 1, 1), dtype="float32") = R.expand_dims(
                    gamma, axis=[0, 2, 3]
                )
                lv17: R.Tensor((1, 64, 112, 112), dtype="float32") = R.multiply(lv15, lv16)
                lv18: R.Tensor((1, 64, 1, 1), dtype="float32") = R.expand_dims(beta, axis=[0, 2, 3])
                lv19: R.Tensor((1, 64, 112, 112), dtype="float32") = R.add(lv17, lv18)
                bn: R.Tuple(
                    R.Tensor((1, 64, 112, 112), dtype="float32"),
                    R.Tensor((64,), dtype="float32"),
                    R.Tensor((64,), dtype="float32"),
                ) = (lv19, moving_mean, moving_var)
                gv: R.Tensor((1, 64, 112, 112), dtype="float32") = bn[0]
                R.output(gv)
            return gv

        @R.function
        def main(
            x: R.Tensor((1, 64, 112, 112), dtype="float32"),
            gamma: R.Tensor((64,), dtype="float32"),
            beta: R.Tensor((64,), dtype="float32"),
            moving_mean: R.Tensor((64,), dtype="float32"),
            moving_var: R.Tensor((64,), dtype="float32"),
        ) -> R.Tensor((1, 64, 112, 112), dtype="float32"):
            with R.dataflow():
                lv: R.Tensor((1, 64, 1, 1), dtype="float32") = R.expand_dims(
                    moving_mean, axis=[0, 2, 3]
                )
                lv1: R.Tensor((1, 64, 112, 112), dtype="float32") = R.subtract(x, lv)
                lv2: R.Tensor((1, 64, 1, 1), dtype="float32") = R.expand_dims(
                    moving_var, axis=[0, 2, 3]
                )
                lv3: R.Tensor((1, 64, 1, 1), dtype="float32") = R.add(
                    lv2, R.const(9.9999997473787516e-06, "float32")
                )
                lv4: R.Tensor((1, 64, 1, 1), dtype="float32") = R.sqrt(lv3)
                lv5: R.Tensor((1, 64, 112, 112), dtype="float32") = R.divide(lv1, lv4)
                lv6: R.Tensor((1, 64, 1, 1), dtype="float32") = R.expand_dims(gamma, axis=[0, 2, 3])
                lv7: R.Tensor((1, 64, 112, 112), dtype="float32") = R.multiply(lv5, lv6)
                lv8: R.Tensor((1, 64, 1, 1), dtype="float32") = R.expand_dims(beta, axis=[0, 2, 3])
                lv9: R.Tensor((1, 64, 112, 112), dtype="float32") = R.add(lv7, lv8)
                bn: R.Tuple(
                    R.Tensor((1, 64, 112, 112), dtype="float32"),
                    R.Tensor((64,), dtype="float32"),
                    R.Tensor((64,), dtype="float32"),
                ) = (lv9, moving_mean, moving_var)
                gv: R.Tensor((1, 64, 112, 112), dtype="float32") = bn[0]
                R.output(gv)
            return gv

    After = relax.transform.DecomposeOpsForInference()(Before)
    tvm.ir.assert_structural_equal(Expected, After)


def test_layer_norm():
    @I.ir_module
    class Before:
        @R.function
        def main(
            x: R.Tensor((4, 64, 112, 112), "float32"),
            gamma: R.Tensor((112, 112), "float32"),
            beta: R.Tensor((112, 112), "float32"),
        ):
            with R.dataflow():
                ln = R.nn.layer_norm(
                    x,
                    gamma,
                    beta,
                    axes=[-2, -1],
                    epsilon=1e-5,
                    center=True,
                    scale=True,
                )
                R.output(ln)
            return ln

    @I.ir_module
    class Expected:
        @R.function
        def main(
            x: R.Tensor((4, 64, 112, 112), dtype="float32"),
            gamma: R.Tensor((112, 112), dtype="float32"),
            beta: R.Tensor((112, 112), dtype="float32"),
        ) -> R.Tensor((4, 64, 112, 112), dtype="float32"):
            with R.dataflow():
                lv: R.Tensor((4, 64, 1, 1), dtype="float32") = R.mean(
                    x, axis=[-2, -1], keepdims=True
                )
                lv1: R.Tensor((4, 64, 112, 112), dtype="float32") = R.subtract(x, lv)
                lv2: R.Tensor((4, 64, 1, 1), dtype="float32") = R.variance(
                    x, axis=[-2, -1], keepdims=True
                )
                lv3: R.Tensor((4, 64, 1, 1), dtype="float32") = R.add(
                    lv2, R.const(9.9999997473787516e-06, "float32")
                )
                lv4: R.Tensor((4, 64, 1, 1), dtype="float32") = R.sqrt(lv3)
                lv5: R.Tensor((4, 64, 112, 112), dtype="float32") = R.divide(lv1, lv4)
                lv6: R.Tensor((4, 64, 112, 112), dtype="float32") = R.multiply(lv5, gamma)
                ln: R.Tensor((4, 64, 112, 112), dtype="float32") = R.add(lv6, beta)
                R.output(ln)
            return ln

    After = relax.transform.DecomposeOpsForTraining()(Before)
    tvm.ir.assert_structural_equal(Expected, After)


def test_op_tensor_to_shape():
    @I.ir_module
    class Before:
        @R.function
        def main(t: R.Tensor([3], dtype="int64")):
            gv: R.Shape(ndim=3) = R.tensor_to_shape(t)
            return gv

    @I.ir_module
    class Expected:
        @R.function
        def main(t: R.Tensor([3], dtype="int64")) -> R.Shape(ndim=3):
            x = T.int64()
            x_1 = T.int64()
            x_2 = T.int64()
            gv: R.Shape(ndim=3) = R.call_pure_packed(
                "vm.builtin.tensor_to_shape", t, ty_args=(R.Shape(ndim=3),)
            )
            y: R.Shape([x, x_1, x_2]) = R.match_cast(gv, R.Shape([x, x_1, x_2]))
            gv_1: R.Shape([x, x_1, x_2]) = R.shape([x, x_1, x_2])
            return gv_1

    After = relax.transform.DecomposeOpsForInference()(Before)
    tvm.ir.assert_structural_equal(Expected, After)


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