# 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.
"""PyTorch integration with nn.Module"""

import inspect
from collections.abc import Callable
from typing import Any

import torch
from tvm_ffi import Array, Shape

from tvm.runtime import Tensor, _tensor
from tvm.runtime.vm import VirtualMachine

from . import core
from . import spec as _spec


class TorchModule:  # pylint: disable=too-few-public-methods
    """A wrapper on top of TVM VirtualMachine that takes torch tensors as inputs and returns torch
    tensors as outputs"""

    spec: _spec.ModuleSpec
    vm: VirtualMachine  # pylint: disable=invalid-name
    params: list[Tensor]
    effects: list[Any]

    def __init__(  # pylint: disable=invalid-name
        self,
        spec: _spec.ModuleSpec,
        vm: VirtualMachine,
        params: list[Tensor],
    ):
        try:
            self.effects = vm["_initialize_effect"]()
        except AttributeError:
            self.effects = None

        self.spec = spec
        self.vm = vm
        self.params = params

    def __getitem__(self, method_name: str) -> Callable:
        def _find_method(method_name):
            for key, value in zip(self.spec.method_names, self.spec.method_specs):
                if method_name == key:
                    return value
            raise ValueError(f"Method `{method_name}` is not found in the module spec. {self.spec}")

        method_spec = _find_method(method_name)
        method = self.vm[method_name]

        def _closure(*args):
            if len(args) != len(method_spec.arg_names):
                raise TypeError(
                    f"Argument length mismatch. Expected {len(method_spec.arg_names)} arguments, "
                    f"but got {len(args)} arguments. The spec is: {method_spec}"
                )
            args = [
                _torch_to_tvm(arg_name, arg_spec, arg)
                for arg_name, arg_spec, arg in zip(
                    method_spec.arg_names, method_spec.arg_specs, args
                )
            ]
            if self.effects is not None:
                outputs, self.effects = method(*args, *self.effects, *self.params)
            else:
                outputs = method(*args, *self.params)
            return _tvm_to_torch(outputs)

        _closure.__name__ = method_name
        return _closure


def _tvm_to_torch(arg):
    if isinstance(arg, list | tuple | Array):
        return [_tvm_to_torch(i) for i in arg]
    if isinstance(arg, _tensor.Tensor):
        return torch.utils.dlpack.from_dlpack(arg)
    if isinstance(arg, Shape):
        return list(arg)
    raise TypeError(f"Unsupported argument type: {type(arg)}")


def _torch_to_tvm(arg_name, arg_spec, arg_torch):
    if isinstance(arg_spec, _spec.Tensor):
        if not isinstance(arg_torch, torch.Tensor):
            raise TypeError(
                f"Expected argument `{arg_name}` to be `torch.Tensor`, but got {type(arg_torch)}"
            )
        return core._from_dlpack(arg_torch)  # pylint: disable=protected-access
    if isinstance(arg_spec, _spec.Int):
        if not isinstance(arg_torch, int):
            raise TypeError(
                f"Expected argument `{arg_name}` to be `int`, but got {type(arg_torch)}"
            )
        return Shape([arg_torch])
    if isinstance(arg_spec, _spec.Tuple):
        return [
            _torch_to_tvm(f"{arg_name}[{i}]", x, arg_torch[i])
            for i, x in enumerate(arg_spec.elements)
        ]
    raise TypeError(f"Unsupported spec item type: {type(arg_spec)}")


def _method_spec_from_torch(
    args_torch: list[Any],
    method: Callable,
):
    def _as_spec(arg_torch):
        if isinstance(arg_torch, torch.Tensor):
            _, dtype = str(arg_torch.dtype).rsplit(".", maxsplit=1)
            return _spec.Tensor(shape=list(arg_torch.shape), dtype=dtype)
        if isinstance(arg_torch, int):
            return _spec.Int()
        raise TypeError(f"Unsupported argument type: {type(arg_torch)}")

    arg_names = list(inspect.signature(method).parameters.keys())
    if len(arg_names) != len(args_torch):
        raise TypeError(f"Expected {len(arg_names)} arguments, but got {len(args_torch)} arguments")
    arg_specs = [_as_spec(i) for i in args_torch]
    return _spec.MethodSpec(method, arg_names, arg_specs, param_mode="plain", effect_mode="plain")
