# 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=import-error, invalid-name, no-member, too-many-locals, too-many-arguments, undefined-variable, too-many-nested-blocks, too-many-branches, too-many-statements, too-many-function-args
"""Non-maximum suppression operator"""

import tvm
from tvm import te
from tvm.script.ir_builder import IRBuilder
from tvm.script.ir_builder import tirx as T
from tvm.tirx import if_then_else

from .. import reduction
from ..math import cast
from ..scan import cumsum
from ..sort import argsort
from ..transform import gather, reshape
from .nms_util import (
    binary_search,
    collect_selected_indices,
    collect_selected_indices_and_scores,
    run_all_class_nms,
)


def _get_valid_counts_ir(
    data, score_threshold, id_index, score_index, valid_count, out_tensor, out_indices
):
    """IR for get_valid_counts. Filters boxes by score and compacts valid ones to the top."""
    batch_size = data.shape[0]
    num_anchors = data.shape[1]
    box_data_length = data.shape[2]

    with IRBuilder() as ib:
        data = T.buffer_proxy(data)
        valid_count = T.buffer_proxy(valid_count)
        out_tensor = T.buffer_proxy(out_tensor)
        out_indices = T.buffer_proxy(out_indices)

        with T.parallel(0, batch_size) as i:
            valid_count[i] = T.int32(0)

            with T.serial(0, num_anchors) as j:
                score = data[i, j, score_index]
                if id_index < 0:
                    is_valid = score > score_threshold
                else:
                    is_valid = tvm.tirx.all(score > score_threshold, data[i, j, id_index] >= 0)

                with T.If(is_valid):
                    with T.Then():
                        cur = valid_count[i]
                        with T.serial(0, box_data_length) as k:
                            out_tensor[i, cur, k] = data[i, j, k]
                        out_indices[i, cur] = j
                        valid_count[i] = cur + 1

            # Fill remaining slots with -1
            with T.serial(0, num_anchors) as j:
                with T.If(j >= valid_count[i]):
                    with T.Then():
                        with T.serial(0, box_data_length) as k:
                            out_tensor[i, j, k] = tvm.tirx.Cast(data.dtype, T.float32(-1.0))
                        out_indices[i, j] = T.int32(-1)

        return ib.get()


def get_valid_counts(data, score_threshold=0, id_index=0, score_index=1):
    """Get valid count of bounding boxes given a score threshold.
    Also moves valid boxes to the top of input data.

    Parameters
    ----------
    data : tvm.te.Tensor
        Input data. 3-D tensor with shape [batch_size, num_anchors, elem_length].

    score_threshold : optional, float
        Lower limit of score for valid bounding boxes.

    id_index : optional, int
        Index of the class categories, -1 to disable.

    score_index: optional, int
        Index of the scores/confidence of boxes.

    Returns
    -------
    valid_count : tvm.te.Tensor
        1-D tensor for valid number of boxes, shape [batch_size].

    out_tensor : tvm.te.Tensor
        Rearranged data tensor, shape [batch_size, num_anchors, elem_length].

    out_indices: tvm.te.Tensor
        Related index in input data, shape [batch_size, num_anchors].
    """
    batch_size = data.shape[0]
    num_anchors = data.shape[1]
    box_data_length = data.shape[2]

    is_score_threshold_tensor = isinstance(score_threshold, te.Tensor)
    if not is_score_threshold_tensor:
        score_threshold = tvm.tirx.const(score_threshold, dtype=data.dtype)

    id_index_const = tvm.tirx.const(id_index, "int32")
    score_index_const = tvm.tirx.const(score_index, "int32")

    valid_count_buf = tvm.tirx.decl_buffer((batch_size,), "int32", "valid_count", layout=None)
    out_tensor_buf = tvm.tirx.decl_buffer(
        (batch_size, num_anchors, box_data_length), data.dtype, "out_tensor", layout=None
    )
    out_indices_buf = tvm.tirx.decl_buffer(
        (batch_size, num_anchors), "int32", "out_indices", layout=None
    )

    if is_score_threshold_tensor:
        score_thresh_buf = tvm.tirx.decl_buffer(
            score_threshold.shape, score_threshold.dtype, "score_threshold", layout=None
        )
        valid_count, out_tensor, out_indices = te.extern(
            [(batch_size,), (batch_size, num_anchors, box_data_length), (batch_size, num_anchors)],
            [data, score_threshold],
            lambda ins, outs: _get_valid_counts_ir(
                ins[0],
                ins[1],
                id_index_const,
                score_index_const,
                outs[0],
                outs[1],
                outs[2],
            ),
            dtype=["int32", data.dtype, "int32"],
            out_buffers=[valid_count_buf, out_tensor_buf, out_indices_buf],
            in_buffers=[
                tvm.tirx.decl_buffer(data.shape, data.dtype, "data", layout=None),
                score_thresh_buf,
            ],
            name="get_valid_counts",
            tag="get_valid_counts",
        )
    else:
        # score_threshold is a TIR constant, not a tensor
        def _ir_with_const_threshold(ins, outs):
            return _get_valid_counts_ir(
                ins[0],
                score_threshold,
                id_index_const,
                score_index_const,
                outs[0],
                outs[1],
                outs[2],
            )

        valid_count, out_tensor, out_indices = te.extern(
            [(batch_size,), (batch_size, num_anchors, box_data_length), (batch_size, num_anchors)],
            [data],
            _ir_with_const_threshold,
            dtype=["int32", data.dtype, "int32"],
            out_buffers=[valid_count_buf, out_tensor_buf, out_indices_buf],
            in_buffers=[tvm.tirx.decl_buffer(data.shape, data.dtype, "data", layout=None)],
            name="get_valid_counts",
            tag="get_valid_counts",
        )

    return valid_count, out_tensor, out_indices


def _classic_nms_ir(
    data,
    sorted_index,
    valid_count,
    indices,
    batch_size,
    num_anchors,
    box_data_length,
    max_output_size,
    iou_threshold,
    force_suppress,
    top_k,
    coord_start,
    score_index,
    id_index,
    return_indices,
    out_data,
    out_box_indices,
    out_valid_box_count,
    soft_nms_sigma=0.0,
    score_threshold=0.0,
):
    """IR for classic single-class non-maximum suppression."""
    with IRBuilder() as ib:
        data = T.buffer_proxy(data)
        sorted_index = T.buffer_proxy(sorted_index)
        valid_count = T.buffer_proxy(valid_count)
        indices = T.buffer_proxy(indices)
        out_data = T.buffer_proxy(out_data)
        out_box_indices = T.buffer_proxy(out_box_indices)
        if out_valid_box_count is not None:
            out_valid_box_count = T.buffer_proxy(out_valid_box_count)

        is_soft_nms = soft_nms_sigma > 0.0
        # For hard NMS the historical threshold is 0.0; for soft NMS use score_threshold.
        thresh = tvm.tirx.Cast(data.dtype, T.float32(score_threshold if is_soft_nms else 0.0))

        with T.parallel(0, batch_size) as i:
            # Step 1: Reorder data by sorted score
            nkeep_buf = T.alloc_buffer((1,), "int32", scope="local")
            nkeep_local = T.buffer_proxy(nkeep_buf)
            nkeep_local[0] = valid_count[i]
            with T.If(tvm.tirx.all(top_k > 0, top_k < nkeep_local[0])):
                with T.Then():
                    nkeep_local[0] = top_k

            # Copy sorted boxes to output
            with T.serial(0, num_anchors) as j:
                with T.If(j < nkeep_local[0]):
                    with T.Then():
                        src_idx = sorted_index[i, j]
                        with T.serial(0, box_data_length) as k:
                            out_data[i, j, k] = data[i, src_idx, k]
                        out_box_indices[i, j] = sorted_index[i, j]
                    with T.Else():
                        with T.serial(0, box_data_length) as k:
                            out_data[i, j, k] = tvm.tirx.Cast(data.dtype, T.float32(-1.0))
                        out_box_indices[i, j] = T.int32(-1)

            # Step 2: Apply NMS - greedy suppression
            num_valid_boxes_buf = T.alloc_buffer((1,), "int32", scope="local")
            num_valid_boxes = T.buffer_proxy(num_valid_boxes_buf)
            num_valid_boxes[0] = T.int32(0)
            best_idx_buf = T.alloc_buffer((1,), "int32", scope="local")
            best_idx = T.buffer_proxy(best_idx_buf)
            best_score_buf = T.alloc_buffer((1,), data.dtype, scope="local")
            best_score = T.buffer_proxy(best_score_buf)
            tmp_idx_buf = T.alloc_buffer((1,), "int32", scope="local")
            tmp_idx = T.buffer_proxy(tmp_idx_buf)
            tmp_val_buf = T.alloc_buffer((1,), data.dtype, scope="local")
            tmp_val = T.buffer_proxy(tmp_val_buf)
            zero = tvm.tirx.Cast(data.dtype, T.float32(0.0))

            def compute_iou(lhs_idx, rhs_idx):
                lhs_l = tvm.te.min(
                    out_data[i, lhs_idx, coord_start],
                    out_data[i, lhs_idx, coord_start + 2],
                )
                lhs_t = tvm.te.min(
                    out_data[i, lhs_idx, coord_start + 1],
                    out_data[i, lhs_idx, coord_start + 3],
                )
                lhs_r = tvm.te.max(
                    out_data[i, lhs_idx, coord_start],
                    out_data[i, lhs_idx, coord_start + 2],
                )
                lhs_b = tvm.te.max(
                    out_data[i, lhs_idx, coord_start + 1],
                    out_data[i, lhs_idx, coord_start + 3],
                )
                rhs_l = tvm.te.min(
                    out_data[i, rhs_idx, coord_start],
                    out_data[i, rhs_idx, coord_start + 2],
                )
                rhs_t = tvm.te.min(
                    out_data[i, rhs_idx, coord_start + 1],
                    out_data[i, rhs_idx, coord_start + 3],
                )
                rhs_r = tvm.te.max(
                    out_data[i, rhs_idx, coord_start],
                    out_data[i, rhs_idx, coord_start + 2],
                )
                rhs_b = tvm.te.max(
                    out_data[i, rhs_idx, coord_start + 1],
                    out_data[i, rhs_idx, coord_start + 3],
                )
                width = tvm.te.max(zero, tvm.te.min(lhs_r, rhs_r) - tvm.te.max(lhs_l, rhs_l))
                height = tvm.te.max(zero, tvm.te.min(lhs_b, rhs_b) - tvm.te.max(lhs_t, rhs_t))
                intersection = height * width
                union = (
                    (lhs_r - lhs_l) * (lhs_b - lhs_t)
                    + (rhs_r - rhs_l) * (rhs_b - rhs_t)
                    - intersection
                )
                return tvm.tirx.Select(union <= zero, zero, intersection / union)

            if is_soft_nms:
                # LiteRT soft-NMS selects the current highest-score candidate each round.
                soft_nms_scale = tvm.tirx.Cast(data.dtype, T.float32(-0.5 / soft_nms_sigma))

                with T.serial(0, nkeep_local[0]) as _:
                    with T.If(
                        tvm.tirx.Select(
                            max_output_size > 0,
                            num_valid_boxes[0] < max_output_size,
                            tvm.tirx.const(True),
                        )
                    ):
                        with T.Then():
                            best_idx[0] = T.int32(-1)
                            best_score[0] = thresh

                            with T.serial(0, nkeep_local[0]) as j:
                                with T.If(
                                    tvm.tirx.all(
                                        j >= num_valid_boxes[0],
                                        out_box_indices[i, j] >= 0,
                                        out_data[i, j, score_index] > best_score[0],
                                    )
                                ):
                                    with T.Then():
                                        best_idx[0] = j
                                        best_score[0] = out_data[i, j, score_index]

                            with T.If(best_idx[0] >= 0):
                                with T.Then():
                                    with T.If(best_idx[0] != num_valid_boxes[0]):
                                        with T.Then():
                                            tmp_idx[0] = out_box_indices[i, num_valid_boxes[0]]
                                            out_box_indices[i, num_valid_boxes[0]] = (
                                                out_box_indices[i, best_idx[0]]
                                            )
                                            out_box_indices[i, best_idx[0]] = tmp_idx[0]

                                            with T.serial(0, box_data_length) as k:
                                                tmp_val[0] = out_data[i, num_valid_boxes[0], k]
                                                out_data[i, num_valid_boxes[0], k] = out_data[
                                                    i, best_idx[0], k
                                                ]
                                                out_data[i, best_idx[0], k] = tmp_val[0]

                                    with T.serial(0, nkeep_local[0]) as j:
                                        with T.If(
                                            tvm.tirx.all(
                                                j > num_valid_boxes[0],
                                                out_box_indices[i, j] >= 0,
                                                out_data[i, j, score_index] > thresh,
                                            )
                                        ):
                                            with T.Then():
                                                do_suppress = tvm.tirx.const(False)
                                                if force_suppress:
                                                    do_suppress = tvm.tirx.const(True)
                                                elif id_index >= 0:
                                                    do_suppress = (
                                                        out_data[i, num_valid_boxes[0], id_index]
                                                        == out_data[i, j, id_index]
                                                    )
                                                else:
                                                    do_suppress = tvm.tirx.const(True)

                                                with T.If(do_suppress):
                                                    with T.Then():
                                                        iou = compute_iou(num_valid_boxes[0], j)

                                                        with T.If(iou >= iou_threshold):
                                                            with T.Then():
                                                                out_box_indices[i, j] = T.int32(-1)
                                                        with T.If(iou < iou_threshold):
                                                            with T.Then():
                                                                out_data[i, j, score_index] = (
                                                                    out_data[i, j, score_index]
                                                                    * tvm.tirx.exp(
                                                                        soft_nms_scale * iou * iou
                                                                    )
                                                                )
                                                                with T.If(
                                                                    out_data[i, j, score_index]
                                                                    <= thresh
                                                                ):
                                                                    with T.Then():
                                                                        out_box_indices[i, j] = (
                                                                            T.int32(-1)
                                                                        )

                                    num_valid_boxes[0] = num_valid_boxes[0] + 1

                if return_indices:
                    out_valid_box_count[i, 0] = num_valid_boxes[0]

                    with T.serial(0, num_anchors) as j:
                        with T.If(j < num_valid_boxes[0]):
                            with T.Then():
                                orig_idx = out_box_indices[i, j]
                                out_box_indices[i, j] = indices[i, orig_idx]
                        with T.If(j >= num_valid_boxes[0]):
                            with T.Then():
                                with T.serial(0, box_data_length) as k:
                                    out_data[i, j, k] = tvm.tirx.Cast(data.dtype, T.float32(-1.0))
                                out_box_indices[i, j] = T.int32(-1)
                else:
                    with T.serial(0, num_anchors) as j:
                        with T.If(j >= num_valid_boxes[0]):
                            with T.Then():
                                with T.serial(0, box_data_length) as k:
                                    out_data[i, j, k] = tvm.tirx.Cast(data.dtype, T.float32(-1.0))
            else:
                with T.serial(0, nkeep_local[0]) as j:
                    with T.If(
                        tvm.tirx.all(
                            out_data[i, j, score_index] > thresh,
                            tvm.tirx.Select(
                                max_output_size > 0,
                                num_valid_boxes[0] < max_output_size,
                                tvm.tirx.const(True),
                            ),
                        )
                    ):
                        with T.Then():
                            num_valid_boxes[0] = num_valid_boxes[0] + 1

                            with T.serial(0, nkeep_local[0]) as k:
                                with T.If(
                                    tvm.tirx.all(k > j, out_data[i, k, score_index] > thresh)
                                ):
                                    with T.Then():
                                        do_suppress = tvm.tirx.const(False)
                                        if force_suppress:
                                            do_suppress = tvm.tirx.const(True)
                                        elif id_index >= 0:
                                            do_suppress = (
                                                out_data[i, j, id_index] == out_data[i, k, id_index]
                                            )
                                        else:
                                            do_suppress = tvm.tirx.const(True)

                                        with T.If(do_suppress):
                                            with T.Then():
                                                iou = compute_iou(j, k)

                                                with T.If(iou >= iou_threshold):
                                                    with T.Then():
                                                        out_data[i, k, score_index] = tvm.tirx.Cast(
                                                            data.dtype, T.float32(-1.0)
                                                        )
                                                        out_box_indices[i, k] = T.int32(-1)

                        with T.Else():
                            with T.serial(0, box_data_length) as k:
                                out_data[i, j, k] = tvm.tirx.Cast(data.dtype, T.float32(-1.0))
                            out_box_indices[i, j] = T.int32(-1)

                if return_indices:
                    valid_idx_buf = T.alloc_buffer((1,), "int32", scope="local")
                    valid_idx = T.buffer_proxy(valid_idx_buf)
                    valid_idx[0] = T.int32(0)

                    with T.serial(0, num_anchors) as j:
                        with T.If(out_box_indices[i, j] >= 0):
                            with T.Then():
                                orig_idx = out_box_indices[i, j]
                                out_box_indices[i, valid_idx[0]] = indices[i, orig_idx]
                                valid_idx[0] = valid_idx[0] + 1

                    out_valid_box_count[i, 0] = valid_idx[0]

                    with T.serial(0, num_anchors) as j:
                        with T.If(j >= valid_idx[0]):
                            with T.Then():
                                out_box_indices[i, j] = T.int32(-1)

        return ib.get()


def non_max_suppression(
    data,
    valid_count,
    indices,
    max_output_size=-1,
    iou_threshold=0.5,
    force_suppress=False,
    top_k=-1,
    coord_start=2,
    score_index=1,
    id_index=0,
    return_indices=True,
    invalid_to_bottom=False,
    soft_nms_sigma=0.0,
    score_threshold=0.0,
):
    """Non-maximum suppression operator for object detection.

    Parameters
    ----------
    data : tvm.te.Tensor
        3-D tensor with shape [batch_size, num_anchors, elem_length].

    valid_count : tvm.te.Tensor
        1-D tensor for valid number of boxes, shape [batch_size].

    indices : tvm.te.Tensor
        2-D tensor with shape [batch_size, num_anchors].

    max_output_size : optional, int
        Max number of output valid boxes for each instance.
        Return all valid boxes if the value is less than 0.

    iou_threshold : optional, float
        Non-maximum suppression IoU threshold.

    force_suppress : optional, boolean
        Whether to suppress all detections regardless of class_id. When
        ``id_index`` is ``-1``, all valid boxes are treated as belonging to the
        same class, so this flag has the same effect as ``True``.

    top_k : optional, int
        Keep maximum top k detections before nms, -1 for no limit.

    coord_start : required, int
        Start index of the consecutive 4 coordinates.

    score_index: optional, int
        Index of the scores/confidence of boxes.

    id_index : optional, int
        Index of the class categories, -1 to disable.

    return_indices : optional, boolean
        Whether to return box indices in input data.

    invalid_to_bottom : optional, boolean
        Whether to move all valid bounding boxes to the top.

    soft_nms_sigma : optional, float
        Sigma for soft-NMS Gaussian penalty. 0.0 means standard hard NMS.

    score_threshold : optional, float
        Post-decay minimum score for a box to remain eligible during soft-NMS.
        Only used when ``soft_nms_sigma > 0``. This is distinct from
        ``get_valid_counts.score_threshold``, which filters boxes before NMS.

    Returns
    -------
    out : tvm.te.Tensor or tuple of tvm.te.Tensor
        The return tuple shape depends on ``soft_nms_sigma``.
        If ``return_indices`` is ``True`` and ``soft_nms_sigma`` is ``0.0``,
        returns a 2-tuple ``(box_indices, valid_box_count)``.
        If ``return_indices`` is ``True`` and ``soft_nms_sigma > 0``,
        returns a 3-tuple ``(out_data, box_indices, valid_box_count)`` where
        decayed ``out_data`` is prepended and has the same shape as the input
        data.
        Otherwise returns the modified data tensor.
    """
    batch_size = data.shape[0]
    num_anchors = data.shape[1]
    box_data_length = data.shape[2]

    if isinstance(max_output_size, int):
        max_output_size = tvm.tirx.const(max_output_size, dtype="int32")
    if isinstance(iou_threshold, float | int):
        iou_threshold = tvm.tirx.const(iou_threshold, dtype=data.dtype)

    # Sort by score
    score_shape = (batch_size, num_anchors)
    score_tensor = te.compute(
        score_shape, lambda i, j: data[i, j, score_index], name="score_tensor"
    )
    sort_tensor = argsort(score_tensor, valid_count=valid_count, axis=1, is_ascend=False)

    data_buf = tvm.tirx.decl_buffer(data.shape, data.dtype, "data", layout=None)
    sort_buf = tvm.tirx.decl_buffer(
        sort_tensor.shape, sort_tensor.dtype, "sorted_index", layout=None
    )
    valid_count_buf = tvm.tirx.decl_buffer(
        valid_count.shape, valid_count.dtype, "valid_count", layout=None
    )
    indices_buf = tvm.tirx.decl_buffer(indices.shape, indices.dtype, "indices", layout=None)

    out_data_buf = tvm.tirx.decl_buffer(data.shape, data.dtype, "out_data", layout=None)
    out_box_indices_buf = tvm.tirx.decl_buffer(
        (batch_size, num_anchors), "int32", "out_box_indices", layout=None
    )

    if return_indices:
        out_valid_box_count_buf = tvm.tirx.decl_buffer(
            (batch_size, 1), "int32", "out_valid_box_count", layout=None
        )

        out_data, out_box_indices, out_valid_box_count = te.extern(
            [data.shape, (batch_size, num_anchors), (batch_size, 1)],
            [data, sort_tensor, valid_count, indices],
            lambda ins, outs: _classic_nms_ir(
                ins[0],
                ins[1],
                ins[2],
                ins[3],
                batch_size,
                num_anchors,
                box_data_length,
                max_output_size,
                iou_threshold,
                force_suppress,
                top_k,
                coord_start,
                score_index,
                id_index,
                return_indices,
                outs[0],
                outs[1],
                outs[2],
                soft_nms_sigma,
                score_threshold,
            ),
            dtype=[data.dtype, "int32", "int32"],
            out_buffers=[out_data_buf, out_box_indices_buf, out_valid_box_count_buf],
            in_buffers=[data_buf, sort_buf, valid_count_buf, indices_buf],
            name="non_max_suppression",
            tag="non_max_suppression",
        )
        if soft_nms_sigma > 0.0:
            return [out_data, out_box_indices, out_valid_box_count]
        return [out_box_indices, out_valid_box_count]

    out_data, out_box_indices = te.extern(
        [data.shape, (batch_size, num_anchors)],
        [data, sort_tensor, valid_count, indices],
        lambda ins, outs: _classic_nms_ir(
            ins[0],
            ins[1],
            ins[2],
            ins[3],
            batch_size,
            num_anchors,
            box_data_length,
            max_output_size,
            iou_threshold,
            force_suppress,
            top_k,
            coord_start,
            score_index,
            id_index,
            return_indices,
            outs[0],
            outs[1],
            None,
            soft_nms_sigma,
            score_threshold,
        ),
        dtype=[data.dtype, "int32"],
        out_buffers=[out_data_buf, out_box_indices_buf],
        in_buffers=[data_buf, sort_buf, valid_count_buf, indices_buf],
        name="non_max_suppression",
        tag="non_max_suppression",
    )

    if invalid_to_bottom:
        # Rearrange to move valid boxes to top
        return _rearrange_out(out_data, batch_size, num_anchors, box_data_length, score_index)

    return out_data


def _rearrange_out(data, batch_size, num_anchors, box_data_length, score_index):
    """Move valid boxes (score >= 0) to the top of output."""
    out_buf = tvm.tirx.decl_buffer(
        (batch_size, num_anchors, box_data_length), data.dtype, "rearranged", layout=None
    )

    def _rearrange_ir(ins, outs):
        with IRBuilder() as ib:
            data = T.buffer_proxy(ins[0])
            out = T.buffer_proxy(outs[0])

            with T.parallel(0, batch_size) as i:
                valid_idx_buf = T.alloc_buffer((1,), "int32", scope="local")
                valid_idx = T.buffer_proxy(valid_idx_buf)
                valid_idx[0] = T.int32(0)

                with T.serial(0, num_anchors) as j:
                    with T.If(data[i, j, score_index] >= tvm.tirx.Cast(data.dtype, T.float32(0.0))):
                        with T.Then():
                            with T.serial(0, box_data_length) as k:
                                out[i, valid_idx[0], k] = data[i, j, k]
                            valid_idx[0] = valid_idx[0] + 1

                with T.serial(0, num_anchors) as j:
                    with T.If(j >= valid_idx[0]):
                        with T.Then():
                            with T.serial(0, box_data_length) as k:
                                out[i, j, k] = tvm.tirx.Cast(data.dtype, T.float32(-1.0))

            return ib.get()

    return te.extern(
        [(batch_size, num_anchors, box_data_length)],
        [data],
        _rearrange_ir,
        dtype=[data.dtype],
        out_buffers=[out_buf],
        name="rearrange_out",
        tag="rearrange_out",
    )


def _nms_loop(
    batch_size,
    top_k,
    iou_threshold,
    max_output_size,
    valid_count,
    on_new_valid_box_func,
    on_new_invalidated_box_func,
    needs_bbox_check_func,
    calc_overlap_func,
    out_scores,
    num_valid_boxes,
    score_threshold=None,
):
    """NMS loop using modern IRBuilder. Must be called within IRBuilder context."""
    out_scores = T.buffer_proxy(out_scores)
    num_valid_boxes = T.buffer_proxy(num_valid_boxes)

    def nms_inner_loop(i, j, nkeep, num_valid_boxes_local):
        on_new_valid_box_func(0, num_valid_boxes_local[0], i, j)
        num_valid_boxes_local[0] = num_valid_boxes_local[0] + 1

        num_boxes_to_check = nkeep - (j + 1)

        with T.parallel(0, num_boxes_to_check) as _k:
            k = j + 1 + _k

            with T.If(
                tvm.tirx.all(
                    k < nkeep,
                    out_scores[i, k] > 0,  # is the box k still valid?
                    needs_bbox_check_func(i, j, k),
                )
            ):
                with T.Then():
                    iou = calc_overlap_func(i, j, k)

                    with T.If(iou >= iou_threshold):
                        with T.Then():
                            out_scores[i, k] = T.float32(-1.0)
                            on_new_invalidated_box_func(i, k)

    with T.serial(0, batch_size) as i:
        nkeep = if_then_else(tvm.tirx.all(top_k > 0, top_k < valid_count[i]), top_k, valid_count[i])

        with T.If(tvm.tirx.all(iou_threshold > te.const(0), valid_count[i] > te.const(0))):
            with T.Then():
                num_valid_boxes_local_buf = T.alloc_buffer((1,), "int32", scope="local")
                num_valid_boxes_local = T.buffer_proxy(num_valid_boxes_local_buf)
                num_valid_boxes_local[0] = T.int32(0)

                with T.serial(0, nkeep) as j:
                    with T.If(
                        tvm.tirx.all(
                            out_scores[i, j] > -1.0,  # box is still valid
                            num_valid_boxes_local[0] < max_output_size,  # haven't reached max limit
                        )
                    ):
                        with T.Then():
                            if score_threshold is not None:
                                with T.If(out_scores[i, j] > score_threshold[()]):
                                    with T.Then():
                                        nms_inner_loop(i, j, nkeep, num_valid_boxes_local)
                            else:
                                nms_inner_loop(i, j, nkeep, num_valid_boxes_local)

                num_valid_boxes[i] = num_valid_boxes_local[0]

            with T.Else():
                num_valid_boxes[i] = T.int32(0)


def _get_valid_box_count(scores, score_threshold):
    batch_classes, num_boxes = scores.shape

    def searchsorted_ir(scores_buf, score_thresh_buf, valid_count_buf):
        with IRBuilder() as ib:
            with T.parallel(0, batch_classes) as i:
                if hasattr(score_threshold, "shape"):
                    if len(score_threshold.shape) == 0:
                        score_thresh_scalar = score_thresh_buf[()]
                    elif len(score_threshold.shape) == 1 and score_threshold.shape[0] > 0:
                        score_thresh_scalar = score_thresh_buf[0]
                    else:
                        score_thresh_scalar = tvm.tirx.FloatImm("float32", 0.0)
                else:
                    score_thresh_scalar = score_threshold
                binary_search(i, num_boxes, scores_buf, score_thresh_scalar, valid_count_buf)

            return ib.get()

    scores_buf = tvm.tirx.decl_buffer(
        scores.shape, scores.dtype, "scores_buf", data_alignment=8, layout=None
    )
    searchsorted_buf = tvm.tirx.decl_buffer(
        (batch_classes,), "int32", "searchsorted", data_alignment=8, layout=None
    )

    if hasattr(score_threshold, "shape"):
        score_thresh_buf = tvm.tirx.decl_buffer(
            score_threshold.shape,
            score_threshold.dtype,
            "score_thresh_buf",
            data_alignment=8,
            layout=None,
        )
        return te.extern(
            [(batch_classes,)],
            [scores, score_threshold],
            lambda ins, outs: searchsorted_ir(ins[0], ins[1], outs[0]),
            dtype=["int32"],
            in_buffers=[scores_buf, score_thresh_buf],
            out_buffers=[searchsorted_buf],
            name="searchsorted",
            tag="searchsorted",
        )
    else:

        def searchsorted_ir_scalar(scores_buf, valid_count_buf):
            with IRBuilder() as ib:
                with T.parallel(0, batch_classes) as i:
                    if isinstance(score_threshold, te.Tensor):
                        if len(score_threshold.shape) == 0:
                            score_thresh_tir = score_threshold()
                        elif len(score_threshold.shape) == 1 and score_threshold.shape[0] == 1:
                            score_thresh_tir = score_threshold[0]
                        else:
                            score_thresh_tir = tvm.tirx.FloatImm("float32", 0.0)
                    else:
                        score_thresh_tir = tvm.tirx.FloatImm("float32", float(score_threshold))
                    binary_search(i, num_boxes, scores_buf, score_thresh_tir, valid_count_buf)

                return ib.get()

        return te.extern(
            [(batch_classes,)],
            [scores],
            lambda ins, outs: searchsorted_ir_scalar(ins[0], outs[0]),
            dtype=["int32"],
            in_buffers=[scores_buf],
            out_buffers=[searchsorted_buf],
            name="searchsorted",
            tag="searchsorted",
        )


def _collect_selected_indices_ir(
    num_class, selected_indices, num_detections, row_offsets, out, max_output_boxes_per_class=None
):
    batch_classes, _ = selected_indices.shape

    with IRBuilder() as ib:
        with T.seq_scope():
            out = T.buffer_proxy(out)

            # Initialize output buffer to zero
            # Calculate the actual output shape based on max_output_boxes_per_class
            if isinstance(max_output_boxes_per_class, int):
                max_output_rows = batch_classes * max_output_boxes_per_class
            else:
                # Fallback to a reasonable default if max_output_boxes_per_class is not an integer
                max_output_rows = batch_classes * 10
            with T.serial(0, max_output_rows) as init_i:
                with T.serial(0, 3) as init_j:  # 3 columns
                    out[init_i, init_j] = cast(0, "int64")

            with T.parallel(0, batch_classes) as i:
                i_64 = cast(i, "int64")
                batch_id = i_64 // num_class
                class_id = i_64 % num_class

                if isinstance(max_output_boxes_per_class, int):
                    limit = tvm.tirx.min(
                        num_detections[i], tvm.tirx.IntImm("int32", max_output_boxes_per_class)
                    )
                elif isinstance(max_output_boxes_per_class, te.Tensor):
                    if len(max_output_boxes_per_class.shape) == 0:
                        max_boxes_val = max_output_boxes_per_class[()]
                    else:
                        max_boxes_val = max_output_boxes_per_class[0]
                    limit = tvm.tirx.min(num_detections[i], max_boxes_val)
                else:
                    limit = num_detections[i]

                with T.serial(0, limit) as j:
                    out[row_offsets[i] + j, 0] = batch_id
                    out[row_offsets[i] + j, 1] = class_id
                    out[row_offsets[i] + j, 2] = cast(selected_indices[i, j], "int64")

        return ib.get()


def _collect_selected_indices_and_scores_ir(
    selected_indices,
    selected_scores,
    num_detections,
    row_offsets,
    num_total_detections,
    collected_indices,
    collected_scores,
):
    batch_size, num_class = row_offsets.shape
    num_boxes = selected_indices.shape[1]

    with IRBuilder() as ib:
        collected_indices = T.buffer_proxy(collected_indices)
        collected_scores = T.buffer_proxy(collected_scores)
        zero = cast(0, "int64")

        with T.parallel(0, batch_size * num_class) as i:
            i_64 = cast(i, "int64")
            batch_id = i_64 // num_class
            class_id = i_64 % num_class

            with T.serial(0, num_boxes) as j:
                with T.If(j < num_detections[batch_id, class_id]):
                    with T.Then():
                        offset = row_offsets[batch_id, class_id] + j
                        collected_indices[batch_id, offset, 0] = class_id
                        collected_indices[batch_id, offset, 1] = cast(
                            selected_indices[i, j], "int64"
                        )
                        collected_scores[batch_id, offset] = selected_scores[i, j]
                    with T.Else():
                        offset = (
                            num_total_detections[batch_id]
                            + class_id * num_boxes
                            - row_offsets[batch_id, class_id]
                            + j
                            - num_detections[batch_id, class_id]
                        )
                        collected_indices[batch_id, offset, 0] = zero
                        collected_indices[batch_id, offset, 1] = zero
                        collected_scores[batch_id, offset] = T.float32(0.0)

        return ib.get()


def all_class_non_max_suppression(
    boxes,
    scores,
    max_output_boxes_per_class,
    iou_threshold,
    score_threshold,
    output_format="onnx",
    output_shape=None,
):
    """Non-maximum suppression operator for object detection, corresponding to ONNX
    NonMaxSuppression and TensorFlow combined_non_max_suppression.
    NMS is performed for each class separately.

    Parameters
    ----------
    boxes : tvm.te.Tensor
        3-D tensor with shape (batch_size, num_boxes, 4)
    scores : tvm.te.Tensor
        3-D tensor with shape (batch_size, num_classes, num_boxes)
    max_output_boxes_per_class : int or tvm.te.Tensor, optional
        The maxinum number of output selected boxes per class
    iou_threshold : float or tvm.te.Tensor, optional
        IoU test threshold
    score_threshold : float or tvm.te.Tensor, optional
        Score threshold to filter out low score boxes early
    output_format : str, optional
        "onnx" or "tensorflow", see below.

    Returns
    -------
    out : list of tvm.te.Tensor
        If `output_format` is "onnx", the output is two tensors. The first is `indices` of size
        `(batch_size * num_class* num_boxes , 3)` and the second is a scalar tensor
        `num_total_detection` of shape `(1,)` representing the total number of selected
        boxes. The three values in `indices` encode batch, class, and box indices.
        Rows of `indices` are ordered such that selected boxes from batch 0, class 0 come
        first, in descending of scores, followed by boxes from batch 0, class 1 etc. Out of
        `batch_size * num_class* num_boxes` rows of indices, only the first `num_total_detection`
        rows are valid.

        .. note::
            **Important**: The output tensor has a fixed size based on `max_output_boxes_per_class`,
            but only the first `num_total_detection` rows contain valid data. The remaining rows
            may contain garbage values. When comparing with ONNX Runtime or other implementations
            that output dynamic shapes, you should only compare the first
            `num_total_detection` rows.
            Example::

                selected_indices, valid_count = nms_output
                actual_count = int(valid_count.numpy()[0])
                valid_indices = selected_indices.numpy()[:actual_count, :]

        If `output_format` is "tensorflow", the output is three tensors, the first
        is `indices` of size `(batch_size, num_class * num_boxes , 2)`, the second is `scores` of
        size `(batch_size, num_class * num_boxes)`, and the third is `num_total_detection` of size
        `(batch_size,)` representing the total number of selected boxes per batch. The two values
        in `indices` encode class and box indices. Of num_class * num_boxes boxes in `indices` at
        batch b, only the first `num_total_detection[b]` entries are valid. The second axis of
        `indices` and `scores` are sorted within each class by box scores, but not across classes.
        So the box indices and scores for the class 0 come first in a sorted order, followed by
        the class 1 etc.
    """
    batch, num_class, num_boxes = scores.shape
    scores = reshape(scores, (batch * num_class, num_boxes))

    sorted_indices = argsort(scores, axis=1, is_ascend=False, dtype="int32")
    sorted_scores = gather(scores, 1, sorted_indices)

    if not isinstance(score_threshold, te.Tensor):
        score_threshold_tensor = te.compute((), lambda: score_threshold, name="score_threshold")
    else:
        score_threshold_tensor = score_threshold

    valid_count = _get_valid_box_count(sorted_scores, score_threshold_tensor)

    selected_indices, selected_scores, num_detections = run_all_class_nms(
        boxes,
        sorted_scores,
        sorted_indices,
        valid_count,
        max_output_boxes_per_class,
        iou_threshold,
        _nms_loop,
        return_scores=(output_format == "tensorflow"),
        score_threshold=score_threshold_tensor,  # Passed score_threshold as tensor
    )

    if output_format == "onnx":
        row_offsets = cumsum(num_detections, exclusive=True, dtype="int64")

        def _sum_clamped_total():
            if isinstance(max_output_boxes_per_class, int):
                k_expr = tvm.tirx.IntImm("int32", int(max_output_boxes_per_class))
                clamped = te.compute(
                    num_detections.shape,
                    lambda i: tvm.tirx.min(num_detections[i], k_expr),
                    name="clamped_num",
                )
                return reduction.sum(cast(clamped, "int64"), axis=0)
            if isinstance(max_output_boxes_per_class, tvm.tirx.IntImm):
                k_expr = tvm.tirx.Cast("int32", max_output_boxes_per_class)
                clamped = te.compute(
                    num_detections.shape,
                    lambda i: tvm.tirx.min(num_detections[i], k_expr),
                    name="clamped_num",
                )
                return reduction.sum(cast(clamped, "int64"), axis=0)
            if isinstance(max_output_boxes_per_class, te.Tensor):
                if len(max_output_boxes_per_class.shape) == 0:
                    kb = te.compute(
                        num_detections.shape,
                        lambda i: cast(max_output_boxes_per_class, "int32"),
                        name="k_broadcast",
                    )
                elif (
                    len(max_output_boxes_per_class.shape) == 1
                    and max_output_boxes_per_class.shape[0] == 1
                ):
                    kb = te.compute(
                        num_detections.shape,
                        lambda i: cast(max_output_boxes_per_class[0], "int32"),
                        name="k_broadcast",
                    )
                else:
                    return reduction.sum(cast(num_detections, "int64"), axis=0)

                clamped = te.compute(
                    num_detections.shape,
                    lambda i: tvm.tirx.min(num_detections[i], kb[i]),
                    name="clamped_num",
                )
                return reduction.sum(cast(clamped, "int64"), axis=0)
            return reduction.sum(cast(num_detections, "int64"), axis=0)

        num_total_scalar = _sum_clamped_total()
        num_total_detections = reshape(num_total_scalar, (1,))

        if output_shape is not None:
            selected_indices = collect_selected_indices(
                num_class,
                selected_indices,
                num_detections,
                row_offsets,
                _collect_selected_indices_ir,
                max_output_boxes_per_class=max_output_boxes_per_class,
                output_shape=output_shape,
            )
        else:
            # Use num_total_detections to enable dynamic trimming
            # Pass image size for intelligent default estimation
            input_image_size = None
            if hasattr(scores, "shape") and len(scores.shape) >= 3:
                # Extract image size from scores shape: (batch, num_classes, num_boxes)
                # We can estimate image size from num_boxes (more boxes = larger image)
                input_image_size = (scores.shape[2],)  # Use num_boxes as proxy for image size

                # TODO: Improve image size estimation by:
                # 1. Accepting actual image dimensions as parameters
                # 2. Using model metadata to infer typical image sizes
                # 3. Learning from historical detection patterns
                # 4. Providing user-configurable estimation strategies

            selected_indices = collect_selected_indices(
                num_class,
                selected_indices,
                num_detections,
                row_offsets,
                _collect_selected_indices_ir,
                max_output_boxes_per_class=max_output_boxes_per_class,
                num_total_detections=num_total_detections,
                input_image_size=input_image_size,
            )
        return [selected_indices, num_total_detections]

    num_detections_per_batch = reshape(num_detections, (batch, num_class))
    row_offsets = cumsum(num_detections_per_batch, exclusive=True, dtype="int64", axis=1)
    num_total_detections = reduction.sum(cast(num_detections_per_batch, "int64"), axis=1)

    selected_indices, selected_scores = collect_selected_indices_and_scores(
        selected_indices,
        selected_scores,
        num_detections_per_batch,
        row_offsets,
        num_total_detections,
        _collect_selected_indices_and_scores_ir,
    )

    return [selected_indices, selected_scores, num_total_detections]
