# 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
"""Developer API of add/remove/replace bindings in Relax."""

import tvm_ffi

import tvm
from tvm.runtime import Object

from . import Binding, DataflowBlock, Expr, Function, Var, _ffi_api


@tvm_ffi.register_object("relax.DataflowBlockRewrite")
class DataflowBlockRewrite(Object):
    """
    A binding/statement-level dataflow block rewriter.

    Notes
    -----
    Due to the immutable and copy-on-write nature of TVM AST nodes, the rewriting is not done in
    place. Instead, a new DataflowBlock is created and returned with mutated_dfb. Similarly, its new
    root Function is created and returned by mutated_root_fn. To apply this change for an IRModule,
    use mutate_irmodule which rewrites the old function that registered in the constructor.
    """

    __slots__ = ("__dict__",)

    def __init__(self, dfb: DataflowBlock, root_fn: Function):
        """
        Construct a rewriter with the DataflowBlock to rewrite and its root function.

        Parameters
        ----------
        dfb : DataflowBlock
            The DataflowBlock to rewrite.
        root_fn : Function
            The root function of the DataflowBlock.
        """
        self.func_name = root_fn.__name__ if hasattr(root_fn, "__name__") else None
        self.__init_handle_by_constructor__(
            _ffi_api.DataflowBlockRewrite,
            dfb,
            root_fn,  # type: ignore
        )

    def replace_all_uses(self, old_var: Var, new_var: Var) -> None:
        """
        Replace all uses of old_var with new_var.

        Parameters
        ----------
        old_var : Var
            The old variable to replace.
        new_var : Var
            The new variable to replace with.
        """
        _ffi_api.dfb_rewrite_replace_all_uses(self, old_var, new_var)  # type: ignore

    def add_binding(self, binding: Binding) -> None:
        return _ffi_api.dfb_rewrite_add_binding(self, binding)  # type: ignore

    def add(self, expr: Expr, name: str | None = None, is_dfvar: bool = False) -> None:
        """
        Add a new statement to the DataflowBlock with an automatically generated variable name.

        Parameters
        ----------
        expr : Expr
            The expression to add.
        name : Optional[str], optional
            Variable name, by default None
        is_dfvar : bool, optional
            The variable type, by default False

        Notes
        -----
        If the variable name is not given, it will be automatically generated in a form of
        "tmp${COUNTER}". The variable type will be DataflowVar if is_dfvar is True, otherwise
        it will be Var. Being Var means the variables are output variables of the DataflowBlock.
        While being DataflowVar means the variables are internal variables of the DataflowBlock.
        """
        _ffi_api.dfb_rewrite_add(self, expr, name, is_dfvar)  # type: ignore

    def remove_unused(self, var: Var, allow_undef=False) -> None:
        """
        Remove a statement by its variable definition if and only if it is unused.

        Parameters
        ----------
        var : Var
            The unused variable definition.
        allow_undef : bool, optional
            Whether to allow var being undefined variable, by default False

        Raises
        ------
        RuntimeError if the variable is used or undefined (allow_undef=False).
        """
        _ffi_api.dfb_rewrite_remove_unused(self, var, allow_undef)  # type: ignore

    def remove_all_unused(self) -> None:
        """
        Remove all unused variables.

        Notes
        -----
        This could remove unused variables in other DataflowBlocks as well.
        """
        _ffi_api.dfb_rewrite_remove_all_unused(self)  # type: ignore

    def mutated_dfb(self) -> DataflowBlock:
        """
        Returns the mutated DataflowBlock.
        """
        return self.dfb

    def mutated_root_fn(self) -> Function:
        """
        Returns the mutated root function.
        """
        ret = self.root_fn
        if self.func_name:
            ret.__name__ = self.func_name
        return ret

    def mutate_irmodule(self, irmodule: tvm.IRModule) -> tvm.IRModule:
        """
        Return an updated IRModule by replacing the old function with the mutated root function.

        Parameters
        ----------
        irmodule : tvm.IRModule
            The base IRModule to update.

        Returns
        -------
        tvm.IRModule
            The updated IRModule.
        """
        ret = _ffi_api.dfb_rewrite_mutate_irmodule(self, irmodule)  # type: ignore
        if hasattr(irmodule, "__name__"):
            ret.__name__ = irmodule.__name__
        return ret
