# 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=redefined-builtin, wrong-import-order, no-member, invalid-name
"""IRBuilder for Relax dialect"""

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

import tvm
from tvm import DataType, relax
from tvm.ir import IRModule, VDevice
from tvm.relax import (
    Call,
    Expr,
    ExternFunc,
    ShapeExpr,
    StringImm,
    TupleGetItem,
    Var,
    VarBinding,
    const,
)
from tvm.relax.dpl import PatternMatchingRewriter

############################### Operators ###############################
from tvm.relax.op import (
    abs,
    acos,
    acosh,
    add,
    arange,
    argmax,
    argmin,
    argsort,
    asin,
    asinh,
    assert_op,
    astype,
    atan,
    atan2,
    atanh,
    bitwise_and,
    bitwise_not,
    bitwise_or,
    bitwise_xor,
    broadcast_to,
    bucketize,
    builtin,
    call_builtin_with_ctx,
    call_dps_packed,
    call_inplace_packed,
    call_pure_packed,
    call_tir,
    call_tir_inplace,
    call_tir_with_grad,
    ccl,
    ceil,
    clip,
    collapse_sum_like,
    collapse_sum_to,
    concat,
    cos,
    cosh,
    cumprod,
    cumsum,
    dequantize,
    divide,
    dynamic_strided_slice,
    einsum,
    equal,
    erf,
    ewise_fma,
    exp,
    expand_dims,
    eye,
    eye_like,
    flatten,
    flip,
    floor,
    floor_divide,
    floor_mod,
    full,
    full_like,
    gather_elements,
    gather_nd,
    grad,
    greater,
    greater_equal,
    hamming_window,
    hint_on_device,
    image,
    index_put,
    index_tensor,
    invoke_closure,
    invoke_pure_closure,
    isfinite,
    isinf,
    isnan,
    layout_transform,
    left_shift,
    less,
    less_equal,
    linear,
    log,
    log_add_exp,
    logical_and,
    logical_not,
    logical_or,
    logical_xor,
    make_closure,
    matmul,
    max,
    maximum,
    mean,
    median,
    memory,
    meshgrid,
    min,
    minimum,
    mod,
    multinomial_from_uniform,
    multiply,
    negative,
    nn,
    nonzero,
    not_equal,
    null_value,
    one_hot,
    ones,
    ones_like,
    outer,
    permute_dims,
    power,
    print,
    prod,
    quantize,
    repeat,
    reshape,
    reverse_sequence,
    right_shift,
    round,
    rsqrt,
    scatter_elements,
    scatter_nd,
    shape_of,
    shape_to_tensor,
    sigmoid,
    sign,
    sin,
    sinh,
    size,
    slice_scatter,
    sort,
    split,
    sqrt,
    square,
    squeeze,
    stack,
    std,
    strided_slice,
    subtract,
    sum,
    take,
    tan,
    tanh,
    tensor_to_shape,
    tile,
    topk,
    tril,
    triu,
    trunc,
    unique,
    variance,
    vision,
    vm,
    where,
    wrap_param,
    zeros,
    zeros_like,
)
from tvm.relax.op import (
    call_py_func as _call_py_func,
)
from tvm.relax.op.builtin import stop_lift_params
from tvm.relax.type import Type
from tvm.relax.utils import convert_to_expr, gen_call_tir_inputs
from tvm.runtime import Object as tvm_Object
from tvm.runtime import ObjectConvertible
from tvm.runtime._tensor import (
    cpu,
    cuda,
    device,
    ext_dev,
    hexagon,
    metal,
    opencl,
    rocm,
    vpi,
    vulkan,
    webgpu,
)
from tvm.script.ir_builder.ir import decl_function, lookup_vdevice

from . import _ffi_api, frame

##################### Python Native Function Alias ######################

py_print = builtins.print
py_tuple = tuple  # pylint: disable=used-before-assignment
py_str = str  # pylint: disable=used-before-assignment


################################ Device ################################


def to_vdevice(data: Expr, dst_vdevice: py_str | VDevice) -> Expr:
    """Copy data to the destination device.

    Parameters
    ----------
    data : Expr
        The tensor to be copied.

    dst_device : Union[py_str, VDevice]
        The destination device where the data is copied to.

    Returns
    -------
    result : Expr
        The copied result.
    """
    if isinstance(dst_vdevice, py_str):
        if ":" in dst_vdevice:
            split_vdev = dst_vdevice.split(":")
            dst_vdevice = lookup_vdevice(split_vdev[0], int(split_vdev[1]))
        else:
            dst_vdevice = lookup_vdevice(dst_vdevice, 0)

    return tvm.relax.op.to_vdevice(data, dst_vdevice)


############################### Function ################################


def function(is_pure: bool = True, is_private: bool = False) -> frame.FunctionFrame:
    """Start a function frame.
    Parameters
    ----------
    is_pure: bool
        Whether the function is annotated as pure.

    is_private : bool
        Whether the function is annotated as private.

    Returns
    -------
    frame: FunctionFrame
        The constructed function frame.
    """
    return _ffi_api.Function(  # type: ignore[attr-defined]  # pylint: disable=no-member
        is_pure, is_private
    )


def arg(name: py_str, ty: Type) -> Var:
    """Add a parameter to the last function frame.
    Parameters
    ----------
    name: str
        The name of the parameter.
    ty: Type
        The type of the parameter

    Returns
    -------
    var: Var
        The created function parameter var.
    """

    return _ffi_api.Arg(name, ty)  # type: ignore[attr-defined] # pylint: disable=no-member


def func_name(name: py_str) -> None:
    """Specify the name of the last function frame.
    Parameters
    ----------
    name: str
        The function name.
    """
    return _ffi_api.FuncName(name)  # type: ignore[attr-defined] # pylint: disable=no-member


def func_attr(attrs: dict[py_str, tvm_Object]) -> None:
    """Specify the attrs of the last function frame.
    Parameters
    ----------
    attrs: Dict[str, Object]
        The function attrs.
    """
    return _ffi_api.FuncAttrs(attrs)  # type: ignore[attr-defined] # pylint: disable=no-member


def func_ret_type(ret_ty: Type) -> None:
    """Specify the return type of the last function frame.
    Parameters
    ----------
    ret_ty: Type
        The function return type.
    """
    return _ffi_api.FuncRetType(ret_ty)  # type: ignore[attr-defined] # pylint: disable=no-member


def func_ret_ty(ret_ty: Type) -> None:
    """Backward-compatible alias for `func_ret_type`."""
    return func_ret_type(ret_ty)


def func_ret_value(value: Expr) -> None:
    """Specify the return value of the last function frame.
    Parameters
    ----------
    value: Expr
        The function return value.
    """
    return _ffi_api.FuncRetValue(value)  # type: ignore[attr-defined] # pylint: disable=no-member


def rewriter(rewriter_mod: IRModule | type) -> PatternMatchingRewriter:
    """Define a pattern-rewrite rule

    The IRModule must have two publicly-exposed functions, `pattern`
    and `replacement`, where `pattern` and `replacement` have the same
    function signature.

    .. code-block:: python

        @R.rewriter
        class RewriteAddIntoMultiply:
            @R.function
            def pattern(A: R.Tensor):
                B = A + A
                return B

            @R.function
            def replacement(A: R.Tensor):
                B = A * 2
                return B

    Parameters
    ----------
    rewriter_mod: Union[IRModule, Type]

        Either an IRModule that defines a rewrite pattern, or a
        TVMScript class that can be parsed into an IRModule.

    Returns
    -------
    rewriter: PatternMatchingRewriter

        A rewriter object, which can be applied either to a Relax
        function or to an entire IRModule.

    """
    if not isinstance(rewriter_mod, IRModule):
        rewriter_mod = tvm.script.ir_module(rewriter_mod)

    return PatternMatchingRewriter.from_module(rewriter_mod)


############################# BindingBlock ##############################


def dataflow() -> frame.BindingBlockFrame:
    """Start a dataflow binding block frame.
    Returns
    -------
    frame: frame.BindingBlockFrame
        The created ir_builder Block frame.
    """
    return _ffi_api.Dataflow()  # type: ignore[attr-defined] # pylint: disable=no-member


def output(*vars: tuple[Var]) -> None:
    """Expose the dataflow block output variables as global ones.
    Parameters
    ----------
    vars: Tuple[Var]
        The output variables of a dataflow block.
    """
    return _ffi_api.DataflowBlockOutput(vars)  # type: ignore[attr-defined] # pylint: disable=no-member


################################## Ops #################################


def call_packed(
    func: py_str,
    *args: Expr,
    ty_args: Type | list[Type] | None = None,
    **kwargs: Any,
) -> Call:
    """Create a relax Call, which calls a packed function.
    Parameters
    ----------
    func: str
        The name of extern function.
    *args : Expr
        The arguments.
    ty_args: Optional[Union[Type, List[Type]]]
        The list of type information arguments.
    kwargs: Expr
        The keyword arguments.

    Returns
    -------
    call: Call
        The created Relax Call
    """
    op = ExternFunc(func)
    args = py_tuple(convert_to_expr(a) for a in args)
    if ty_args is None:
        ty_args = []
    if isinstance(ty_args, py_tuple):  # type: ignore
        ty_args = list(ty_args)
    elif not isinstance(ty_args, list):
        ty_args = [ty_args]

    ty_args = [
        (ty() if callable(ty) else ty.asobject() if isinstance(ty, ObjectConvertible) else ty)
        for ty in ty_args
    ]

    is_default = False
    if "attrs_type_key" in kwargs:
        attrs_type_key = kwargs["attrs_type_key"]
        kwargs.pop("attrs_type_key")
    else:
        attrs_type_key = "ir.DictAttrs"
        is_default = True
    attrs = None
    if kwargs or not is_default:
        attrs = tvm.ir.attrs.make_node(attrs_type_key, **kwargs)

    return Call(op, args, attrs=attrs, ty_args=ty_args)


def call_py_func(
    py_func_name: py_str,
    *args: Expr,
    out_ty: Type | list[Type],
) -> Call:
    """Create a relax Call, which calls a Python function.

    Parameters
    ----------
    py_func_name: str
        The name of the Python function to call. This should correspond to a function
        in the IRModule's pyfuncs attribute.
    *args : Expr
        The arguments.
    out_ty: Union[Type, List[Type]]
        The type information of the call_py_func output.
        It should be a single or a list of TensorType. Each one denotes the
        type information of a returned tensor.

    Returns
    -------
    call: Call
        The created Relax Call for call_py_func operator.
    """
    args = py_tuple(convert_to_expr(a) for a in args)
    if isinstance(out_ty, py_tuple):  # type: ignore
        out_ty = list(out_ty)
    elif not isinstance(out_ty, list):
        out_ty = [out_ty]

    out_ty = [
        (ty() if callable(ty) else ty.asobject() if isinstance(ty, ObjectConvertible) else ty)
        for ty in out_ty
    ]

    # Convert string to StringImm
    try:
        func_name_imm = (
            StringImm(py_func_name) if isinstance(py_func_name, py_str) else py_func_name
        )
    except (TypeError, ValueError, AttributeError):
        func_name_imm = StringImm(py_func_name)
    return _call_py_func(func_name_imm, args, out_ty)


def _ty_arg_wrapper(func):
    """A wrapper to convert TypeProxies to Type for builtin operators with ty_args"""

    def _convert_tensor_type(args):
        if isinstance(args, list | py_tuple):  # type: ignore
            new_args = [_convert_tensor_type(x) for x in args]
            return type(args)(new_args)
        if isinstance(args, dict):
            return {_convert_tensor_type(k): _convert_tensor_type(v) for k, v in args.items()}
        if inspect.isfunction(args):
            args = args()
        if isinstance(args, ObjectConvertible):
            args = args.asobject()
        return args

    @functools.wraps(func)
    def wrapped(*args, **kwargs):
        return func(*_convert_tensor_type(args), **_convert_tensor_type(kwargs))

    return wrapped  # type: ignore


invoke_closure = _ty_arg_wrapper(invoke_closure)  # pylint: disable=invalid-name

call_builtin_with_ctx = _ty_arg_wrapper(call_builtin_with_ctx)  # pylint: disable=invalid-name


############################### Emits ###############################


def emit(value: Expr, annotate_ty: Type | None = None) -> Var:
    """Emit a binding to the last binding block frame.
    Parameters
    ----------
    value: Expr
        The right side value of the bindings to be emitted.

    annotate_ty: Optional[Type]
        The optional type annotation for the emitted value.

    Returns
    -------
    var: Var
        The left side var of the emitted binding.
    """
    return _ffi_api.Emit(value, annotate_ty)  # type: ignore[attr-defined] # pylint: disable=no-member


def emit_te(func: Callable, *args: Any, **kwargs: Any) -> Call:
    """Emit a call node according to the te function.
    This function converts arguments from relax expression to te tensor,
    The callback func should return a te tensor or a list of te tensors.

    Parameters
    ----------
    func : Callable
        A function that returns a te tensor or a list of te tensors.

    args : Any, optional
        arguments passed to the function.

    kwargs : Any, optional
        The keyword arguments passed to the function.
        Note that the following keyword args are reserved:

            - 'primfunc_name_hint' for passing name hint to the PrimFunc
                that gets generated.
            - 'primfunc_attrs' is reserved for passing func attributes to
                be added to the PrimFunc that gets created.

    Returns
    -------
    call : Call
        A newly created call that calls into a tirx function.
    """
    primfunc_name_hint = kwargs.pop("primfunc_name_hint", None)
    tir_func, call_args, out_ty, tir_vars = gen_call_tir_inputs(func, *args, **kwargs)
    if not primfunc_name_hint:
        primfunc_name_hint = func.__name__
    gvar = decl_function(primfunc_name_hint, tir_func)  # type: ignore
    return call_tir(gvar, call_args, out_ty, tir_vars)


def emit_match_cast(value: Expr, ty: Type) -> Var:
    """Emit a match_cast binding to the last binding block frame.
    Parameters
    ----------
    value: Expr
        The value of the MatchCast to be emitted.
    ty: Type
        The ty of the MatchCast to be emitted.

    Returns
    -------
    var: Var
        The left side var of the emitted binding.
    """
    return _ffi_api.EmitMatchCast(value, ty)  # type: ignore


def emit_var_binding(value: VarBinding) -> Var:
    """Emit a binding to the last binding block frame.
    Parameters
    ----------
    value: VarBinding
        The binding to be emitted.
    Returns
    -------
    var: Var
        The left side var of the emitted binding.
    """
    return _ffi_api.EmitVarBinding(value)  # type: ignore


def emit_with_type(
    op: str,
    args: Expr,
    ty_args: Type | list[Type] | None = None,
) -> Call:
    """Create a Relax Call with type arguments.
    Parameters
    ----------
    op: Expr
        The relax op for which type args are to be appended
    args : Expr
        The arguments.
    ty_args: Optional[Union[Type, List[Type]]]
        The list of type arguments.

    Returns
    -------
    call: Call
        The created Relax Call
    """
    builtin_call = tvm.ir.Op.get(op)
    return Call(builtin_call, args, attrs=None, ty_args=ty_args)


def emit_with_ty(
    op: str,
    args: Expr,
    ty_args: Type | list[Type] | None = None,
) -> Call:
    """Backward-compatible alias for `emit_with_type`."""
    return emit_with_type(op, args, ty_args)


############################### SeqExpr ###############################


def SeqExpr() -> frame.SeqExprFrame:  # pylint: disable=invalid-name
    """Create a SeqExpr frame.
    Returns
    -------
    res : frame.SeqExprFrame
        The result SeqExprFrame
    """
    return _ffi_api.SeqExpr()  # type: ignore[attr-defined] # pylint: disable=no-member


############################# If Then Else #############################


def If(condition: Expr) -> frame.IfFrame:  # pylint: disable=invalid-name
    """Create an if frame.

    Parameters
    ----------
    condition : Expr

        The condition of if statement, executes the true branch if the
        condition is true, otherwise jump into the false branch.

    Returns
    -------
    res : frame.IfFrame
        The result IfFrame.

    """
    if not isinstance(condition, Expr):
        condition = relax.prim_value(condition)

    return _ffi_api.If(condition)  # type: ignore[attr-defined] # pylint: disable=no-member


def Then() -> frame.ThenFrame:  # pylint: disable=invalid-name
    """Create a then frame.
    Returns
    -------
    res : frame.ThenFrame
        The result ThenFrame.
    """
    return _ffi_api.Then()  # type: ignore[attr-defined] # pylint: disable=no-member


def Else() -> frame.ElseFrame:  # pylint: disable=invalid-name
    """Create an else frame.
    Returns
    -------
    res : frame.ElseFrame
        The result ElseFrame.
    """
    return _ffi_api.Else()  # type: ignore[attr-defined] # pylint: disable=no-member


############################### R.tuple ################################


def tuple(*fields: Expr) -> Expr:
    """Create a tuple expression.
    Parameters
    ----------
    *fields : Expr
        The fields of the tuple.
    Returns
    -------
    res : Expr
        The result tuple.
    """
    if len(fields) == 0:
        fields = py_tuple()

    return relax.Tuple(fields)  # type: ignore[attr-defined] # pylint: disable=no-member


############################### R.shape ################################


def shape(value: list[Expr]) -> Expr:
    """Create a ShapeExpr.
    Parameters
    ----------
    value : List[Expr]
        The fields of the tuple.
    Returns
    -------
    res : Expr
        The result tuple.
    """
    return relax.ShapeExpr(value)  # pylint: disable=no-member # type: ignore


############################### Expr ###############################


def prim_value(value: Expr | int | float) -> Expr:
    """Convert a value to a primitive expression.

    Parameters
    ----------
    value : Expr | int | float
        The value to convert.

    Returns
    -------
    res : Expr
        The primitive expression.
    """
    return relax.prim_value(value)  # type: ignore[attr-defined] # pylint: disable=no-member


def str(value: py_str) -> Expr:
    """Create a string imm expression.
    Parameters
    ----------
    value : str
        The value of the str.
    Returns
    -------
    res : Expr
        The result str.
    """
    return relax.StringImm(value)  # type: ignore[attr-defined] # pylint: disable=no-member


def dtype(value: py_str | DataType) -> Expr:
    """Create a dtype imm expression.
    Parameters
    ----------
    value : dtype
        The value of the dtype.
    Returns
    -------
    res : Expr
        The result dtype.
    """
    return relax.DataTypeImm(value)  # type: ignore[attr-defined] # pylint: disable=no-member


############################### Importer ###############################

__all__ = [
    "Else",
    "ExternFunc",
    "If",
    "SeqExpr",
    "ShapeExpr",
    "Then",
    "TupleGetItem",
    "abs",
    "acos",
    "acosh",
    "add",
    "arange",
    "arg",
    "argmax",
    "argmin",
    "argsort",
    "asin",
    "asinh",
    "assert_op",
    "astype",
    "atan",
    "atan2",
    "atanh",
    "bitwise_and",
    "bitwise_not",
    "bitwise_or",
    "bitwise_xor",
    "broadcast_to",
    "bucketize",
    "builtin",
    "call_builtin_with_ctx",
    "call_dps_packed",
    "call_inplace_packed",
    "call_packed",
    "call_pure_packed",
    "call_py_func",
    "call_tir",
    "call_tir_inplace",
    "call_tir_with_grad",
    "ccl",
    "ceil",
    "clip",
    "collapse_sum_like",
    "collapse_sum_to",
    "concat",
    "const",
    "cos",
    "cosh",
    "cpu",
    "cuda",
    "cumprod",
    "cumsum",
    "dataflow",
    "dequantize",
    "device",
    "divide",
    "dtype",
    "dynamic_strided_slice",
    "einsum",
    "emit",
    "emit_match_cast",
    "emit_te",
    "emit_var_binding",
    "emit_with_ty",
    "emit_with_type",
    "equal",
    "erf",
    "ewise_fma",
    "exp",
    "expand_dims",
    "ext_dev",
    "eye",
    "eye_like",
    "flatten",
    "flip",
    "floor",
    "floor_divide",
    "floor_mod",
    "full",
    "full_like",
    "func_attr",
    "func_name",
    "func_ret_ty",
    "func_ret_type",
    "func_ret_value",
    "function",
    "gather_elements",
    "gather_nd",
    "grad",
    "greater",
    "greater_equal",
    "hamming_window",
    "hexagon",
    "hint_on_device",
    "image",
    "index_put",
    "index_tensor",
    "invoke_closure",
    "invoke_pure_closure",
    "isfinite",
    "isinf",
    "isnan",
    "layout_transform",
    "left_shift",
    "less",
    "less_equal",
    "linear",
    "log",
    "log_add_exp",
    "logical_and",
    "logical_not",
    "logical_or",
    "logical_xor",
    "make_closure",
    "matmul",
    "max",
    "maximum",
    "mean",
    "median",
    "memory",
    "meshgrid",
    "metal",
    "min",
    "minimum",
    "mod",
    "multinomial_from_uniform",
    "multiply",
    "negative",
    "nn",
    "nonzero",
    "not_equal",
    "null_value",
    "one_hot",
    "ones",
    "ones_like",
    "opencl",
    "outer",
    "output",
    "permute_dims",
    "power",
    "prim_value",
    "print",
    "prod",
    "quantize",
    "repeat",
    "reshape",
    "reverse_sequence",
    "rewriter",
    "right_shift",
    "rocm",
    "round",
    "rsqrt",
    "scatter_elements",
    "scatter_nd",
    "shape",
    "shape_of",
    "shape_to_tensor",
    "sigmoid",
    "sign",
    "sin",
    "sinh",
    "size",
    "slice_scatter",
    "sort",
    "split",
    "sqrt",
    "square",
    "squeeze",
    "stack",
    "std",
    "stop_lift_params",
    "str",
    "str",
    "strided_slice",
    "subtract",
    "sum",
    "take",
    "tan",
    "tanh",
    "tensor_to_shape",
    "tile",
    "to_vdevice",
    "topk",
    "tril",
    "triu",
    "trunc",
    "tuple",
    "unique",
    "variance",
    "vision",
    "vm",
    "vpi",
    "vulkan",
    "webgpu",
    "where",
    "wrap_param",
    "zeros",
    "zeros_like",
]
