# 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.
# pylint: disable=import-outside-toplevel
"""The entry point of TVM parser for ir module."""

import inspect
from collections.abc import Callable

from tvm import cpu, ir
from tvm.ir import GlobalVar, IRModule

from .._core import parse, utils


# this formulation allows us to support having @I.ir_module
# appear as a decorator by itself or to have optional arguments
# like @I.ir_module(check_well_formed=False)
def ir_module(
    mod: type | None = None, check_well_formed: bool = True, s_tir: bool = False
) -> IRModule:
    """The parsing method for ir module, by using `@ir_module` as decorator.

    Parameters
    ----------
    mod : Type
        The class to be parsed as ir module.

    check_well_formed : bool
        Whether to check well-formedness during parsing.

    Returns
    -------
    ir_module : IRModule
        The parsed ir module.
    """

    # Capture stack outside wrapper (wrapper adds to the stack)
    outer_stack = inspect.stack()

    def decorator_wrapper(mod):
        if not inspect.isclass(mod):
            raise TypeError(f"Expect a class, but got: {mod}")

        # Check BasePyModule inheritance
        base_py_module_inherited = any(base.__name__ == "BasePyModule" for base in mod.__bases__)

        extra_vars = utils.inspect_class_capture(mod)
        # Resolve closure variables hidden by PEP 563 (annotation-only names)
        utils.resolve_closure_vars(mod, extra_vars, outer_stack)
        m = parse(mod, extra_vars, check_well_formed=check_well_formed, s_tir=s_tir)

        if base_py_module_inherited:
            # Lazy import: tvm.relax cannot be imported at module level in tvm.script.parser
            # because tvm.script is loaded before tvm.relax during tvm initialization.
            from tvm.relax.base_py_module import BasePyModule
            from tvm.relax.expr import ExternFunc  # pylint: disable=import-outside-toplevel

            # Collect pyfunc methods
            pyfunc_methods = [
                name
                for name, attr in mod.__dict__.items()
                if hasattr(attr, "dispatch_token") and attr.dispatch_token == "pyfunc"
            ]

            mod._pyfunc_methods = pyfunc_methods

            # Create ExternFunc nodes

            for method_name in pyfunc_methods:
                try:
                    existing_gvars = [
                        global_var
                        for global_var in m.get_global_vars()
                        if global_var.name_hint == method_name
                    ]

                    extern_func = ExternFunc(method_name)
                    extern_func = extern_func.with_attr("is_pyfunc", True)
                    extern_func = extern_func.with_attr("function_type", "python")
                    extern_func = extern_func.with_attr("python_function_name", method_name)
                    extern_func = extern_func.with_attr(
                        "python_source", f"# Source for {method_name}"
                    )
                    extern_func = extern_func.with_attr("python_packed_func", None)

                    if existing_gvars:
                        m[existing_gvars[0]] = extern_func
                    else:
                        m[GlobalVar(method_name)] = extern_func

                except Exception:  # pylint: disable=broad-exception-caught
                    continue

            class ModuleFactory:
                """Factory class for creating BasePyModule instances with Python functions."""

                def __init__(self, module, pyfunc_methods, original_class):
                    self.ir_module = module
                    self.pyfunc_methods = pyfunc_methods
                    self.original_class = original_class

                def __call__(self, device=None, target=None):
                    if device is None:
                        device = cpu(0)

                    instance_ir_mod = ir.IRModule()
                    for global_var, func in self.ir_module.functions_items():
                        instance_ir_mod[global_var] = func

                    instance = BasePyModule(instance_ir_mod, device, target)

                    for method_name in self.pyfunc_methods:
                        if hasattr(self.original_class, method_name):
                            method = getattr(self.original_class, method_name)
                            instance.add_python_function(method_name, method)

                    return instance

                def __getattr__(self, name):
                    if hasattr(self.ir_module, name):
                        return getattr(self.ir_module, name)
                    raise AttributeError(
                        f"'{self.__class__.__name__}' object has no attribute '{name}'"
                    )

            factory = ModuleFactory(m, pyfunc_methods, mod)
            setattr(factory, "__name__", mod.__name__)
            return factory

        setattr(m, "__name__", mod.__name__)
        return m

    if mod is not None:
        # if there are no optional args given, this will directly invoke the wrapper
        return decorator_wrapper(mod)
    else:
        # if there is a optional arg given, it returns the wrapper function
        # as a new decorator and applies it
        setattr(decorator_wrapper, "dispatch_token", "ir")
        return decorator_wrapper


def pyfunc(func: Callable):
    # Set the dispatch_token on the decorated function
    setattr(func, "dispatch_token", "pyfunc")
    return func


setattr(pyfunc, "dispatch_token", "pyfunc")
