# 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=missing-docstring, unused-argument

import functools
import numbers
from typing import Any

import numpy as np
import tvm_ffi

import tvm
from tvm import relax
from tvm.ir import GlobalVar
from tvm.relax import Expr, Type
from tvm.relax.script import builder as R
from tvm.relax.script.builder.frame import BindingBlockFrame
from tvm.relax.utils import convert_to_expr
from tvm.script.ir_builder import ir as I
from tvm.script.ir_builder.base import IRBuilder
from tvm.script.parser._core import Parser, dispatch, doc
from tvm.tirx.script import builder as T

from .entry import (
    MatchCastPair,
    PrimProxy,
    TypeProxy,
    _normalize_ty,
    _normalize_ty_proxy,
)

relax.Expr._dispatch_type = relax.Expr  # pylint: disable=protected-access
dispatch.register_op(relax.Expr, doc.GtE, 0)(lambda lhs, rhs: lhs >= rhs)
dispatch.register_op(relax.Expr, doc.Gt, 0)(lambda lhs, rhs: lhs > rhs)
dispatch.register_op(relax.Expr, doc.LtE, 0)(lambda lhs, rhs: lhs <= rhs)
dispatch.register_op(relax.Expr, doc.Lt, 0)(lambda lhs, rhs: lhs < rhs)


def bind_assign_value(
    self: Parser,
    node: doc.expr,
    var_name: str,
    value: Any,
    anno_ty: Type | None = None,
    symbolic_declarations: set[str] | None = None,
) -> Any:
    var_table = self.var_table.get()

    # Primitive assignments emit Relax bindings by default, reversing the old default.
    # Explicit I.meta_var values and literal T.dtype() declarations are the two
    # parser-time exceptions.
    if isinstance(value, I.meta_var):
        return value.value

    if tvm.ir.is_prim_var(value) and var_name in (symbolic_declarations or set()):
        if anno_ty is not None and not tvm_ffi.structural_equal(anno_ty, value.ty):
            self.report_error(
                node,
                f"Expected the declared symbolic variable to have type {anno_ty}, "
                f"but got {value.ty}",
            )
        if value.name and var_name != value.name:
            self.report_error(
                node,
                "Cannot define PrimType values with different names. The LHS of binding "
                "must have the same name provided in the RHS.",
            )
        if var_name in var_table:
            prev_value = var_table[var_name]
            if not tvm.ir.is_prim_var(prev_value):
                self.report_error(
                    node,
                    "Cannot redefine a non-PrimType object to a PrimType value. Please "
                    "define the PrimType value with another name.",
                )
            if prev_value.ty != value.ty:
                self.report_error(
                    node,
                    f"Expected the same dtype for PrimType values but got "
                    f"{value.ty} vs {prev_value.ty}",
                )
            value = prev_value
        IRBuilder.name(var_name, value)
        return value

    if isinstance(value, tuple):
        value = convert_to_expr(value)
    if isinstance(value, numbers.Number | np.bool_):
        if isinstance(anno_ty, tvm.ir.PrimType):
            value = relax.prim_value(value, dtype=anno_ty.dtype)
        else:
            value = R.const(value)

    if isinstance(value, relax.Expr):
        var = R.emit(value, anno_ty)
    elif isinstance(value, MatchCastPair):
        if anno_ty is not None and not tvm_ffi.structural_equal(anno_ty, value.ty):
            self.report_error(
                node, "Cannot specify inconsistent annotation for a match cast pair. "
            )
        var = R.emit_match_cast(value.value, value.ty)
    else:
        return value

    IRBuilder.name(var_name, var)
    return var


def is_symbolic_var_declaration(node: doc.expr) -> bool:
    """Return whether an expression is literal ``T.dtype()`` declaration syntax.

    Detection is syntactic so declaration semantics do not depend on evaluating its RHS.
    """
    if not (
        isinstance(node, doc.Call)
        and not node.args
        and not node.keywords
        and isinstance(node.func, doc.Attribute)
        and isinstance(node.func.value, doc.Name)
        and node.func.value.id == "T"
    ):
        return False
    constructor = getattr(T, node.func.attr, None)
    return isinstance(constructor, T.DtypeConstructor) or constructor is T.bool


def collect_symbolic_var_declaration_nodes(
    target: doc.expr, value: doc.expr
) -> dict[str, doc.expr]:
    """Pair targets with literal ``T.dtype()`` declarations.

    Declaration-vs-binding is per target, allowing tuple unpacking to mix both.
    """
    if isinstance(target, doc.Name):
        return {target.id: value} if is_symbolic_var_declaration(value) else {}
    if isinstance(target, doc.Tuple | doc.List) and isinstance(value, doc.Tuple | doc.List):
        if len(target.elts) != len(value.elts):
            return {}
        declarations = {}
        for lhs, rhs in zip(target.elts, value.elts):
            declarations.update(collect_symbolic_var_declaration_nodes(lhs, rhs))
        return declarations
    return {}


def collect_symbolic_var_declarations(target: doc.expr, value: doc.expr) -> set[str]:
    """Collect assignment targets whose matching RHS is ``T.dtype()``."""
    return set(collect_symbolic_var_declaration_nodes(target, value))


def eval_ty_proxy(self: Parser, node: doc.expr) -> TypeProxy:
    try:
        annotation = self.eval_expr(node)
        return _normalize_ty_proxy(annotation)
    except Exception as err:  # pylint: disable=broad-except
        self.report_error(node, err)
        raise


def eval_ty(self: Parser, node: doc.expr, eval_str: bool = False) -> Type:
    var_table = self.var_table.get() if eval_str else None
    try:
        ty = self.eval_expr(node)
        return _normalize_ty(ty, var_table)
    except Exception as err:  # pylint: disable=broad-except
        self.report_error(node, err)
        raise


def is_called(node: Any, func_name: str) -> bool:
    # Check if it calls into a func
    if isinstance(node, doc.Call):
        # Recursive call was found
        if isinstance(node.func, doc.Name) and node.func.id == func_name:
            return True
    elif isinstance(node, list | tuple):
        for stmt in node:
            if is_called(stmt, func_name):
                return True
    elif isinstance(node, doc.AnnAssign | doc.Assign | doc.Return | doc.Expr):
        return is_called(node.value, func_name)
    elif isinstance(node, doc.With):
        return is_called(node.body, func_name)
    elif isinstance(node, doc.If):
        smts = []
        if node.body is not None:
            smts = smts + list(node.body)
        if node.orelse is not None:
            smts = smts + list(node.orelse)
        return is_called(smts, func_name)
    return False


def is_recursive(node: doc.FunctionDef) -> bool:
    # Check if it is a recursive function
    for stmt in node.body:
        if is_called(stmt, node.name):
            return True
    return False


def collect_symbolic_var_from_prelude(
    self: Parser, node: doc.FunctionDef, symbolic_vars: dict[str, tvm.ir.Var]
) -> dict[str, tvm.ir.Var]:
    prelude_vars = {}
    for stmt in node.body:
        if isinstance(stmt, doc.Assign) and len(stmt.targets) == 1:
            declarations = collect_symbolic_var_declaration_nodes(stmt.targets[0], stmt.value)
            for name, value_node in declarations.items():
                if name not in symbolic_vars:
                    continue
                declared_var = self.eval_expr(value_node)
                if tvm.ir.is_prim_var(declared_var) and declared_var.ty == symbolic_vars[name].ty:
                    prelude_vars[name] = declared_var

    return {**symbolic_vars, **prelude_vars}


def collect_symbolic_var_from_params(self: Parser, node: doc.FunctionDef) -> None:
    symbolic_vars = {}
    prim_params = set()
    with self.var_table.with_frame():
        for arg in node.args.args:
            if arg.annotation is None:
                self.report_error(arg, "Type annotation is required for function parameters.")
            param_ty_proxy = eval_ty_proxy(self, arg.annotation)

            for var_name in param_ty_proxy.get_symbolic_vars():
                if var_name not in prim_params and var_name not in symbolic_vars:
                    symbolic_vars[var_name] = tvm.ir.Var(var_name, "int64")

            if isinstance(param_ty_proxy, PrimProxy):
                temp_param = tvm.ir.Var(arg.arg, param_ty_proxy.as_ty())
                self.var_table.add(arg.arg, temp_param)
                prim_params.add(arg.arg)

    # Prelude declarations select each scope symbol's canonical Var object.
    # Per-assignment declaration classification separately controls emission.
    symbolic_vars = collect_symbolic_var_from_prelude(self, node, symbolic_vars)

    # Define symbolic vars to the current var_table frame
    for var_name, var in symbolic_vars.items():
        self.var_table.add(var_name, var, allow_shadowing=False)


def parse_function_params(self: Parser, node: doc.FunctionDef, param_factory) -> list[tvm.ir.Var]:
    collect_symbolic_var_from_params(self, node)
    params = []
    for arg in node.args.args:
        if arg.annotation is None:
            self.report_error(arg, "Type annotation is required for function parameters.")
        param_ty = eval_ty(self, arg.annotation, eval_str=True)
        param = param_factory(arg.arg, param_ty)
        self.var_table.add(arg.arg, param)
        params.append(param)

    return params


@dispatch.register(token="relax", type_name="FunctionDef")
def visit_function_def(self: Parser, node: doc.FunctionDef) -> None:
    is_inner_function = self.inside_function
    self.inside_function = True

    # reserve a var for local function
    func_val = self.var_table.get().get(node.name)
    if func_val is None and is_recursive(node):
        with self.var_table.with_frame():
            provisional_params = parse_function_params(self, node, tvm.ir.Var)
            if node.returns is None:
                ret_ty = relax.TupleType([])
            else:
                ret_ty = eval_ty(self, node.returns, eval_str=True)
        params_ty = [param.ty for param in provisional_params]
        # created a var for the local function, the same var could be used for recursive call
        local_func_var = tvm.ir.Var(node.name, relax.FuncType(params_ty, ret_ty))
        self.var_table.add(node.name, local_func_var)

    purity = find_decorator_annotation(node, "pure")
    # treat the function as private if we are inside another function
    # or if it has a privacy annotation
    privacy = is_inner_function or find_decorator_annotation(node, "private", default=False)

    with self.var_table.with_frame():
        with self.with_dispatch_token("relax"):
            with R.function(is_pure=purity, is_private=privacy):
                R.func_name(node.name)
                parse_function_params(self, node, R.arg)

                if node.returns is not None:
                    ann_ty = eval_ty(self, node.returns, eval_str=True)
                    R.func_ret_ty(ann_ty)

                for stmt in node.body:
                    if isinstance(stmt, doc.FunctionDef):
                        if not stmt.decorator_list:
                            self.report_error(stmt, "Function must be decorated")
                        dec = self.eval_expr(stmt.decorator_list[-1])
                        # inline prim_func was found
                        if dec.dispatch_token == "tirx":
                            self.report_error(stmt, "inline prim_func is disallowed in Relax IR")

                self.visit_body(node.body)
    self.inside_function = is_inner_function


def find_decorator_annotation(node: doc.FunctionDef, annotation: str, default: bool = True) -> bool:
    """
    Check the value of given annotation (argument name) in the function decorator.
    Returns the value of the annotation if present, otherwise giving the default value.
    """
    # look for the named argument in the function decorator
    for dec in node.decorator_list:
        if not isinstance(dec, doc.Call) or dec.func.attr != "function":
            continue
        for keyword in dec.keywords:
            if keyword.arg == annotation:
                return keyword.value.value
    return default


@dispatch.register(token="relax", type_name="tvm_declare_function")
def visit_tvm_declare_function(self: Parser, node: doc.FunctionDef) -> GlobalVar:
    with self.var_table.with_frame():
        params = parse_function_params(self, node, tvm.ir.Var)

        if node.returns is None:
            # Use AnyType as unknown return type
            # NOTE: Cannot use VoidType here because the return type can be refined later.
            ret_ty = relax.AnyType()
        else:
            ret_ty = eval_ty(self, node.returns, eval_str=True)
    is_pure = find_decorator_annotation(node, "pure")

    func_signature = relax.Function.create_empty(params, ret_ty, is_pure=is_pure)
    return I.decl_function(node.name, func_signature)


@dispatch.register(token="relax", type_name="pre_visit_local_function")
def pre_visit_local_function(self: Parser, node: doc.Expr) -> None:
    ir_builder = IRBuilder()
    ir_builder.__enter__()


@dispatch.register(token="relax", type_name="post_visit_local_function")
def post_visit_local_function(self: Parser, node: doc.Expr) -> None:
    ir_builder = IRBuilder.current()
    result = ir_builder.get()
    ir_builder.__exit__(None, None, None)
    # reuse var if it is reserved
    reserved_var = self.var_table.get().get(node.name)
    if reserved_var is not None:
        var = R.emit_var_binding(relax.VarBinding(reserved_var, result))
    else:
        var = R.emit(result)
    IRBuilder.name(node.name, var)
    self.var_table.add(node.name, var, allow_shadowing=False)


@dispatch.register(token="relax", type_name="Expr")
def visit_expr_stmt(self: Parser, node: doc.Expr) -> None:
    value = self.eval_expr(node.value)
    if isinstance(value, relax.Expr):
        var = R.emit(value)
        IRBuilder.name("_", var)
        is_void_value = isinstance(var.ty, relax.TupleType) and len(var.ty.fields) == 0

        if not is_void_value:
            self.report_error(
                node,
                f"Non-void relax expressions must be bound to a variable, "
                f"but expression of type {var.ty} was used as a statement.",
            )

    elif value is not None:
        self.report_error(node, f"Unsupported Expr stmt type {value}.")


@dispatch.register(token="relax", type_name="arguments")
def visit_arguments(self: Parser, node: doc.arguments) -> None:
    arg: doc.arg
    for arg in node.args:
        if arg.annotation is None:
            self.report_error(arg, "Type annotation is required for function parameters.")
        param_ty = eval_ty(self, arg.annotation, eval_str=True)
        param = R.arg(arg.arg, param_ty)

        self.var_table.add(arg.arg, param)


@dispatch.register(token="relax", type_name="tvm_annotation")
def visit_tvm_annotation(self: Parser, node: doc.expr) -> Type:
    return eval_ty(self, node, eval_str=False)


@dispatch.register(token="relax", type_name="With")
def visit_with(self: Parser, node: doc.With) -> None:
    # Currently only `with R.dataflow()` is supported
    if len(node.items) != 1:
        self.report_error(node, "Only one item is allowed.")
    item = node.items[0]
    if item.optional_vars is not None:
        self.report_error(
            item.context_expr,
            "Relax syntax doesn't allow binding expressions in `with` to variables",
        )
    frame = self.eval_expr(item.context_expr)
    with self.var_table.with_frame():
        with frame:
            self.visit(node.body)
    if isinstance(frame, BindingBlockFrame) and frame.is_dataflow:
        output_vars = frame.output_vars
        for var in output_vars:
            self.var_table.add(var.name, var, allow_shadowing=True)


@dispatch.register(token="relax", 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=functools.partial(
            bind_assign_value,
            symbolic_declarations=collect_symbolic_var_declarations(lhs, node.value),
        ),
        allow_shadowing=True,
    )


@dispatch.register(token="relax", type_name="AnnAssign")
def visit_ann_assign(self: Parser, node: doc.AnnAssign) -> None:
    lhs = node.target
    rhs = self.eval_expr(node.value)
    anno_ty = self.visit_tvm_annotation(node.annotation)
    self.eval_assign(
        target=lhs,
        source=rhs,
        bind_value=functools.partial(
            bind_assign_value,
            anno_ty=anno_ty,
            symbolic_declarations=collect_symbolic_var_declarations(lhs, node.value),
        ),
        allow_shadowing=True,
    )


@dispatch.register(token="relax", type_name="Return")
def visit_return(self: Parser, node: doc.Assign) -> None:
    value = self.eval_expr(node.value)
    value = convert_to_expr(value)
    R.func_ret_value(value)


@dispatch.register(token="relax", type_name="If")
def visit_if(self: Parser, node: doc.If) -> None:
    if node.orelse is None:
        raise ValueError("Else statements are required for relax dialect.")
    with R.If(self.eval_expr(node.test)) as if_frame:
        with self.var_table.with_frame():
            with R.Then():
                self.visit_body(node.body)
        with self.var_table.with_frame():
            with R.Else():
                self.visit_body(node.orelse)
    self.var_table.add(if_frame.var_name, if_frame.var, allow_shadowing=True)


@dispatch.register(token="relax", type_name="enter_token")
def enter_token(self: Parser) -> dict[str, Any]:
    def relax_call(self, *args) -> Expr:
        args = [convert_to_expr(arg) if isinstance(arg, tuple) else arg for arg in args]

        if all(isinstance(x, Expr) for x in args):
            return relax.Call(self, args)
        arg_types = [type(x) for x in args]
        raise RuntimeError(f"Do not know how to handle GlobalVar.__call__ for types {arg_types}")

    context = {"GlobalVar.__call__": GlobalVar.__call__}
    GlobalVar.__call__ = relax_call
    return context


@dispatch.register(token="relax", type_name="exit_token")
def exit_token(self: Parser, context: dict[str, Any]) -> None:
    assert "GlobalVar.__call__" in context
    GlobalVar.__call__ = context.get("GlobalVar.__call__")
