# 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.
"""The entry point of TVM parser for tirx."""

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

from tvm.ir.base import deprecated
from tvm.script.parser._core import parse, scan_macro, utils
from tvm.script.parser.core.parser import Parser, ScriptMacro, VarTable
from tvm.tirx import Buffer, PrimFunc
from tvm.tirx.script.builder import block_name_suffix_context, buffer, ptr


def prim_func(
    func: Callable | None = None,
    private: bool = False,
    check_well_formed=True,
    s_tir: bool = False,
    persistent: bool = False,
) -> PrimFunc | Callable:
    """The parsing method for tirx prim func, by using `@prim_func` as decorator.

    Parameters
    ----------
    func : Callable
        The function to be parsed as prim func.
        (Listed as optional to allow the decorator to be used
        without arguments, like `@prim_func`,
        or with an argument, `@prim_func(private=True)`)

    private : bool, optional
        Whether the function should be treated as private.
        A private function has no global symbol attribute;
        if the function is not private, it will have a global symbol
        matching the function name.

    Returns
    -------
    res : Union[PrimFunc, Callable]
        The parsed tirx prim func.
    """
    # pylint: disable=unused-argument
    # (private will be used in the parser, but not immediately)

    # need to capture this var outside the wrapper because the wrapper
    # adds to the stack
    outer_stack = inspect.stack()

    def decorator_wrapper(func):
        if not inspect.isfunction(func):
            raise TypeError(f"Expect a function, but got: {func}")
        if utils.is_defined_in_class(outer_stack, func):
            return func
        extra_vars = utils.inspect_function_capture(func)
        utils.resolve_closure_vars(func, extra_vars, outer_stack)
        f = parse(func, extra_vars, check_well_formed=check_well_formed, s_tir=s_tir)
        setattr(f, "__name__", func.__name__)
        return f

    if func is not None:
        # no optional args given => use wrapper directly
        return decorator_wrapper(func)
    else:
        # if there is an optional arg given, return a new decorator
        # that will then be invoked
        setattr(decorator_wrapper, "dispatch_token", "tirx")
        return decorator_wrapper


setattr(prim_func, "dispatch_token", "tirx")


class TIRInline(ScriptMacro):
    """Specialization of ScriptMacro for TIR with Python LEGB scoping.

    Two definition paths:
    1. Outside @T.prim_func (standalone @T.inline): definition_depth is None,
       closure_vars captured at definition time are used (module globals are
       effectively late-bound since they don't change during parsing).
    2. Inside @T.prim_func (inline def in parsed body): definition_depth is set
       to the VarTable frame depth at definition time, and defining_var_table
       stores a reference to the VarTable that was active. At call time,
       defining_var_table.get_at_depth(definition_depth) reads current values
       from the lexically enclosing frames.

    Attributes
    ----------
    definition_depth : Optional[int]
        VarTable frame depth at definition time, or None for outside-prim_func.
    defining_var_table : Optional[VarTable]
        Reference to the VarTable that was active at definition time.
    call_count : int
        Counter for unique block name suffixes.
    """

    def __init__(
        self,
        source,
        closure_vars: dict[str, Any],
        func: Callable,
        definition_depth: int | None = None,
        defining_var_table: VarTable | None = None,
    ) -> None:
        # hygienic=True for the base class (field kept for compat but not used in dispatch)
        super().__init__(source, closure_vars, func, hygienic=True)
        self.definition_depth = definition_depth
        self.defining_var_table = defining_var_table
        self.call_count = 0

    def parse_macro(self, parser: Parser) -> None:
        macro_def = self.get_macro_def()
        suffix = f"_{self.call_count}" if self.call_count > 0 else ""
        self.call_count += 1
        with block_name_suffix_context(suffix):
            parser.visit_body(macro_def.body)

    def __call__(self, *args, **kwargs):
        param_binding = inspect.signature(self.func).bind(*args, **kwargs)
        param_binding.apply_defaults()
        local_vars = param_binding.arguments
        parser = self._find_parser_def()

        with parser.with_diag_source(self.source):
            if self.defining_var_table is not None:
                # Inside-prim_func path: LEGB late binding from the defining scope
                enclosing_vars = self.defining_var_table.get_at_depth(self.definition_depth)
            else:
                # Outside-prim_func path: use captured closure vars
                enclosing_vars = self.closure_vars

            saved_var_table = parser.var_table
            parser.var_table = VarTable()

            with parser.var_table.with_frame():
                for k, v in enclosing_vars.items():
                    parser.var_table.add(k, v)
                with parser.var_table.with_frame():
                    for k, v in local_vars.items():
                        parser.var_table.add(k, v)

                    parse_result = self.parse_macro(parser)

            parser.var_table = saved_var_table

        return parse_result


def inline(*args, definition_depth: int | None = None, defining_var_table=None) -> Callable:
    """Decorator for inline function definitions with Python LEGB scoping.

    @T.inline follows Python's lexical scoping with late binding:
    - At definition time, record which scopes are visible.
    - At call time, read current values from those scopes.

    Example::

        import tvm
        from tvm.script import tirx as T

        x_value = 128

        @T.inline
        def capture(A, B):
            B[()] = A[x_value]          # x_value resolved from enclosing scope

        @T.prim_func(s_tir=True)
        def use(A: T.Buffer((1024,), "int32"), B: T.Buffer((), "int32")) -> None:
            capture(A, B)               # Produces B[()] = A[128]
    """

    def _decorator(func: Callable) -> Callable:
        source, closure_vars = scan_macro(func, utils.inspect_function_capture(func))
        obj = TIRInline(
            source,
            closure_vars,
            func,
            definition_depth=definition_depth,
            defining_var_table=defining_var_table,
        )

        def wrapper(*args, **kwargs):
            return obj(*args, **kwargs)

        return wrapper

    if len(args) == 0:
        setattr(_decorator, "dispatch_token", "tir.inline")
        return _decorator
    if len(args) == 1 and inspect.isfunction(args[0]):
        return _decorator(args[0])

    raise ValueError("Invalid use of T.inline. Usage: @T.inline or @T.inline()")


setattr(inline, "dispatch_token", "tir.inline")


class TIRJit:
    """Top-level kernel decorator with compile-time ``.specialize()`` params.

    Parses the function body lazily: parsing is deferred until ``.specialize()``
    supplies concrete values for the params annotated as ``T.constexpr``. The
    return type of ``.specialize()`` is a ``tvm.tirx.PrimFunc``, identical in
    type to what ``@T.prim_func`` produces today.

    Constexpr params are removed from the resulting PrimFunc's parameter list;
    their values are baked into the IR (e.g. into ``T.Buffer((M, K), ...)``
    shape annotations and into the body).
    """

    def __init__(
        self,
        func: Callable,
        check_well_formed: bool = True,
        is_stir: bool = False,
        persistent: bool = False,
        private: bool = False,
    ) -> None:
        self.func = func
        self.check_well_formed = check_well_formed
        self.is_stir = is_stir
        self.persistent = persistent  # pylint: disable=unused-private-member
        self.private = private  # pylint: disable=unused-private-member
        # Resolved closure vars (computed once; the function itself is the
        # capture point, so this never changes between specializations).
        self._closure_vars: dict[str, Any] = utils.inspect_function_capture(func)
        # Detect which params are marked T.constexpr or T.Optional. With PEP 563
        # (``from __future__ import annotations``), each annotation is a
        # string; we eval them one-by-one so a constexpr probe is not
        # blocked by sibling annotations that reference yet-undefined names
        # (e.g. ``A: T.Buffer((N,), ...)`` referencing constexpr ``N``).
        raw_anns = getattr(func, "__annotations__", {}) or {}
        eval_globals = {**func.__globals__, **self._closure_vars}
        sig = inspect.signature(func)
        constexpr_names: set[str] = set()
        constexpr_defaults: dict[str, Any] = {}
        optional_names: set[str] = set()
        for name, param in sig.parameters.items():
            raw_ann = raw_anns.get(name)
            ann = raw_ann
            if isinstance(ann, str):
                try:
                    ann = eval(ann, eval_globals)  # pylint: disable=eval-used
                except Exception:  # pylint: disable=broad-except
                    ann = None
            if ann is constexpr:
                constexpr_names.add(name)
                if param.default is not inspect.Parameter.empty:
                    constexpr_defaults[name] = param.default
            if isinstance(ann, _OptionalAnnotation) or _is_explicit_optional_annotation(raw_ann):
                optional_names.add(name)
        self.constexpr_names: frozenset[str] = frozenset(constexpr_names)
        self.constexpr_defaults: dict[str, Any] = constexpr_defaults
        self.optional_names: frozenset[str] = frozenset(optional_names)
        self._cache: dict[tuple, PrimFunc] = {}

    def specialize(self, **specialization_kwargs) -> PrimFunc:
        """Build a PrimFunc by binding constexprs and absent optional params.

        Parameters
        ----------
        **specialization_kwargs
            One value per ``T.constexpr``-annotated parameter.  A
            ``T.Optional`` parameter may additionally be supplied as ``None``
            to remove it from the resulting PrimFunc ABI.  Omitting an
            optional parameter keeps it as a normal runtime parameter.

        Returns
        -------
        PrimFunc
            A concrete TIRx PrimFunc, identical in type to the output of
            ``@T.prim_func``.
        """
        specializable_names = self.constexpr_names | self.optional_names
        extra = specialization_kwargs.keys() - specializable_names
        if extra:
            raise TypeError(
                f"{self.func.__name__}.specialize() got unexpected arg(s): "
                f"{sorted(extra)} (specializable params are: {sorted(specializable_names)})"
            )
        invalid_optional = {
            name: specialization_kwargs[name]
            for name in self.optional_names & specialization_kwargs.keys()
            if specialization_kwargs[name] is not None
        }
        if invalid_optional:
            raise TypeError(
                f"{self.func.__name__}.specialize(): T.Optional parameters only accept None; "
                f"pass actual tensors when calling the compiled kernel (got: {invalid_optional!r})"
            )

        supplied_constexprs = {
            name: specialization_kwargs[name]
            for name in self.constexpr_names & specialization_kwargs.keys()
        }
        effective = {**self.constexpr_defaults, **supplied_constexprs}
        missing = self.constexpr_names - effective.keys()
        if missing:
            raise TypeError(
                f"{self.func.__name__}.specialize() missing constexpr arg(s) "
                f"(no default provided): {sorted(missing)}"
            )

        absent_params = {name: None for name in self.optional_names & specialization_kwargs.keys()}
        try:
            cache_key = (tuple(sorted(effective.items())), tuple(sorted(absent_params)))
            cached = self._cache.get(cache_key)
        except TypeError as err:
            raise TypeError(
                f"{self.func.__name__}.specialize(): all constexpr values must "
                f"be hashable (got: {effective!r})"
            ) from err
        if cached is not None:
            return cached

        extra_vars = {**self._closure_vars, **effective}
        prim_func = parse(
            self.func,
            extra_vars,
            check_well_formed=self.check_well_formed,
            s_tir=self.is_stir,
            absent_params=absent_params,
        )
        setattr(prim_func, "__name__", self.func.__name__)
        self._cache[cache_key] = prim_func
        return prim_func


def jit(
    func: Callable | None = None,
    private: bool = False,
    check_well_formed: bool = True,
    is_stir: bool = False,
    persistent: bool = False,
) -> "TIRJit | Callable":
    """Decorator: capture the kernel and defer parsing until ``.specialize()``.

    Use ``@T.jit`` (instead of ``@T.prim_func``) when the kernel takes
    compile-time parameters annotated with ``T.constexpr`` or runtime
    parameters that may be removed with ``T.Optional``. The resulting object
    exposes ``.specialize(**specialization_kwargs)``, which returns a
    ``tvm.tirx.PrimFunc``.

    Example::

        from tvm.script import tirx as T

        @T.jit
        def add(
            A: T.Buffer((N,), "float32"),
            B: T.Buffer((N,), "float32"),
            *,
            N: T.constexpr,
        ):
            ...

        kernel = add.specialize(N=1024)  # returns a PrimFunc

        @T.jit
        def guarded(optional: T.Optional(T.handle), out: T.handle):
            if optional is not None:
                ...

        present = guarded.specialize()
        absent = guarded.specialize(optional=None)
    """

    def decorator_wrapper(func: Callable) -> TIRJit:
        if not inspect.isfunction(func):
            raise TypeError(f"Expect a function, but got: {func}")
        return TIRJit(
            func,
            check_well_formed=check_well_formed,
            is_stir=is_stir,
            persistent=persistent,
            private=private,
        )

    if func is not None:
        return decorator_wrapper(func)
    setattr(decorator_wrapper, "dispatch_token", "tirx")
    return decorator_wrapper


setattr(jit, "dispatch_token", "tirx")


class TIRMacro(ScriptMacro):
    """Specialization of the ScriptMacro class for TIR.

    Apache-compatible hygienic macro. Distinct from ``TIRInline`` (which
    uses Python LEGB late binding) so upstream code that relies on
    capture-at-definition-time semantics keeps working.

    Attributes
    ----------
    call_count : int
        Counter for the number of times this macro has been invoked.
        Used to generate unique block name suffixes.
    """

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.call_count = 0

    def parse_macro(self, parser: Parser) -> None:
        macro_def = self.get_macro_def()
        suffix = f"_{self.call_count}" if self.call_count > 0 else ""
        self.call_count += 1
        with block_name_suffix_context(suffix):
            parser.visit_body(macro_def.body)


def macro(*args, hygienic: bool = True) -> Callable:
    """Decorator for macro definitions with hygienic capture.

    Parameters
    ----------
    hygienic: bool
        Specifies whether the macro is hygienic or not. A hygienic macro
        resolves symbols at definition time; a non-hygienic macro at use
        time. Defaults to ``True``.
    """

    def _decorator(func: Callable) -> TIRMacro:
        source, closure_vars = scan_macro(func, utils.inspect_function_capture(func))
        obj = TIRMacro(source, closure_vars, func, hygienic)

        def wrapper(*args, **kwargs):
            return obj(*args, **kwargs)

        return wrapper

    if len(args) == 0:
        return _decorator
    if len(args) == 1 and inspect.isfunction(args[0]):
        return _decorator(args[0])

    raise ValueError("Invalid use of T.macro. Usage: @T.macro or @T.macro()")


setattr(macro, "dispatch_token", "tir.macro")


class BufferProxy:
    """Buffer proxy class for constructing tirx buffer."""

    def __or__(self, other):
        """Support ``T.Buffer | None`` union syntax in annotations."""
        return self

    def __ror__(self, other):
        """Support ``None | T.Buffer`` union syntax in annotations."""
        return self

    def __call__(
        self,
        shape,
        dtype="float32",
        data=None,
        strides=None,
        elem_offset=None,
        byte_offset=None,
        scope="global",
        align=0,
        offset_factor=0,
        layout="default",
    ) -> Buffer:
        return buffer(
            shape,
            dtype=dtype,
            data=data,
            strides=strides,
            elem_offset=elem_offset,
            byte_offset=byte_offset,
            scope=scope,
            align=align,
            offset_factor=offset_factor,
            layout=layout,
        )

    @deprecated("T.Buffer[...]", "T.Buffer(...)")
    def __getitem__(self, keys) -> Buffer:
        if not isinstance(keys, tuple):
            return self(keys)
        if len(keys) >= 2 and not isinstance(keys[1], str):
            return self(keys)
        return self(*keys)  # type: ignore[attr-defined] # pylint: disable=no-member


class PtrProxy:
    """Ptr proxy class for constructing tirx pointer."""

    def __or__(self, other):
        """Support union syntax in annotations."""
        return self

    def __ror__(self, other):
        """Support union syntax in annotations."""
        return self

    @deprecated("T.Ptr(...)", "T.handle(...)")
    def __call__(self, dtype, storage_scope="global"):
        if callable(dtype):
            dtype = dtype().ty.dtype
        return ptr(dtype, storage_scope)  # type: ignore[attr-defined] # pylint: disable=no-member

    @deprecated("T.Ptr[...]", "T.handle(...)")
    def __getitem__(self, keys):
        if not isinstance(keys, tuple):
            return self(keys)
        return self(*keys)


class _ConstexprProxy:
    """Sentinel marker for compile-time (specialization-time) parameters.

    Used as a parameter annotation in ``@T.jit`` decorated functions to mark
    a parameter as constexpr — its value is supplied to ``.specialize(**kwargs)``
    rather than at call time, and it is removed from the generated PrimFunc's
    runtime parameter list.
    """

    def __or__(self, other):
        return self

    def __ror__(self, other):
        return self


class _OptionalAnnotation:
    """Wrapper around the runtime annotation of an optional JIT parameter."""

    def __init__(self, annotation: Any) -> None:
        self.annotation = annotation


class _OptionalProxy:
    """Construct an explicit ``T.Optional(...)`` JIT parameter annotation."""

    def __call__(self, annotation: Any) -> _OptionalAnnotation:
        return _OptionalAnnotation(annotation)


def _is_explicit_optional_annotation(annotation: Any) -> bool:
    """Recognize deferred ``T.Optional(...)`` without accepting typing unions."""

    if not isinstance(annotation, str):
        return False
    try:
        node = ast.parse(annotation, mode="eval").body
    except SyntaxError:
        return False
    if not isinstance(node, ast.Call):
        return False
    func = node.func
    return (isinstance(func, ast.Attribute) and func.attr == "Optional") or (
        isinstance(func, ast.Name) and func.id == "Optional"
    )


Buffer = BufferProxy()  # pylint: disable=invalid-name
Ptr = PtrProxy()  # pylint: disable=invalid-name
constexpr = _ConstexprProxy()  # pylint: disable=invalid-name
Optional = _OptionalProxy()  # pylint: disable=invalid-name
