# 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=unused-argument
"""The base parser for ir module"""

from tvm.ir import GlobalVar

from ...ir_builder import ir as I
from .._core import Parser, dispatch, doc


class ModuleWithGlobalVars:
    """A Module that can add global vars during parsing, to support `Module.function` syntax."""

    def __getattr__(self, attr):
        # Customize the error message.
        # NOTE: `__getattr__` is only called when the attribute access fails with an AttributeError
        raise AttributeError(f"Cannot find the function `{attr}` in the current IRModule")


@dispatch.register(token="ir", type_name="ClassDef")
def _visit_class_def(self: Parser, node: doc.ClassDef) -> None:
    """The class definition visiting method for ir module.

    Parameters
    ----------
    self : Parser
        The visiting parser.

    node : doc.ClassDef
        The doc AST class definition node.
    """

    with self.var_table.with_frame():
        with I.ir_module():
            # Step 0. Add the class name to the var table
            fake_module = ModuleWithGlobalVars()
            self.var_table.add(node.name, fake_module)

            # Step 1: Check if this class inherits from BasePyModule
            is_base_py_module = _check_base_py_module_inheritance(node)
            if is_base_py_module:
                # Store this information in the IRModule for later use
                I.module_attrs({"base_py_module": True})
                # Set the parser context to allow Python functions
                self.set_class_context(node.name, True)
            else:
                # Set the parser context to disallow Python functions
                self.set_class_context(node.name, False)

            # Step 2. Visit non-function stmts, including but not limited to
            # 1. `I.module_attrs`
            # 2. `I.module_global_infos`
            with self.with_dispatch_token("ir"):
                for stmt in node.body:
                    if not isinstance(stmt, doc.FunctionDef):
                        self.visit(stmt)

            # Step 3. Visit function stmts to declare the global vars
            for stmt in node.body:
                if isinstance(stmt, doc.FunctionDef):
                    global_var = self.visit_tvm_declare_function(stmt)
                    fake_module.__setattr__(stmt.name, global_var)

            # Step 4. Visit and parse the functions
            with self.with_dispatch_token("ir"):
                for stmt in node.body:
                    if isinstance(stmt, doc.FunctionDef):
                        self.visit(stmt)


@dispatch.register(token="ir", type_name="Assign")
def _visit_assign(self: Parser, node: doc.Assign) -> None:
    """The assign visiting method for ir module.

    Parameters
    ----------
    self : Parser
        The visiting parser.

    node : doc.ClassDef
        The doc AST assign node.
    """
    if len(node.targets) != 1:
        self.report_error(node, "Consequential assignments like 'a = b = c' are not supported.")
    lhs = node.targets[0].id
    rhs = self.eval_expr(node.value)

    I.decl_function(lhs, rhs)
    I.def_function(lhs, rhs)


@dispatch.register(token="ir", type_name="Expr")
def _visit_expr(self: Parser, node: doc.Expr) -> None:
    """The expression visiting method for ir module.

    Parameters
    ----------
    self : Parser
        The visiting parser.

    node : doc.ClassDef
        The doc AST expression node.
    """
    self.eval_expr(node.value)


@dispatch.register(token="default", type_name="Assign")
def visit_assign(self: Parser, node: doc.Assign) -> None:
    if len(node.targets) != 1:
        self.report_error(node, "Consequential assignments like 'a = b = c' are not supported.")
    lhs = node.targets[0]
    rhs = self.eval_expr(node.value)
    self.eval_assign(
        target=lhs, source=rhs, bind_value=lambda _a, _b, _c, value: value, allow_shadowing=True
    )


@dispatch.register(token="default", type_name="pre_visit_local_function")
def pre_visit_local_function(self: Parser, node: doc.Expr) -> None:
    pass


@dispatch.register(token="default", type_name="post_visit_local_function")
def post_visit_local_function(self: Parser, node: doc.Expr) -> None:
    pass


@dispatch.register(token="pyfunc", type_name="tvm_declare_function")
def visit_tvm_declare_function(self: Parser, node: doc.FunctionDef) -> GlobalVar:
    """Declare a Python function as an ExternFunc in the IRModule."""
    # Check if Python functions are allowed in this context
    # We need to check if we're in a class that inherits from BasePyModule
    current_class = self._get_current_class_context()
    if current_class and not self._is_base_py_module_context():
        self.report_error(
            node,
            "@I.pyfunc are only allowed in classes that inherit from BasePyModule. "
            f"Class '{current_class}' does not inherit from BasePyModule.",
        )

    # 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 import ExternFunc  # pylint: disable=import-outside-toplevel

    # Create ExternFunc with proper attributes for Python functions
    func = ExternFunc(node.name)
    func = func.with_attr("is_pyfunc", True)
    func = func.with_attr("function_type", "python")
    func = func.with_attr("python_function_name", node.name)

    # Add placeholder attributes that will be filled in later
    func = func.with_attr("python_source", f"# Source will be filled for {node.name}")
    func = func.with_attr("python_packed_func", None)  # Will be filled in entry.py

    # Store the function name for later retrieval
    return I.decl_function(node.name, func)


@dispatch.register(token="pyfunc", type_name="FunctionDef")
def visit_function_def(self: Parser, node: doc.FunctionDef) -> None:
    """Visit Python function definition - no need to parse the body."""
    # Python function body is not parsed in TVMScript


def _check_base_py_module_inheritance(node: doc.ClassDef) -> bool:
    """Check if a class inherits from BasePyModule.

    Parameters
    ----------
    node : doc.ClassDef
        The class definition node to check.

    Returns
    -------
    bool
        True if the class inherits from BasePyModule, False otherwise.
    """
    if not node.bases:
        return False

    # Check each base class
    for base in node.bases:
        if hasattr(base, "id"):
            if base.id == "BasePyModule":
                return True
        elif hasattr(base, "attr"):
            if base.attr == "BasePyModule":
                return True
        elif hasattr(base, "value") and hasattr(base.value, "id"):
            if (
                base.value.id in ["BasePyModule", "tvm", "relax"]
                and hasattr(base, "attr")
                and base.attr == "BasePyModule"
            ):
                return True

    return False
