# 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 numpy as np
import pytest

import tvm
import tvm.testing
from tvm.relax.frontend import nn


class ParamContainerModule(nn.Module):
    def __init__(self):
        self.list_params = nn.ParameterList(
            [
                nn.Parameter((4,), "float32"),
                nn.Parameter((4,), "float32"),
            ]
        )
        self.dict_params = nn.ParameterDict(
            {
                "foo": nn.Parameter((4,), "float32"),
                "bar": nn.Parameter((4,), "float32"),
            }
        )


def test_parameter_list_basic_behavior():
    p0 = nn.Parameter((4,), "float32")
    p1 = nn.Parameter((4,), "float32")
    params = nn.ParameterList([p0])
    params.append(p1)

    assert len(params) == 2
    assert params[0] is p0
    assert list(params) == [p0, p1]

    p2 = nn.Parameter((4,), "float32")
    params[1] = p2
    assert params[1] is p2

    p3 = nn.Parameter((4,), "float32")
    params.extend([p3])
    assert list(params) == [p0, p2, p3]


def test_parameter_dict_basic_behavior():
    p0 = nn.Parameter((4,), "float32")
    p1 = nn.Parameter((4,), "float32")
    params = nn.ParameterDict({"foo": p0})
    params["bar"] = p1

    assert len(params) == 2
    assert params["foo"] is p0
    assert "bar" in params
    assert list(params) == ["foo", "bar"]
    assert list(params.keys()) == ["foo", "bar"]
    assert list(params.values()) == [p0, p1]
    assert list(params.items()) == [("foo", p0), ("bar", p1)]
    assert params.get("foo") is p0

    p2 = nn.Parameter((4,), "float32")
    params.update({"baz": p2})
    assert list(params.keys()) == ["foo", "bar", "baz"]
    assert params.pop("baz") is p2
    params.clear()
    assert len(params) == 0


def test_type_validation():
    with pytest.raises(TypeError):
        nn.ParameterList([object()])

    with pytest.raises(TypeError):
        nn.ParameterDict({"bad": object()})

    with pytest.raises(TypeError):
        nn.ParameterDict({1: nn.Parameter((4,), "float32")})

    with pytest.raises(TypeError):
        nn.ParameterList()[0] = object()


def test_named_parameters_parameters_and_state_dict():
    m = ParamContainerModule()

    expected = [
        "list_params.0",
        "list_params.1",
        "dict_params.foo",
        "dict_params.bar",
    ]

    assert list(m.state_dict().keys()) == expected
    assert [name for name, _ in m.named_parameters()] == expected
    assert len(list(m.parameters())) == 4


def test_nested_traversal_through_module_dict():
    class Inner(nn.Module):
        def __init__(self):
            self.params = nn.ParameterList([nn.Parameter((4,), "float32")])

    class Outer(nn.Module):
        def __init__(self):
            self.blocks = nn.ModuleDict({"inner": Inner()})

    m = Outer()
    assert list(m.state_dict().keys()) == ["blocks.inner.params.0"]


def test_nested_traversal_through_module_list():
    class Inner(nn.Module):
        def __init__(self):
            self.params = nn.ParameterList([nn.Parameter((4,), "float32")])

    class Outer(nn.Module):
        def __init__(self):
            self.blocks = nn.ModuleList([Inner()])

    m = Outer()
    assert list(m.state_dict().keys()) == ["blocks.0.params.0"]


def test_to_dtype():
    m = ParamContainerModule()
    m.to(dtype="float16")

    assert m.list_params[0].dtype == "float16"
    assert m.list_params[1].dtype == "float16"
    assert m.dict_params["foo"].dtype == "float16"
    assert m.dict_params["bar"].dtype == "float16"


def test_load_state_dict():
    m = ParamContainerModule()
    p0 = nn.Parameter((4,), "float32")
    p0.data = np.full((4,), 1.0, dtype="float32")
    p1 = nn.Parameter((4,), "float32")
    p1.data = np.full((4,), 2.0, dtype="float32")
    p2 = nn.Parameter((4,), "float32")
    p2.data = np.full((4,), 3.0, dtype="float32")
    p3 = nn.Parameter((4,), "float32")
    p3.data = np.full((4,), 4.0, dtype="float32")
    state_dict = {
        "list_params.0": p0,
        "list_params.1": p1,
        "dict_params.foo": p2,
        "dict_params.bar": p3,
    }

    missing_keys, unexpected_keys = m.load_state_dict(state_dict)

    assert missing_keys == []
    assert unexpected_keys == []
    tvm.testing.assert_allclose(m.list_params[0].data.numpy(), np.full((4,), 1.0, "float32"))
    tvm.testing.assert_allclose(m.list_params[1].data.numpy(), np.full((4,), 2.0, "float32"))
    tvm.testing.assert_allclose(m.dict_params["foo"].data.numpy(), np.full((4,), 3.0, "float32"))
    tvm.testing.assert_allclose(m.dict_params["bar"].data.numpy(), np.full((4,), 4.0, "float32"))


def test_export_tvm_parameter_names():
    class M(nn.Module):
        def __init__(self):
            self.biases = nn.ParameterList(
                [
                    nn.Parameter((4,), "float32"),
                    nn.Parameter((4,), "float32"),
                ]
            )
            self.scales = nn.ParameterDict({"main": nn.Parameter((4,), "float32")})

        def forward(self, x):
            return x + self.biases[0] + self.biases[1] + self.scales["main"]

    _, params = M().export_tvm(
        spec={"forward": {"x": nn.spec.Tensor((4,), "float32")}},
        debug=False,
    )
    assert [name for name, _ in params] == ["biases.0", "biases.1", "scales.main"]


def test_mutator_parameter_container_names():
    seen = []

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

    m = ParamContainerModule()
    Recorder().visit_module("", m)

    assert seen == [
        "list_params.0",
        "list_params.1",
        "dict_params.foo",
        "dict_params.bar",
    ]


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