# 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=no-else-return, invalid-name
# pylint: disable=unidiomatic-typecheck
"""
This file contains the set of passes for Relax, which exposes an interface for
configuring the passes and scripting them in Python.
"""

from collections.abc import Callable
from enum import IntEnum

import tvm
from tvm import IRModule, tirx
from tvm.ir import Call, Type
from tvm.relax.expr import Binding, DataflowBlock, Expr, Function, GlobalVar, Var
from tvm.relax.type import FuncType
from tvm.tirx import Buffer, IndexMap, PrimFunc, SBlock

from . import _ffi_api


def get_static_type(ty: Type) -> Type:
    """Get the corresponding static type from a Type.

    Parameters
    ----------
    ty : Type
        The input type.

    Returns
    -------
    ret : Type
        The corresponding static type.
    """
    return _ffi_api.GetStaticType(ty)  # type: ignore


def erase_to_well_defined(
    ty: Type,
    shape_var_map: dict[tirx.Var, tirx.Expr] | None = None,
    var_map: dict[Var, Expr] | None = None,
) -> Type:
    """Erase ty into a well defined form.

    This function removes the Type's dependencies on shape and vars that
    are not defined in given maps.

    Parameters
    ----------
    ty : Type
        The input type.

    shape_var_map : Dict[tirx.Var, tirx.Expr]
        Specifies the defined shape vars and the values they should map to.

    var_map : Dict[Var, Expr]
        Specifies the defined vars and the values they should map to.

    Returns
    -------
    ret : Type
        The corresponding erased type.
    """
    shape_var_map = {} if shape_var_map is None else shape_var_map
    var_map = {} if var_map is None else var_map

    return _ffi_api.EraseToWellDefined(ty, shape_var_map, var_map)  # type: ignore


class BaseCheckResult(IntEnum):
    """Return result of fine-grained base check.

    Note
    ----
    Base check comes with fine-grained fail levels.

    - FAIL_L0: The lhs and rhs have no intersection at all.
    - FAIL_L1: We get the failure by looking at static information.
    - FAIL_L2: We get the failure due to unknown symbolic variable relations.
    """

    FAIL_L0 = 0
    FAIL_L1 = 1
    FAIL_L2 = 2
    PASS = 3


def type_base_check(base: Type, derived: Type) -> BaseCheckResult:
    """Run a base check to see if base subsumes derived.

    Parameters
    ----------
    base: Type
        The base type.

    derived: Type
        The derived type.

    Returns
    -------
    ret : Type
        The derived return value type.
    """
    return _ffi_api.TypeBaseCheck(base, derived)  # type: ignore


def derive_call_ret_type(func_ty: FuncType, call: Call, ctx: "tvm.relax.BlockBuilder") -> Type:
    """Derive the call's ret value type from inputs.

    Parameters
    ----------
    func_ty: FuncType
        The call's function signature.

    call: Call
        The call expression

    ctx: tvm.relax.BlockBuilder
        The context block builder.

    Returns
    -------
    ret : Type
        The derived return value type.

    Note
    ----
    This is an internal derivation function, call.op field is
    ignored in this case and the derivation only depends on func_ty.
    """
    return _ffi_api.DeriveCallRetType(func_ty, call, ctx)  # type: ignore


def type_lca(lhs: Type, rhs: Type) -> Type:
    """Unify the two type to their least common ancestor.

    Parameters
    ----------
    lhs: Type
        The left operand.

    rhs: Type
        The right operand.

    Returns
    -------
    ret : Type
        The corresponding lca result.
    """
    return _ffi_api.TypeLCA(lhs, rhs)  # type: ignore


def tir_vars_in_type(ty: Type) -> list[tirx.Var]:
    """Get the TIR variables that appear in the input type.
    The returned list is deduplicated - each TIR variable will appear at most once.

    Parameters
    ----------
    ty : Type
        The type object to be analyzed.

    Returns
    -------
    ret : List[tirx.Var]
        The list of TIR variables that appear in the input type.
    """
    return _ffi_api.TIRVarsInType(ty)  # type: ignore


def definable_tir_vars_in_type(ty: Type) -> list[tirx.Var]:
    """Get the TIR variables that may be defined from input type.
    The returned list is deduplicated - each TIR variable will appear at most once.

    Parameters
    ----------
    ty : Type
        The type object to be analyzed.

    Returns
    -------
    ret : List[tirx.Var]

        The list of TIR variables that can be defined from the Type
    """
    return _ffi_api.DefinableTIRVarsInType(ty)  # type: ignore


def collect_non_negative_expressions(ty: Type) -> list[tirx.Expr]:
    """Collect TIR expressions used in non-negative contexts

    Get TIR variables that are non-negative within the context where
    the type is used.  For example, any expression used as a
    tensor shape.

    The returned list is deduplicated - each TIR expression will
    appear at most once.  The order of the list is in the order of
    occurrence within the type.

    Parameters
    ----------
    ty : Type
        The type object to be analyzed.

    Returns
    -------
    ret : List[tirx.Var]

        The list of TIR variables that can be defined from the Type

    """

    return _ffi_api.CollectNonNegativeExpressions(ty)  # type: ignore


def defined_symbolic_vars(func: Function) -> list[Var]:
    """Get the TIR variables that defined in the input function.
    The returned list is deduplicated - each TIR variable will appear at most once.

    Parameters
    ----------
    func : Function
        The function object to be analyzed.

    Returns
    -------
    ret : List[Var]
        The list of symbolic variables that are defined in the input function.
    """
    return _ffi_api.DefinedSymbolicVars(func)  # type: ignore


def free_symbolic_vars(func: Function) -> list[Var]:
    """Get the TIR variables that are used but not defined in the input function.
    The returned list is deduplicated - each TIR variable will appear at most once.

    Parameters
    ----------
    func : Function
        The function object to be analyzed.

    Returns
    -------
    ret : List[Var]
        The list of symbolic variables that are used but not defined in the input function.
    """
    return _ffi_api.FreeSymbolicVars(func)  # type: ignore


def bound_vars(expr: Expr) -> list[Var]:
    """
    Return all bound variables from expression expr.
    Bound variables are all variables that are declared in the expr.
    They only have meaning inside that expr, and can only be used in it.
    Parameters
    ----------
    expr: Expr
        The expression.
    Returns
    -------
    ret: List[Var]
        List of bound vars in expr, in post-DFS order
    """
    return _ffi_api.bound_vars(expr)


def free_vars(expr: Expr) -> list[Var]:
    """
    Return all free variables from expression expr.
    Free variables are variables that are not bound by a
    VarBinding or a function parameter in the expression.
    Parameters
    ----------
    expr: Expr
        The expression.
    Returns
    -------
    ret: List[Var]
        List of free vars in expr, in post-DFS order
    """
    return _ffi_api.free_vars(expr)


def all_vars(expr: Expr) -> list[Var]:
    """
    Return all (local) variables from expression expr.
    Parameters
    ----------
    expr: Expr
        The expression.
    Returns
    -------
    ret: List[Var]
        List of vars in expr, in post-DFS order
    """
    return _ffi_api.all_vars(expr)


def used_vars(expr: Expr) -> list[Var]:
    """
    Return all variables used in an expression.

    This function collects all variable references within the given expression,
    which is useful for analyzing variable dependencies.

    Parameters
    ----------
    expr: Expr
        The expression to analyze.

    Returns
    -------
    ret: List[Var]
        List of variables used in the expression.
    """
    return _ffi_api.used_vars(expr)  # type: ignore


def all_global_vars(expr: Expr) -> list[GlobalVar]:
    """
    Return all global variables from expression expr.
    Parameters
    ----------
    expr: Expr
        The expression.
    Returns
    -------
    ret: List[GlobalVar]
        List of global vars in expr, in post-DFS order
    """
    return _ffi_api.all_global_vars(expr)


def post_order_visit(expr, fvisit):
    """Recursively visit the ir in post DFS order node,
    apply fvisit. Each node is guaranteed to be visited
    only once.

    Parameters
    ----------
    expr : tvm.relax.Expr
        The input expression.

    fvisit : function
        The visitor function to be applied.
    """
    return _ffi_api.post_order_visit(expr, fvisit)  # type: ignore


def has_reshape_pattern(func: tirx.PrimFunc) -> bool:
    """Check if the given PrimFunc is essentially doing a reshape operation.
    The reshape operation also includes expand_dims, squeeze, flatten, etc.

    Here the allowed reshape pattern is: for example, assume the operation is
    `B[l_0, l_1, ..., l_b] = A[r_0, r_1, ..., r_a]`, we check if we can prove
    that the flattened index of l_0, ..., l_b under buffer B equals to the
    flattened index of r_0, ..., r_a under buffer A.

    Parameters
    ----------
    func : tirx.PrimFunc
        The function to be examined.

    Returns
    -------
    ret : bool
        A boolean indicating if the given PrimFunc is doing a reshape.

    Notes
    -----
    According to the description above, the returned result can only be
    false-negative and cannot be false-positive, since whenever we cannot
    prove the equality, we return false. This property guarantees the safety
    of this function.
    """
    return _ffi_api.has_reshape_pattern(func)  # type: ignore


def contains_impure_call(expr: Expr, own_name: Var | GlobalVar | None = None) -> bool:
    """
    Check if the given expression (likely a function body) contains any impure calls.

    Parameters
    ----------
    expr : Expr
        The expression to be examined. If expr is a function, we check the body.

    own_name : Var or GlobalVar (optional)
        For a recursive function, the analysis can ignore the self-calls
        for checking purity.

    Returns
    -------
    ret : bool
        True if there is an impure call
        (call to a function that may have visible side effects).

    Notes
    -----
    Relies on Type annotations, so ensure that the module has been normalized first.
    Also, an impure call in a *nested* function does *not* mean that the outer expression contains
    an impure call--it only does if the nested function is *later called*.
    """
    return _ffi_api.contains_impure_call(expr, own_name)


def get_var2val(func: Function) -> dict[Var, Expr]:
    """
    Get a mapping from Var to Expr for each variable in the function.

    Parameters
    ----------
    func : Function
        The input function to be analyzed.

    Returns
    -------
    Dict[Var, Expr]
        A mapping from Var to Expr.
    """
    return _ffi_api.get_var2val(func)  # type: ignore


def udchain(dfb: DataflowBlock) -> dict[Var, list[Var]]:
    """
    Analyze the variable use-def chain in a dataflow block.

    Parameters
    ----------
    dfb : DataflowBlock
        The dataflow block to analyze

    Returns
    -------
    Dict[Var, List[Var]]
        A mapping from variable definition to its uses.
    """
    return _ffi_api.udchain(dfb)  # type: ignore


def name_to_binding(func: Function) -> dict[str, list[Binding]]:
    """Return a map from variable name to its bindings."""
    return _ffi_api.name_to_binding(func)  # type: ignore


def remove_all_unused(func: Function) -> Function:
    """It removes:
    1. Unused local VarBindings in a DataflowBlock.
    2. Unused DataflowBlocks in a function.

    Parameters
    ----------
    func : Function
        The input function to be analyzed.

    Notes
    -----
    For IRModule-wise DCE, use py:func:`tvm.relax.transform.DeadCodeElimination`.

    Returns
    -------
    Function
        The function with unused variables removed.
    """
    return _ffi_api.remove_all_unused(func)  # type: ignore


def well_formed(obj: IRModule | Function, check_ty: bool = True) -> None:
    """Check if the IRModule is well formed, raising on the first violation.

    Raises an error (seeded with the offending node so a pass runner can report a
    precise access path) on the first well-formedness violation.  Use
    :func:`check_well_formed` for a boolean answer.

    Parameters
    ----------
    obj : Union[tvm.IRModule, Function]
        The input IRModule or relax.Function.

    check_ty : bool
        A boolean flag indicating if the property "every Expr must
        have defined type information" will be checked.

    Note
    ----
    By default the type information is always checked. It is only in test cases
    where `check_ty` might be false, so that other well-formed requirements
    will be well tested and will not be blocked by not having type information.
    """
    _ffi_api.well_formed(obj, check_ty)  # type: ignore


def check_well_formed(obj: IRModule | Function, check_ty: bool = True) -> bool:
    """Return whether the IRModule or Function is well formed.

    Wraps :func:`well_formed`, returning False instead of raising on the first violation.

    Parameters
    ----------
    obj : Union[tvm.IRModule, Function]
        The input IRModule or relax.Function.

    check_ty : bool
        A boolean flag indicating if the property "every Expr must
        have defined type information" will be checked.

    Returns
    -------
    ret: bool
        True if the IRModule is well formed, False if not.
    """
    return _ffi_api.check_well_formed(obj, check_ty)  # type: ignore


def _get_prim_func_default_dtype(func: PrimFunc):
    """Detect default index dtype from function buffer map"""
    for _, v in func.buffer_map.items():
        for value in v.shape:
            return value.ty
    return "int64"


def suggest_layout_transforms(
    func: PrimFunc, write_buffer_transforms: list[IndexMap | Callable]
) -> dict[SBlock, dict[SBlock | Buffer, IndexMap]]:
    """Suggest Layout transformations of blocks and buffers in a PrimFunc.

    Parameters
    ----------
    func: PrimFunc
        PrimFunc on which analysis will be performed and transformations suggested.

    write_buffer_transforms: List[Union[IndexMap, Callable]
        List of layout transformations on the output buffers. The number of layout
        transformations must match the number of outputs of the PrimFunc.

    Returns
    -------
    ret: Dict[SBlock, Dict[Union[SBlock, Buffer], IndexMap]]
         Suggested transforms per block in `func`. For each block the returned value is a map
         from the object (block or buffer) to it's index map transformation.
    """
    write_buffer_index_maps = []
    default_index_dtype = _get_prim_func_default_dtype(func)
    for transform in write_buffer_transforms:
        if callable(transform):
            transform = IndexMap.from_func(transform, index_dtype=default_index_dtype)
        assert isinstance(transform, IndexMap)
        write_buffer_index_maps.append(transform)
    return _ffi_api.suggest_layout_transforms(func, write_buffer_index_maps)  # type: ignore


def detect_recursion(mod: tvm.IRModule) -> list[list[GlobalVar]]:
    """
    Find all sets of recursive or mutually recursive functions in the module.

    Two or more functions are mutually recursive if there is some cycle of references
    among them. For example, if there are two functions A and B, they are
    mutually recursive if A calls B and B calls A. Another case would be with
    three functions A, B, and C, where A calls B, B calls C, and C calls A.

    (Note that functions do not have to call each other to reference each other.
    For example, if a function returns another function, that is still a reference
    that could potentially be recursive, even without a call.)


    If a function is simply recursive and not mutually recursive with any other,
    it will be reported as a group by itself.

    Parameters
    ----------
    mod: The module

    Returns
    -------
    ret: List[List[GlobalVar]]
        Each member of the list is a list of global functions
        that references each other mutually recursively.
        If a function is simply recursive and not mutually recursive
        with any other, it will be a singleton in this list.
    """
    return _ffi_api.detect_recursion(mod)  # type: ignore


def computable_at_compile_time(func: Function) -> list[Var]:
    """Collect variables whose value can be computed at compile-time

    If a function has the `kNumInput` attribute, then the first
    `kNumInput` parameters are provided at run-time, while all
    remaining parameters may be known at compile-time.  This utility
    collects all variable bindings that only depend, directly or
    indirectly, on the parameters known at compile-time.

    Parameters
    ----------
    func: Function

        The `relax.Function` to analyze

    Returns
    -------
    ret: List[Var]

        The set of variables that can be computed at compile-time, in
        order of their occurrence within the function.
    """
    return _ffi_api.computable_at_compile_time(func)  # type: ignore
