# 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.
"""Statement functor utilities for IR transformations"""

from typing import TypeVar

import tvm
from tvm.ir import Range

from . import _ffi_api
from .expr_functor import ExprMutator, ExprVisitor, _visit_array
from .function import PrimFunc

T = TypeVar("T")


class StmtFunctor:
    """An abstract visitor over Statement, with visiting functions defined for each Stmt type."""

    def __init__(self):
        self._dispatch_map = {
            "tirx.Bind": self.visit_bind_,
            "tirx.AttrStmt": self.visit_attr_,
            "tirx.IfThenElse": self.visit_if_then_else_,
            "tirx.For": self.visit_for_,
            "tirx.While": self.visit_while_,
            "tirx.Break": self.visit_break_,
            "tirx.Continue": self.visit_continue_,
            "tirx.Allocate": self.visit_allocate_,
            "tirx.AllocateConst": self.visit_allocate_const_,
            "tirx.DeclBuffer": self.visit_decl_buffer_,
            "tirx.BufferStore": self.visit_buffer_store_,
            "tirx.BufferRealize": self.visit_buffer_realize_,
            "tirx.AssertStmt": self.visit_assert_,
            "tirx.ProducerStore": self.visit_producer_store_,
            "tirx.ProducerRealize": self.visit_producer_realize_,
            "tirx.Prefetch": self.visit_prefetch_,
            "tirx.SeqStmt": self.visit_seqstmt_,
            "tirx.Evaluate": self.visit_evaluate_,
            "tirx.SBlock": self.visit_block_,
            "tirx.SBlockRealize": self.visit_block_realize_,
            "tirx.ScopeIdDefStmt": self.visit_scope_id_def_stmt_,
            "tirx.TilePrimitiveCall": self.visit_op_call_,
            "tirx.AllocBuffer": self.visit_alloc_buffer_,
        }

    def visit_stmt(self, stmt):
        """Apply the visitor to a statement.

        Parameters
        ----------
        stmt : tvm.tirx.Stmt
            The statement to be visited.

        Returns
        -------
        result : Any
            The result of the visit.
        """
        if stmt is None:
            return None
        if isinstance(stmt, tvm.tirx.TilePrimitiveCall):
            # subclass of TilePrimitiveCall only exists in python side
            # and are not handled by dispatch map
            key = "TilePrimitiveCall"
        else:
            key = stmt.__class__.__name__
        if key.endswith("Node"):
            key = key[:-4]  # Remove the "Node" suffix

        key = "tirx." + key
        if key in self._dispatch_map:
            return self._dispatch_map[key](stmt)

        return self.visit_stmt_default_(stmt)

    def visit_stmt_default_(self, op):
        """Default visitor implementation for statements."""
        raise NotImplementedError(f"Do not have a default for {op.__class__.__name__}")

    def visit_bind_(self, op):
        """Visitor for Bind nodes."""
        return self.visit_stmt_default_(op)

    def visit_attr_(self, op):
        """Visitor for AttrStmt nodes."""
        return self.visit_stmt_default_(op)

    def visit_if_then_else_(self, op):
        """Visitor for IfThenElse nodes."""
        return self.visit_stmt_default_(op)

    def visit_for_(self, op):
        """Visitor for For nodes."""
        return self.visit_stmt_default_(op)

    def visit_while_(self, op):
        """Visitor for While nodes."""
        return self.visit_stmt_default_(op)

    def visit_break_(self, op):
        """Visitor for Break nodes."""
        return self.visit_stmt_default_(op)

    def visit_continue_(self, op):
        """Visitor for Continue nodes."""
        return self.visit_stmt_default_(op)

    def visit_allocate_(self, op):
        """Visitor for Allocate nodes."""
        return self.visit_stmt_default_(op)

    def visit_allocate_const_(self, op):
        """Visitor for AllocateConst nodes."""
        return self.visit_stmt_default_(op)

    def visit_decl_buffer_(self, op):
        """Visitor for DeclBuffer nodes."""
        return self.visit_stmt_default_(op)

    def visit_buffer_store_(self, op):
        """Visitor for BufferStore nodes."""
        return self.visit_stmt_default_(op)

    def visit_buffer_realize_(self, op):
        """Visitor for BufferRealize nodes."""
        raise ValueError("BufferRealize is not allowed")

    def visit_assert_(self, op):
        """Visitor for AssertStmt nodes."""
        return self.visit_stmt_default_(op)

    def visit_producer_store_(self, op):
        """Visitor for ProducerStore nodes."""
        raise ValueError("ProducerStore is not allowed")

    def visit_producer_realize_(self, op):
        """Visitor for ProducerRealize nodes."""
        raise ValueError("ProducerRealize is not allowed")

    def visit_prefetch_(self, op):
        """Visitor for Prefetch nodes."""
        raise ValueError("Prefetch is not allowed")

    def visit_seqstmt_(self, op):
        """Visitor for SeqStmt nodes."""
        return self.visit_stmt_default_(op)

    def visit_evaluate_(self, op):
        """Visitor for Evaluate nodes."""
        return self.visit_stmt_default_(op)

    def visit_block_(self, op):
        """Visitor for Block nodes."""
        return self.visit_stmt_default_(op)

    def visit_block_realize_(self, op):
        """Visitor for BlockRealize nodes."""
        return self.visit_stmt_default_(op)

    def visit_scope_id_def_stmt_(self, op):
        """Visitor for ScopeIdDefStmt nodes."""
        return self.visit_stmt_default_(op)

    def visit_op_call_(self, op):
        """Visitor for TilePrimitiveCall nodes."""
        return self.visit_stmt_default_(op)

    def visit_buffer_region_(self, op):
        """Visitor for BufferRegion nodes."""
        return self.visit_stmt_default_(op)

    def visit_alloc_buffer_(self, op):
        """Visitor for AllocBuffer nodes."""
        return self.visit_stmt_default_(op)

    def __call__(self, stmt):
        """Call visitor on statement.

        Parameters
        ----------
        stmt : tvm.tirx.Stmt
            The statement.

        Returns
        -------
        result : Any
            The result of visiting.
        """
        return self.visit_stmt(stmt)


class StmtVisitor(StmtFunctor):
    """A visitor over Stmt.

    This is a visitor that recursively traverses a statement. Subclasses can
    override the visit methods to customize the behavior.
    """

    def visit_expr(self, expr):
        """Visit expressions that occur in a statement.

        This method can be overridden to implement expression
        traversal in a statement visitor.

        Parameters
        ----------
        expr : Expr
            The expression to be visited.
        """
        pass

    def visit_bind_(self, op):
        """Visitor implementation for Bind."""
        self.visit_expr(op.value)

    def visit_attr_(self, op):
        """Visitor implementation for AttrStmt."""
        self.visit_expr(op.value)
        self.visit_stmt(op.body)

    def visit_if_then_else_(self, op):
        """Visitor implementation for IfThenElse."""
        self.visit_expr(op.condition)
        self.visit_stmt(op.then_case)
        if op.else_case:
            self.visit_stmt(op.else_case)

    def visit_for_(self, op):
        """Visitor implementation for For."""
        self.visit_expr(op.min)
        self.visit_expr(op.extent)
        if op.step is not None:
            self.visit_expr(op.step)
        self.visit_stmt(op.body)

    def visit_while_(self, op):
        """Visitor implementation for While."""
        self.visit_expr(op.condition)
        self.visit_stmt(op.body)

    def visit_break_(self, op):
        """Visitor implementation for Break."""
        pass

    def visit_continue_(self, op):
        """Visitor implementation for Continue."""
        pass

    def visit_allocate_(self, op):
        """Visitor implementation for Allocate."""
        _visit_array(op.extents, lambda x: self.visit_expr(x))
        self.visit_stmt(op.body)
        self.visit_expr(op.condition)

    def visit_allocate_const_(self, op):
        """Visitor implementation for AllocateConst."""
        _visit_array(op.extents, lambda x: self.visit_expr(x))
        self.visit_stmt(op.body)

    def visit_decl_buffer_(self, op):
        """Visitor implementation for DeclBuffer."""
        if hasattr(op, "body"):
            self.visit_stmt(op.body)
            return
        return

    def visit_buffer_store_(self, op):
        """Visitor implementation for BufferStore."""
        self.visit_expr(op.value)
        _visit_array(op.indices, lambda x: self.visit_expr(x))
        if op.predicate is not None:
            self.visit_expr(op.predicate)

    def visit_assert_(self, op):
        """Visitor implementation for AssertStmt."""
        self.visit_expr(op.condition)
        for message_part in op.message_parts:
            if tvm.ir.is_prim_expr(message_part):
                self.visit_expr(message_part)

    def visit_seqstmt_(self, op):
        """Visitor implementation for SeqStmt."""
        _visit_array(op.seq, lambda s: self.visit_stmt(s))

    def visit_evaluate_(self, op):
        """Visitor implementation for Evaluate."""
        self.visit_expr(op.value)

    def visit_block_(self, op):
        """Visitor implementation for Block."""
        # Visit IterVars
        for iter_var in op.iter_vars:
            self.visit_expr(iter_var.dom.min)
            self.visit_expr(iter_var.dom.extent)

        # Visit buffer regions (reads and writes)
        def _visit_buffer_region(buffer_region):
            for r in buffer_region.region:
                self.visit_expr(r.min)
                self.visit_expr(r.extent)

        _visit_array(op.reads, _visit_buffer_region)
        _visit_array(op.writes, _visit_buffer_region)

        # Visit match buffers
        for match_buffer in op.match_buffers:
            _visit_buffer_region(match_buffer.source)

        # Visit init statement
        if op.init is not None:
            self.visit_stmt(op.init)

        # Visit body
        self.visit_stmt(op.body)

    def visit_block_realize_(self, op):
        """Visitor implementation for BlockRealize."""
        _visit_array(op.iter_values, lambda x: self.visit_expr(x))
        self.visit_expr(op.predicate)
        self.visit_stmt(op.block)

    def visit_scope_id_def_stmt_(self, op):
        """Visitor implementation for ScopeIdDefStmt.

        Mirrors the C++ visitor: walk extents and preferred_extents via
        ``visit_expr``; there is no body to recurse into (the def vars
        themselves are leaves the visitor doesn't otherwise inspect).
        """
        # The C++ field is named ``def``, which is a Python keyword,
        # so it's accessed via ``getattr``.
        sid = getattr(op, "def")
        if sid.extents is not None:
            for e in sid.extents:
                self.visit_expr(e)
        if sid.preferred_extents is not None:
            for e in sid.preferred_extents:
                self.visit_expr(e)

    def visit_op_call_(self, op):
        """Visitor implementation for TilePrimitiveCall."""
        for arg in op.args:
            if tvm.ir.is_prim_expr(arg):
                self.visit_expr(arg)
            elif isinstance(arg, tvm.tirx.Stmt):
                self.visit_stmt(arg)
            elif isinstance(arg, tvm.tirx.BufferRegion):
                self.visit_buffer_region_(arg)
        for value in op.config.values():
            if tvm.ir.is_prim_expr(value):
                self.visit_expr(value)
            elif isinstance(value, tvm.tirx.Stmt):
                self.visit_stmt(value)

    def visit_buffer_region_(self, op):
        """Visitor implementation for BufferRegion."""

        def _visit_range(range):
            self.visit_expr(range.min)
            self.visit_expr(range.extent)

        _visit_array(op.region, _visit_range)

    def visit_alloc_buffer_(self, op):
        """Visitor implementation for AllocBuffer."""
        if hasattr(op, "body"):
            self.visit_stmt(op.body)
            return
        return


class StmtMutator(StmtFunctor):
    """A mutator over Stmt.

    This is a mutator that recursively transforms a statement. Subclasses can
    override the visit methods to customize the behavior.
    """

    def visit_expr(self, expr):
        """Visit and mutate expressions that occur in a statement.

        This method can be overridden to implement expression
        mutation in a statement mutator.

        Parameters
        ----------
        expr : Expr
            The expression to be visited.

        Returns
        -------
        result : Expr
            The mutated expression.
        """
        return expr

    def visit_bind_(self, op):
        """Mutator implementation for Bind."""
        value = self.visit_expr(op.value)

        if value is op.value:
            return op

        return tvm.tirx.Bind(op.var, value, op.span)

    def visit_attr_(self, op):
        """Mutator implementation for AttrStmt."""
        value = self.visit_expr(op.value)
        body = self.visit_stmt(op.body)

        if value is op.value and body is op.body:
            return op

        return tvm.tirx.AttrStmt(op.node, op.attr_key, value, body, op.span)

    def visit_if_then_else_(self, op):
        """Mutator implementation for IfThenElse."""
        condition = self.visit_expr(op.condition)
        then_case = self.visit_stmt(op.then_case)
        else_case = self.visit_stmt(op.else_case) if op.else_case else None

        if condition is op.condition and then_case is op.then_case and else_case is op.else_case:
            return op

        return tvm.tirx.IfThenElse(condition, then_case, else_case, op.span)

    def visit_for_(self, op):
        """Mutator implementation for For."""
        min_val = self.visit_expr(op.min)
        extent = self.visit_expr(op.extent)
        step = self.visit_expr(op.step) if op.step is not None else None
        body = self.visit_stmt(op.body)

        if min_val is op.min and extent is op.extent and step is op.step and body is op.body:
            return op

        return tvm.tirx.For(
            op.loop_var,
            min_val,
            extent,
            op.kind,
            body,
            op.thread_binding,
            op.annotations,
            step,
            op.span,
        )

    def visit_while_(self, op):
        """Mutator implementation for While."""
        condition = self.visit_expr(op.condition)
        body = self.visit_stmt(op.body)

        if condition is op.condition and body is op.body:
            return op

        return tvm.tirx.While(condition, body, op.span)

    def visit_break_(self, op):
        """Mutator implementation for Break."""
        return op

    def visit_continue_(self, op):
        """Mutator implementation for Continue."""
        return op

    def visit_allocate_(self, op):
        """Mutator implementation for Allocate."""
        extents = [self.visit_expr(extent) for extent in op.extents]
        body = self.visit_stmt(op.body)
        condition = self.visit_expr(op.condition)

        extents_changed = any(old is not new for old, new in zip(op.extents, extents))

        if not extents_changed and body is op.body and condition is op.condition:
            return op

        return tvm.tirx.Allocate(
            op.buffer_var, op.dtype, extents, condition, body, op.annotations, op.span
        )

    def visit_allocate_const_(self, op):
        """Mutator implementation for AllocateConst."""
        extents = [self.visit_expr(extent) for extent in op.extents]
        body = self.visit_stmt(op.body)

        extents_changed = any(old is not new for old, new in zip(op.extents, extents))

        if not extents_changed and body is op.body:
            return op

        # Create the data_or_idx parameter based on what's available
        if op.data is not None:
            data_or_idx = op.data
        elif op.irmod_storage_idx is not None:
            data_or_idx = op.irmod_storage_idx
        else:
            data_or_idx = None

        return tvm.tirx.AllocateConst(
            op.buffer_var, op.dtype, extents, data_or_idx, body, op.annotations, op.span
        )

    def visit_decl_buffer_(self, op):
        """Mutator implementation for DeclBuffer."""
        if hasattr(op, "body"):
            body = self.visit_stmt(op.body)
            if body is op.body:
                return op
            return tvm.tirx.DeclBuffer(op.buffer, body, op.span)
        return op

    def visit_buffer_store_(self, op):
        """Mutator implementation for BufferStore."""
        value = self.visit_expr(op.value)
        indices = [self.visit_expr(idx) for idx in op.indices]
        predicate = self.visit_expr(op.predicate) if op.predicate is not None else None

        indices_changed = any(old is not new for old, new in zip(op.indices, indices))

        if value is op.value and not indices_changed and predicate is op.predicate:
            return op

        return tvm.tirx.BufferStore(op.buffer, value, indices, predicate, op.span)

    def visit_buffer_realize_(self, op):
        """Mutator implementation for BufferRealize."""
        bounds = []
        bounds_changed = False

        for r in op.bounds:
            new_min = self.visit_expr(r.min)
            new_extent = self.visit_expr(r.extent)

            if new_min is not r.min or new_extent is not r.extent:
                bounds_changed = True
                bounds.append(tvm.ir.Range(new_min, new_extent))
            else:
                bounds.append(r)

        condition = self.visit_expr(op.condition)
        body = self.visit_stmt(op.body)

        if not bounds_changed and condition is op.condition and body is op.body:
            return op

        return tvm.tirx.BufferRealize(op.buffer, bounds, condition, body, op.span)

    def visit_assert_(self, op):
        """Mutator implementation for AssertStmt."""
        condition = self.visit_expr(op.condition)
        message_parts = []
        message_parts_changed = False
        for message_part in op.message_parts:
            if tvm.ir.is_prim_expr(message_part):
                new_message_part = self.visit_expr(message_part)
                if new_message_part is not message_part:
                    message_parts_changed = True
                message_parts.append(new_message_part)
            else:
                message_parts.append(message_part)

        if condition is op.condition and not message_parts_changed:
            return op

        return tvm.tirx.AssertStmt(op.kind, condition, message_parts, op.span)

    def visit_producer_store_(self, op):
        """Mutator implementation for ProducerStore."""
        value = self.visit_expr(op.value)
        indices = [self.visit_expr(idx) for idx in op.indices]

        indices_changed = any(old is not new for old, new in zip(op.indices, indices))

        if value is op.value and not indices_changed:
            return op

        return tvm.tirx.ProducerStore(op.producer, value, indices, op.span)

    def visit_producer_realize_(self, op):
        """Mutator implementation for ProducerRealize."""
        bounds = []
        bounds_changed = False

        for r in op.bounds:
            new_min = self.visit_expr(r.min)
            new_extent = self.visit_expr(r.extent)

            if new_min is not r.min or new_extent is not r.extent:
                bounds_changed = True
                bounds.append(tvm.ir.Range(new_min, new_extent))
            else:
                bounds.append(r)

        condition = self.visit_expr(op.condition)
        body = self.visit_stmt(op.body)

        if not bounds_changed and condition is op.condition and body is op.body:
            return op

        return tvm.tirx.ProducerRealize(
            op.producer, bounds, condition, body, op.storage_scope, op.span
        )

    def visit_prefetch_(self, op):
        """Mutator implementation for Prefetch."""
        bounds = []
        bounds_changed = False

        for r in op.bounds:
            new_min = self.visit_expr(r.min)
            new_extent = self.visit_expr(r.extent)

            if new_min is not r.min or new_extent is not r.extent:
                bounds_changed = True
                bounds.append(tvm.ir.Range(new_min, new_extent))
            else:
                bounds.append(r)

        if not bounds_changed:
            return op

        return tvm.tirx.Prefetch(op.buffer, bounds, op.span)

    def visit_seqstmt_(self, op):
        """Mutator implementation for SeqStmt."""
        new_seq = []
        changed = False

        for stmt in op.seq:
            new_stmt = self.visit_stmt(stmt)
            if new_stmt is not stmt:
                changed = True
            if isinstance(new_stmt, tvm.tirx.SeqStmt):
                # Flatten nested SeqStmt
                new_seq.extend(new_stmt.seq)
                changed = True
            else:
                new_seq.append(new_stmt)

        if not changed:
            return op

        if len(new_seq) == 1:
            return new_seq[0]

        return tvm.tirx.SeqStmt(new_seq, op.span)

    def visit_evaluate_(self, op):
        """Mutator implementation for Evaluate."""
        value = self.visit_expr(op.value)

        if value is op.value:
            return op

        return tvm.tirx.Evaluate(value, op.span)

    def visit_block_(self, op):
        """Mutator implementation for Block."""
        # Process iter_vars
        iter_vars = []
        iter_vars_changed = False

        for iv in op.iter_vars:
            old_dom = iv.dom
            new_min = self.visit_expr(old_dom.min)
            new_extent = self.visit_expr(old_dom.extent)

            if new_min is not old_dom.min or new_extent is not old_dom.extent:
                iter_vars_changed = True
                new_dom = tvm.ir.Range(new_min, new_extent)
                iter_vars.append(tvm.tirx.IterVar(new_dom, iv.var, iv.iter_type, iv.thread_tag))
            else:
                iter_vars.append(iv)

        # Process reads/writes buffer regions
        def _mutate_buffer_regions(regions):
            new_regions = []
            regions_changed = False

            for region in regions:
                new_ranges = []
                ranges_changed = False

                for r in region.region:
                    new_min = self.visit_expr(r.min)
                    new_extent = self.visit_expr(r.extent)

                    if new_min is not r.min or new_extent is not r.extent:
                        ranges_changed = True
                        new_ranges.append(tvm.ir.Range(new_min, new_extent))
                    else:
                        new_ranges.append(r)

                if ranges_changed:
                    regions_changed = True
                    new_regions.append(tvm.tirx.BufferRegion(region.buffer, new_ranges))
                else:
                    new_regions.append(region)

            return new_regions, regions_changed

        reads, reads_changed = _mutate_buffer_regions(op.reads)
        writes, writes_changed = _mutate_buffer_regions(op.writes)

        # Process match buffers
        match_buffers = []
        match_buffers_changed = False

        for match_buffer in op.match_buffers:
            source_region = match_buffer.source
            new_ranges = []
            ranges_changed = False

            for r in source_region.region:
                new_min = self.visit_expr(r.min)
                new_extent = self.visit_expr(r.extent)

                if new_min is not r.min or new_extent is not r.extent:
                    ranges_changed = True
                    new_ranges.append(tvm.ir.Range(new_min, new_extent))
                else:
                    new_ranges.append(r)

            if ranges_changed:
                match_buffers_changed = True
                new_source = tvm.tirx.BufferRegion(source_region.buffer, new_ranges)
                match_buffers.append(tvm.tirx.MatchBufferRegion(match_buffer.buffer, new_source))
            else:
                match_buffers.append(match_buffer)

        # Process init and body
        init = self.visit_stmt(op.init) if op.init is not None else None
        body = self.visit_stmt(op.body)

        # Check if anything changed
        if (
            not iter_vars_changed
            and not reads_changed
            and not writes_changed
            and not match_buffers_changed
            and (init is op.init or (init is None and op.init is None))
            and body is op.body
        ):
            return op
        return tvm.tirx.SBlock(
            iter_vars,
            reads,
            writes,
            op.name_hint,
            body,
            init,
            op.alloc_buffers,
            match_buffers,
            op.annotations,
        )

    def visit_block_realize_(self, op):
        """Mutator implementation for BlockRealize."""
        iter_values = [self.visit_expr(val) for val in op.iter_values]
        predicate = self.visit_expr(op.predicate)
        block = self.visit_stmt(op.block)

        iter_values_changed = any(old is not new for old, new in zip(op.iter_values, iter_values))

        if not iter_values_changed and predicate is op.predicate and block is op.block:
            return op

        if not isinstance(block, tvm.tirx.SBlock):
            raise TypeError(f"Expected SBlock, but got {type(block)}")

        return tvm.tirx.SBlockRealize(iter_values, predicate, block)

    def visit_scope_id_def_stmt_(self, op):
        """Mutator implementation for ScopeIdDefStmt.

        Mirrors the C++ mutator: rewrite ``extents`` and
        ``preferred_extents`` via ``visit_expr``. Deferred-extent defs
        (extents is None) and unchanged extents pass through.
        """
        from .exec_scope import _SCOPE_BINDING_TO_PARENT_CUR, ScopeIdDef

        # ``def`` is a Python keyword; access the C++ field via ``getattr``.
        sid = getattr(op, "def")
        changed = False

        def _walk(arr):
            nonlocal changed
            if arr is None:
                return None
            out = []
            for e in arr:
                ne = self.visit_expr(e)
                if ne is not e:
                    changed = True
                out.append(ne)
            return out

        new_extents = _walk(sid.extents)
        new_pref = _walk(sid.preferred_extents)
        if not changed:
            return op
        parent, cur = _SCOPE_BINDING_TO_PARENT_CUR[sid.scope]
        new_def = ScopeIdDef(sid.def_ids, new_extents, parent, cur, new_pref)
        return tvm.tirx.ScopeIdDefStmt(new_def, op.span)

    def visit_op_call_(self, op):
        """Mutator implementation for TilePrimitiveCall."""
        new_args = []
        args_changed = False

        for arg in op.args:
            if tvm.ir.is_prim_expr(arg):
                new_arg = self.visit_expr(arg)
            elif isinstance(arg, tvm.tirx.Stmt):
                new_arg = self.visit_stmt(arg)
            elif isinstance(arg, tvm.tirx.BufferRegion):
                new_arg = self.visit_buffer_region_(arg)
            else:
                new_arg = arg

            if new_arg is not arg:
                args_changed = True
            new_args.append(new_arg)

        # Also mutate Expr values in the config map
        new_config = {}
        config_changed = False
        for key, value in op.config.items():
            if tvm.ir.is_prim_expr(value):
                new_value = self.visit_expr(value)
            elif isinstance(value, tvm.tirx.Stmt):
                new_value = self.visit_stmt(value)
            else:
                new_value = value
            if new_value is not value:
                config_changed = True
            new_config[key] = new_value

        if not args_changed and not config_changed:
            return op

        return tvm.tirx.TilePrimitiveCall(
            *new_args,
            op=op.op,
            workspace=op.workspace,
            config=new_config,
            dispatch=op.dispatch,
            scope=op.scope,
        )

    def visit_buffer_region_(self, op):
        """Mutator implementation for BufferRegion."""

        def _mutate_range(range):
            new_min = self.visit_expr(range.min)
            new_extent = self.visit_expr(range.extent)

            if new_min is range.min and new_extent is range.extent:
                return range
            else:
                return Range.from_min_extent(new_min, new_extent)

        region = [_mutate_range(r) for r in op.region]

        if all(old_r is new_r for old_r, new_r in zip(op.region, region)):
            return op
        else:
            return tvm.tirx.BufferRegion(op.buffer, region)

    def visit_alloc_buffer_(self, op):
        """Mutator implementation for AllocBuffer."""
        if hasattr(op, "body"):
            body = self.visit_stmt(op.body)
            if body is op.body:
                return op
            return tvm.tirx.AllocBuffer(op.buffer, body, op.annotations, op.span)
        return op

    def __call__(self, stmt):
        """Call mutator on statement.

        Parameters
        ----------
        stmt : tvm.tirx.Stmt
            The statement to be mutated.

        Returns
        -------
        result : tvm.tirx.Stmt
            The mutated statement
        """
        return self.visit_stmt(stmt)


class StmtExprVisitor(StmtVisitor, ExprVisitor):
    """A visitor over both statements and expressions.

    This class inherits from both StmtVisitor and ExprVisitor to recursively visit
    both statements and expressions.
    """

    def __init__(self):
        StmtVisitor.__init__(self)
        self._stmt_dispatch_map = self._dispatch_map.copy()
        ExprVisitor.__init__(self)
        self._expr_dispatch_map = self._dispatch_map.copy()
        self._dispatch_map = {}
        self._dispatch_map.update(self._stmt_dispatch_map)
        self._dispatch_map.update(self._expr_dispatch_map)

    def visit_expr(self, expr):
        """Visit an expression used in a statement.

        Parameters
        ----------
        expr : Expr
            The expression to be visited.
        """
        return ExprVisitor.visit_expr(self, expr)


class StmtExprMutator(StmtMutator, ExprMutator):
    """A mutator over both statements and expressions.

    This class inherits from both StmtMutator and ExprMutator to recursively transform
    both statements and expressions.
    """

    def __init__(self):
        StmtMutator.__init__(self)
        self._stmt_dispatch_map = self._dispatch_map.copy()
        ExprMutator.__init__(self)
        self._expr_dispatch_map = self._dispatch_map.copy()
        self._dispatch_map = {}
        self._dispatch_map.update(self._stmt_dispatch_map)
        self._dispatch_map.update(self._expr_dispatch_map)

    def visit_expr(self, expr):
        """Mutate an expression used in a statement.

        Parameters
        ----------
        expr : Expr
            The expression to be mutated.

        Returns
        -------
        result : Expr
            The mutated expression.
        """
        return ExprMutator.visit_expr(self, expr)


def ir_transform(stmt, preorder, postorder, only_enable=None):
    """Recursively visit and transform ir nodes in post DFS order.

    Parameters
    ----------
    stmt : tvm.tirx.Stmt
        The input to be transformed.

    preorder: function
        The function called in before recursive mutation
        If preorder returns None, then the transform will proceed to recursive call.
        If preorder returns a not None tvm.tirx.Stmt/Expr, the transformer will simply return it and
        won't do further recursion.

    postorder : function
        The function called after recursive mutation.

    only_enable : Optional[List[str]]
        List of types that we only enable.

    Returns
    -------
    result : tvm.tirx.Stmt
        The result.
    """
    return _ffi_api.IRTransform(stmt, preorder, postorder, only_enable)  # type: ignore


def post_order_visit(node, fvisit):
    """Recursively visit a statement or expression in post DFS order, applying fvisit.
       Each node is guaranteed to be visited only once.

    Parameters
    ----------
    node : tvm.tirx.Stmt or tvm.ir.Expr
        The statement or expression to visit.

    fvisit: function
        The visitor function.
    """
    return _ffi_api.PostOrderVisit(node, fvisit)  # type: ignore


def pre_order_visit(node, fvisit):
    """Recursively visit a statement or expression in pre-order, applying fvisit.
       If fvisit returns False, it won't visit the children of the node.

    Parameters
    ----------
    node : tvm.tirx.Stmt or tvm.ir.Expr
        The statement or expression to visit.

    fvisit: function of the signature Object -> bool
        The visitor function.
    """
    return _ffi_api.PreOrderVisit(node, fvisit)  # type: ignore


def substitute(node, vmap):
    """Substitute the var specified by vmap.

    Parameters
    ----------
    node: ObjectRef
        The input.

    vmap : Dict[Var, Expr]
        The variable mapping.

    Returns
    -------
    result : tvm.tirx.Stmt
        The result.
    """
    return _ffi_api.Substitute(node, vmap)  # type: ignore


def renew_defs(func: PrimFunc):
    """Re-generate the definition nodes for a TIR, including VarDef, BufferDef.
    This pass works as a simple DeepCopy to duplicate a function with different Vars and
    Buffers but the same behavior

    Parameters
    ----------
    func: PrimFunc
        The input function

    Returns
    -------
    result : PrimFunc
        The new generated func.
    """
    return _ffi_api.RenewDefs(func)  # type: ignore
