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

from typing import Any

import tvm
from tvm.relax.frontend import nn


def test_mutator_naming_basic():
    class Module0(nn.Module):
        def __init__(self) -> None:
            super().__init__()
            self.param0 = nn.Parameter((32, 128), "float64")

    class Module1(nn.Module):
        def __init__(self) -> None:
            super().__init__()
            self.mod0 = Module0()
            self.param1 = nn.Parameter((32, 128), "float32")

    class Module2(nn.Module):
        def __init__(self) -> None:
            super().__init__()
            self.mod1 = Module1()
            self.param2 = nn.Parameter((32, 128), "float16")

    class Module3(nn.Module):
        def __init__(self) -> None:
            super().__init__()
            self.mod2 = Module2()
            self.param3 = nn.Parameter((32, 128), "float8")

    class Mutator(nn.Mutator):
        def visit_param(self, name: str, node: nn.Parameter) -> Any:
            if node.dtype == "float8":
                assert name == "mod3.param3"
                return node
            elif node.dtype == "float16":
                assert name == "mod3.mod2.param2"
                return node
            elif node.dtype == "float32":
                assert name == "mod3.mod2.mod1.param1"
                return node
            elif node.dtype == "float64":
                assert name == "mod3.mod2.mod1.mod0.param0"
                return node

    mod3 = Module3()
    mutator = Mutator()
    mutator.visit("mod3", mod3)


def test_mutator_naming_moduledict():
    class Module(nn.Module):
        def __init__(self, dtype) -> None:
            super().__init__()
            self.param = nn.Parameter((32, 128), dtype)

    class Mutator(nn.Mutator):
        def visit_param(self, name: str, node: nn.Parameter) -> Any:
            if node.dtype == "float64":
                assert name == "mod_dict.k0.0.param"
                return node
            elif node.dtype == "float32":
                assert name == "mod_dict.k0.1.param"
                return node
            elif node.dtype == "float16":
                assert name == "mod_dict.k1.0.param"
                return node
            elif node.dtype == "float8":
                assert name == "mod_dict.k1.1.param"
                return node

    mod_dict = nn.ModuleDict(
        {
            "k0": nn.ModuleList([Module("float64"), Module("float32")]),
            "k1": nn.ModuleList([Module("float16"), Module("float8")]),
        }
    )
    mutator = Mutator()
    mutator.visit("mod_dict", mod_dict)


def test_mutator_naming_modulelist():
    class Module(nn.Module):
        def __init__(self, dtype) -> None:
            super().__init__()
            self.param = nn.Parameter((32, 128), dtype)

    class Mutator(nn.Mutator):
        def visit_param(self, name: str, node: nn.Parameter) -> Any:
            if node.dtype == "float64":
                assert name == "mod_list.0.0.param"
                return node
            elif node.dtype == "float32":
                assert name == "mod_list.0.1.param"
                return node
            elif node.dtype == "float16":
                assert name == "mod_list.1.0.param"
                return node
            elif node.dtype == "float8":
                assert name == "mod_list.1.1.param"
                return node

    mod_list = nn.ModuleList(
        [
            nn.ModuleList([Module("float64"), Module("float32")]),
            nn.ModuleList([Module("float16"), Module("float8")]),
        ]
    )
    mutator = Mutator()
    mutator.visit("mod_list", mod_list)


def test_mutator_naming_parameter_containers():
    class Module(nn.Module):
        def __init__(self) -> None:
            super().__init__()
            self.param_list = nn.ParameterList(
                [
                    nn.Parameter((32, 128), "float64"),
                    nn.Parameter((32, 128), "float32"),
                ]
            )
            self.param_dict = nn.ParameterDict(
                {
                    "k0": nn.Parameter((32, 128), "float16"),
                    "k1": nn.Parameter((32, 128), "float8"),
                }
            )

    seen = []

    class Mutator(nn.Mutator):
        def visit_param(self, name: str, node: nn.Parameter) -> Any:
            seen.append((name, node.dtype))
            return node

    module = Module()
    mutator = Mutator()
    mutator.visit("", module)

    assert seen == [
        ("param_list.0", "float64"),
        ("param_list.1", "float32"),
        ("param_dict.k0", "float16"),
        ("param_dict.k1", "float8"),
    ]


def test_mutator_module():
    class SubModule1(nn.Module):
        def __init__(self) -> None:
            super().__init__()

    class SubModule2(nn.Module):
        def __init__(self) -> None:
            super().__init__()

    class Module(nn.Module):
        def __init__(self) -> None:
            super().__init__()
            self.mod = SubModule1()

    class Mutator(nn.Mutator):
        def visit_module(self, name: str, node: nn.Module) -> Any:
            if isinstance(node, SubModule1):
                return SubModule2()
            else:
                return node

    mutator = Mutator()
    module = Module()
    assert isinstance(module.mod, SubModule1)
    module = mutator.visit("", module)
    assert isinstance(module.mod, SubModule2)


def test_mutator_moduledict():
    class Module1(nn.Module):
        def __init__(self) -> None:
            super().__init__()

    class Module2(nn.Module):
        def __init__(self) -> None:
            super().__init__()

    class Module3(nn.Module):
        def __init__(self) -> None:
            super().__init__()

    class Mutator(nn.Mutator):
        def visit_module(self, name: str, node: nn.Module) -> Any:
            if isinstance(node, Module3):
                return Module1()
            else:
                return node

    mutator = Mutator()
    module_dict = nn.ModuleDict({"k0": Module1(), "k1": Module2(), "k2": Module3()})
    assert isinstance(module_dict["k0"], Module1)
    assert isinstance(module_dict["k1"], Module2)
    assert isinstance(module_dict["k2"], Module3)
    module_dict = mutator.visit("", module_dict)
    assert isinstance(module_dict["k0"], Module1)
    assert isinstance(module_dict["k1"], Module2)
    assert isinstance(module_dict["k2"], Module1)


def test_mutator_modulelist():
    class Module1(nn.Module):
        def __init__(self) -> None:
            super().__init__()

    class Module2(nn.Module):
        def __init__(self) -> None:
            super().__init__()

    class Module3(nn.Module):
        def __init__(self) -> None:
            super().__init__()

    class Mutator(nn.Mutator):
        def visit_module(self, name: str, node: nn.Module) -> Any:
            if isinstance(node, Module3):
                return Module1()
            else:
                return node

    mutator = Mutator()
    module_list = nn.ModuleList([Module1(), Module2(), Module3()])
    assert isinstance(module_list[0], Module1)
    assert isinstance(module_list[1], Module2)
    assert isinstance(module_list[2], Module3)
    module_list = mutator.visit("", module_list)
    assert isinstance(module_list[0], Module1)
    assert isinstance(module_list[1], Module2)
    assert isinstance(module_list[2], Module1)


def test_mutator_effect():
    class Effect1(nn.Effect):
        pass

    class Effect2(nn.Effect):
        pass

    class Module(nn.Module):
        def __init__(self) -> None:
            super().__init__()
            self.effect = Effect1()

    class Mutator(nn.Mutator):
        def visit_effect(self, name: str, node: nn.Effect) -> Any:
            if isinstance(node, Effect1):
                return Effect2()

    mutator = Mutator()
    module = Module()
    assert isinstance(module.effect, Effect1)
    module = mutator.visit("", module)
    assert isinstance(module.effect, Effect2)


def test_mutator_param():
    class Module(nn.Module):
        def __init__(self) -> None:
            super().__init__()
            self.weight = nn.Parameter((128, 64), "float16")

    class Mutator(nn.Mutator):
        def visit_param(self, name: str, node: nn.Parameter) -> Any:
            if node.dtype == "float16":
                return nn.Parameter(node.shape, "float32")

    mutator = Mutator()
    module = Module()
    assert module.weight.dtype == "float16"
    module = mutator.visit("", module)
    assert module.weight.dtype == "float32"


def test_mutator_recursively():
    class SubModule(nn.Module):
        def __init__(self) -> None:
            super().__init__()
            self.weight = nn.Parameter((128, 64), "float16")

    class Module(nn.Module):
        def __init__(self) -> None:
            super().__init__()
            self.mod = SubModule()

    class Mutator(nn.Mutator):
        def visit_param(self, name: str, node: nn.Parameter) -> Any:
            if node.dtype == "float16":
                return nn.Parameter(node.shape, "float32")

    mutator = Mutator()
    module = Module()
    assert module.mod.weight.dtype == "float16"
    module = mutator.visit("", module)
    assert module.mod.weight.dtype == "float32"


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