# 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=invalid-name,unused-argument
"""Default legalization function for neural network operators."""

import logging
import math

from tvm import s_tir, te, tirx, topi
from tvm.ir import Call

from ...block_builder import BlockBuilder
from ...expr import Expr
from .common import _call_topi_without_attr, register_legalize


@register_legalize("relax.nn.conv1d")
def _nn_conv1d(bb: BlockBuilder, call: Call) -> Expr:
    if call.attrs.out_layout != call.attrs.data_layout:
        logging.info(
            "TOPI conv1d does not support different input-output "
            "layouts, and thus cannot be legalized by TOPI"
        )
        return call
    if len(call.attrs.data_layout) != 3 or len(call.attrs.kernel_layout) != 3:
        logging.info(
            "Conv1D where data layout or kernel layout have channel chunk "
            "cannot be legalized by TOPI at this moment."
        )
        return call
    if call.attrs.groups != 1:
        data_layout = s_tir.slayout(call.attrs.data_layout)
        kernel_layout = s_tir.slayout(call.attrs.kernel_layout)
        ic = call.args[0].ty.shape.values[data_layout.index_of("C")]
        oc = call.args[1].ty.shape.values[kernel_layout.index_of("O")]
        if not isinstance(ic, tirx.IntImm) or not isinstance(oc, tirx.IntImm):
            logging.info(
                "Conv1D where number of groups is more than one and input or output "
                "channel size is symbolic cannot be legalized by TOPI at this moment."
            )
            return call

    return bb.call_te(
        topi.nn.conv1d,
        data=call.args[0],
        kernel=call.args[1],
        strides=call.attrs.strides,
        padding=call.attrs.padding,
        dilation=call.attrs.dilation,
        groups=call.attrs.groups,
        data_layout=call.attrs.data_layout,
        kernel_layout=call.attrs.kernel_layout,
        out_dtype=call.attrs.out_dtype if call.attrs.out_dtype != "" else None,
        primfunc_name_hint="conv1d",
    )


@register_legalize("relax.nn.conv2d")
def _nn_conv2d(bb: BlockBuilder, call: Call) -> Expr:
    if call.attrs.out_layout != call.attrs.data_layout:
        logging.info(
            "TOPI conv2d does not support different input-output "
            "layouts, and thus cannot be legalized by TOPI"
        )
        return call
    if len(call.attrs.data_layout) != 4 or len(call.attrs.kernel_layout) != 4:
        logging.info(
            "Conv2D where data layout or kernel layout have channel chunk "
            "cannot be legalized by TOPI at this moment."
        )
        return call
    if call.attrs.groups != 1:
        data_layout = s_tir.slayout(call.attrs.data_layout)
        kernel_layout = s_tir.slayout(call.attrs.kernel_layout)
        ic = call.args[0].ty.shape.values[data_layout.index_of("C")]
        oc = call.args[1].ty.shape.values[kernel_layout.index_of("O")]
        if not isinstance(ic, tirx.IntImm) or not isinstance(oc, tirx.IntImm):
            logging.info(
                "Conv2D where number of groups is more than one and input or output "
                "channel size is symbolic cannot be legalized by TOPI at this moment."
            )
            return call

    return bb.call_te(
        topi.nn.conv,
        inp=call.args[0],
        filt=call.args[1],
        stride=call.attrs.strides,
        padding=call.attrs.padding,
        dilation=call.attrs.dilation,
        groups=call.attrs.groups,
        data_layout=call.attrs.data_layout,
        kernel_layout=call.attrs.kernel_layout,
        out_dtype=call.attrs.out_dtype if call.attrs.out_dtype != "" else None,
        primfunc_name_hint="conv2d",
    )


@register_legalize("relax.nn.conv3d")
def _nn_conv3d(bb: BlockBuilder, call: Call) -> Expr:
    if call.attrs.out_layout != call.attrs.data_layout:
        logging.info(
            "TOPI conv3d does not support different input-output "
            "layouts, and thus cannot be legalized by TOPI"
        )
        return call
    if len(call.attrs.data_layout) != 5 or len(call.attrs.kernel_layout) != 5:
        logging.info(
            "Conv3D where data layout or kernel layout have channel chunk "
            "cannot be legalized by TOPI at this moment."
        )
        return call
    if call.attrs.groups != 1:
        data_layout = s_tir.slayout(call.attrs.data_layout)
        kernel_layout = s_tir.slayout(call.attrs.kernel_layout)
        ic = call.args[0].ty.shape.values[data_layout.index_of("C")]
        oc = call.args[1].ty.shape.values[kernel_layout.index_of("O")]
        if not isinstance(ic, tirx.IntImm) or not isinstance(oc, tirx.IntImm):
            logging.info(
                "Conv3D where number of groups is more than one and input or output "
                "channel size is symbolic cannot be legalized by TOPI at this moment."
            )
            return call

    return bb.call_te(
        topi.nn.conv,
        inp=call.args[0],
        filt=call.args[1],
        stride=call.attrs.strides,
        padding=call.attrs.padding,
        dilation=call.attrs.dilation,
        groups=call.attrs.groups,
        data_layout=call.attrs.data_layout,
        kernel_layout=call.attrs.kernel_layout,
        out_dtype=call.attrs.out_dtype if call.attrs.out_dtype != "" else None,
        primfunc_name_hint="conv3d",
    )


@register_legalize("relax.nn.conv1d_transpose")
def _nn_conv1d_transpose(bb: BlockBuilder, call: Call) -> Expr:
    if call.attrs.out_layout != call.attrs.data_layout:
        logging.info(
            "TOPI conv1d_transpose does not support different input-output "
            "layouts, and thus cannot be legalized by TOPI"
        )
        return call
    if call.attrs.data_layout != "NCW" or call.attrs.kernel_layout != "IOW":
        logging.info(
            "TOPI conv1d_transpose does not support input layout other than NCW, "
            "and kernel layout other than IOW, so cannot be legalized by TOPI"
        )
        return call
    strides = [int(s) for s in call.attrs.strides]
    padding = [int(p) for p in call.attrs.padding]
    output_padding = [int(o) for o in call.attrs.output_padding]
    groups = int(call.attrs.groups)
    out_dtype = call.ty.dtype
    dilation = [int(d) for d in call.attrs.dilation]

    def te_conv1d_transpose(data, kernel):
        # Dilated transposed conv == transposed conv with a spatially dilated (zero-filled) kernel.
        if any(d != 1 for d in dilation):
            kernel = topi.nn.dilate(kernel, [1, 1, dilation[0]], name="kernel_dilate")
        return topi.nn.group_conv1d_transpose_ncw(
            data, kernel, strides, padding, out_dtype, output_padding, groups
        )

    return bb.call_te(
        te_conv1d_transpose, call.args[0], call.args[1], primfunc_name_hint="conv1d_transpose"
    )


@register_legalize("relax.nn.conv2d_transpose")
def _nn_conv2d_transpose(bb: BlockBuilder, call: Call) -> Expr:
    if call.attrs.out_layout != call.attrs.data_layout:
        logging.info(
            "TOPI conv2d_transpose does not support different input-output "
            "layouts, and thus cannot be legalized by TOPI"
        )
        return call
    if call.attrs.data_layout != "NCHW" or call.attrs.kernel_layout != "IOHW":
        logging.info(
            "TOPI conv2d_transpose does not support input layout other than NCHW, "
            "and kernel layout other than IOHW, so cannot be legalized by TOPI"
        )
        return call
    strides = [int(s) for s in call.attrs.strides]
    padding = [int(p) for p in call.attrs.padding]
    output_padding = [int(o) for o in call.attrs.output_padding]
    groups = int(call.attrs.groups)
    out_dtype = call.ty.dtype
    dilation = [int(d) for d in call.attrs.dilation]

    def te_conv2d_transpose(data, kernel):
        # Dilated transposed conv == transposed conv with a spatially dilated (zero-filled) kernel.
        if any(d != 1 for d in dilation):
            kernel = topi.nn.dilate(kernel, [1, 1, dilation[0], dilation[1]], name="kernel_dilate")
        return topi.nn.group_conv2d_transpose_nchw(
            data, kernel, strides, padding, out_dtype, output_padding, groups
        )

    return bb.call_te(
        te_conv2d_transpose, call.args[0], call.args[1], primfunc_name_hint="conv2d_transpose"
    )


@register_legalize("relax.nn.conv3d_transpose")
def _nn_conv3d_transpose(bb: BlockBuilder, call: Call) -> Expr:
    # Keep policy in sync with _nn_conv2d_transpose: only lower when TOPI supports
    # the layout/dilation.
    if call.attrs.out_layout != call.attrs.data_layout:
        logging.info(
            "TOPI conv3d_transpose does not support different input-output "
            "layouts, and thus cannot be legalized by TOPI"
        )
        return call
    if call.attrs.data_layout != "NCDHW" or call.attrs.kernel_layout != "IODHW":
        logging.info(
            "TOPI conv3d_transpose does not support input layout other than NCDHW, "
            "and kernel layout other than IODHW, so cannot be legalized by TOPI"
        )
        return call
    strides = [int(s) for s in call.attrs.strides]
    padding = [int(p) for p in call.attrs.padding]
    output_padding = [int(o) for o in call.attrs.output_padding]
    groups = int(call.attrs.groups)
    out_dtype = call.ty.dtype
    dilation = [int(d) for d in call.attrs.dilation]

    def te_conv3d_transpose(data, kernel):
        # Dilated transposed conv == transposed conv with a spatially dilated (zero-filled) kernel.
        if any(d != 1 for d in dilation):
            kernel = topi.nn.dilate(
                kernel, [1, 1, dilation[0], dilation[1], dilation[2]], name="kernel_dilate"
            )
        return topi.nn.group_conv3d_transpose_ncdhw(
            data, kernel, strides, padding, out_dtype, output_padding, groups
        )

    return bb.call_te(
        te_conv3d_transpose, call.args[0], call.args[1], primfunc_name_hint="conv3d_transpose"
    )


@register_legalize("relax.nn.pad")
def _nn_pad(bb: BlockBuilder, call: Call) -> Expr:
    pad_mode = call.attrs.pad_mode
    pad_widths = call.attrs.pad_width
    pad_before = pad_widths[::2]
    pad_after = pad_widths[1::2]
    if pad_mode == "reflect":
        return bb.call_te(
            topi.nn.reflect_pad, call.args[0], pad_before=pad_before, pad_after=pad_after
        )
    elif pad_mode == "replicate":
        return bb.call_te(
            topi.nn.replicate_pad, call.args[0], pad_before=pad_before, pad_after=pad_after
        )
    elif pad_mode == "circular":
        return bb.call_te(
            topi.nn.circular_pad, call.args[0], pad_before=pad_before, pad_after=pad_after
        )
    else:
        return bb.call_te(
            topi.nn.pad,
            call.args[0],
            pad_before=pad_before,
            pad_after=pad_after,
            pad_value=call.attrs.pad_value,
            primfunc_name_hint="pad",
        )


@register_legalize("relax.nn.pixel_shuffle")
def _nn_pixel_shuffle(bb: BlockBuilder, call: Call) -> Expr:
    upscale_factor = call.attrs.upscale_factor
    return bb.call_te(topi.nn.pixel_shuffle, call.args[0], upscale_factor=upscale_factor)


@register_legalize("relax.nn.max_pool1d")
def _nn_max_pool1d(bb: BlockBuilder, call: Call) -> Expr:
    if call.attrs.out_layout != call.attrs.layout:
        logging.info(
            "TOPI max_pool1d does not support different input-output "
            "layouts, and thus cannot be legalized by TOPI"
        )
        return call

    return bb.call_te(
        topi.nn.pool1d,
        call.args[0],
        kernel=call.attrs.pool_size,
        stride=call.attrs.strides,
        dilation=call.attrs.dilation,
        padding=call.attrs.padding,
        pool_type="max",
        ceil_mode=call.attrs.ceil_mode,
        layout=call.attrs.layout,
        primfunc_name_hint="max_pool1d",
    )


@register_legalize("relax.nn.max_pool2d")
def _nn_max_pool2d(bb: BlockBuilder, call: Call) -> Expr:
    if call.attrs.out_layout != call.attrs.layout:
        logging.info(
            "TOPI max_pool2d does not support different input-output "
            "layouts, and thus cannot be legalized by TOPI"
        )
        return call

    return bb.call_te(
        topi.nn.pool2d,
        call.args[0],
        kernel=call.attrs.pool_size,
        stride=call.attrs.strides,
        dilation=call.attrs.dilation,
        padding=call.attrs.padding,
        pool_type="max",
        ceil_mode=call.attrs.ceil_mode,
        layout=call.attrs.layout,
        primfunc_name_hint="max_pool2d",
    )


@register_legalize("relax.nn.max_pool3d")
def _nn_max_pool3d(bb: BlockBuilder, call: Call) -> Expr:
    if call.attrs.out_layout != call.attrs.layout:
        logging.info(
            "TOPI max_pool3d does not support different input-output "
            "layouts, and thus cannot be legalized by TOPI"
        )
        return call

    return bb.call_te(
        topi.nn.pool3d,
        call.args[0],
        kernel=call.attrs.pool_size,
        stride=call.attrs.strides,
        dilation=call.attrs.dilation,
        padding=call.attrs.padding,
        pool_type="max",
        ceil_mode=call.attrs.ceil_mode,
        layout=call.attrs.layout,
        primfunc_name_hint="max_pool3d",
    )


@register_legalize("relax.nn.avg_pool1d")
def _nn_avg_pool1d(bb: BlockBuilder, call: Call) -> Expr:
    if call.attrs.out_layout != call.attrs.layout:
        logging.info(
            "TOPI avg_pool1d does not support different input-output "
            "layouts, and thus cannot be legalized by TOPI"
        )
        return call

    return bb.call_te(
        topi.nn.pool1d,
        call.args[0],
        kernel=call.attrs.pool_size,
        stride=call.attrs.strides,
        dilation=call.attrs.dilation,
        padding=call.attrs.padding,
        pool_type="avg",
        ceil_mode=call.attrs.ceil_mode,
        layout=call.attrs.layout,
        count_include_pad=call.attrs.count_include_pad,
        primfunc_name_hint="avg_pool1d",
    )


@register_legalize("relax.nn.avg_pool2d")
def _nn_avg_pool2d(bb: BlockBuilder, call: Call) -> Expr:
    if call.attrs.out_layout != call.attrs.layout:
        logging.info(
            "TOPI avg_pool2d does not support different input-output "
            "layouts, and thus cannot be legalized by TOPI"
        )
        return call

    return bb.call_te(
        topi.nn.pool2d,
        call.args[0],
        kernel=call.attrs.pool_size,
        stride=call.attrs.strides,
        dilation=call.attrs.dilation,
        padding=call.attrs.padding,
        pool_type="avg",
        ceil_mode=call.attrs.ceil_mode,
        layout=call.attrs.layout,
        count_include_pad=call.attrs.count_include_pad,
        primfunc_name_hint="avg_pool2d",
    )


@register_legalize("relax.nn.avg_pool3d")
def _nn_avg_pool3d(bb: BlockBuilder, call: Call) -> Expr:
    if call.attrs.out_layout != call.attrs.layout:
        logging.info(
            "TOPI avg_pool3d does not support different input-output "
            "layouts, and thus cannot be legalized by TOPI"
        )
        return call

    return bb.call_te(
        topi.nn.pool3d,
        call.args[0],
        kernel=call.attrs.pool_size,
        stride=call.attrs.strides,
        dilation=call.attrs.dilation,
        padding=call.attrs.padding,
        pool_type="avg",
        ceil_mode=call.attrs.ceil_mode,
        layout=call.attrs.layout,
        count_include_pad=call.attrs.count_include_pad,
        primfunc_name_hint="avg_pool3d",
    )


@register_legalize("relax.nn.adaptive_avg_pool1d")
def _nn_adaptive_avg_pool1d(bb: BlockBuilder, call: Call) -> Expr:
    if call.attrs.out_layout != call.attrs.layout:
        logging.info(
            "TOPI adaptive_avg_pool1d does not support different input-output "
            "layouts, and thus cannot be legalized by TOPI"
        )
        return call

    def te_adaptive_avg_pool1d(data, output_size, layout_str):
        if output_size is None:
            layout = s_tir.slayout(layout_str)
            idx_W = layout.index_of("W")
            assert idx_W != -1
            output_size = data.shape[idx_W]

        return topi.nn.adaptive_pool1d(data, output_size, "avg", layout_str)

    return bb.call_te(
        te_adaptive_avg_pool1d,
        call.args[0],
        call.attrs.output_size,
        call.attrs.layout,
        primfunc_name_hint="adaptive_avg_pool1d",
    )


@register_legalize("relax.nn.adaptive_avg_pool2d")
def _nn_adaptive_avg_pool2d(bb: BlockBuilder, call: Call) -> Expr:
    if call.attrs.out_layout != call.attrs.layout:
        logging.info(
            "TOPI adaptive_avg_pool2d does not support different input-output "
            "layouts, and thus cannot be legalized by TOPI"
        )
        return call

    def te_adaptive_avg_pool2d(data, output_size, layout_str):
        if output_size is None:
            layout = s_tir.slayout(layout_str)
            idx_H = layout.index_of("H")
            idx_W = layout.index_of("W")
            assert idx_H != -1 and idx_W != -1
            output_size = (data.shape[idx_H], data.shape[idx_W])

        return topi.nn.adaptive_pool(data, output_size, "avg", layout_str)

    return bb.call_te(
        te_adaptive_avg_pool2d,
        call.args[0],
        call.attrs.output_size,
        call.attrs.layout,
        primfunc_name_hint="adaptive_avg_pool2d",
    )


@register_legalize("relax.nn.adaptive_avg_pool3d")
def _nn_adaptive_avg_pool3d(bb: BlockBuilder, call: Call) -> Expr:
    if call.attrs.out_layout != call.attrs.layout:
        logging.info(
            "TOPI adaptive_avg_pool3d does not support different input-output "
            "layouts, and thus cannot be legalized by TOPI"
        )
        return call

    def te_adaptive_avg_pool3d(data, output_size, layout_str):
        if output_size is None:
            layout = s_tir.slayout(layout_str)
            idx_D = layout.index_of("D")
            idx_H = layout.index_of("H")
            idx_W = layout.index_of("W")
            assert idx_D != -1 and idx_H != -1 and idx_W != -1
            output_size = (data.shape[idx_D], data.shape[idx_H], data.shape[idx_W])

        return topi.nn.adaptive_pool3d(data, output_size, "avg", layout_str)

    return bb.call_te(
        te_adaptive_avg_pool3d,
        call.args[0],
        call.attrs.output_size,
        call.attrs.layout,
        primfunc_name_hint="adaptive_avg_pool3d",
    )


register_legalize("relax.nn.relu", _call_topi_without_attr(topi.nn.relu))


@register_legalize("relax.nn.leakyrelu")
def _nn_leakyrelu(bb: BlockBuilder, call: Call) -> Expr:
    return bb.call_te(topi.nn.leaky_relu, call.args[0], call.attrs.alpha)


@register_legalize("relax.nn.prelu")
def _nn_prelu(bb: BlockBuilder, call: Call) -> Expr:
    return bb.call_te(topi.nn.prelu, call.args[0], call.args[1], call.attrs.axis)


@register_legalize("relax.nn.gelu")
def _nn_gelu(bb: BlockBuilder, call: Call) -> Expr:
    def te_gelu(x: te.Tensor):
        dtype = x.dtype
        erf_inp = x * tirx.const(0.5**0.5, dtype)

        if dtype == "float16":
            erf = topi.math.cast(topi.erf(topi.math.cast(erf_inp, "float32")), "float16")
        else:
            erf = topi.erf(erf_inp)

        return x * (tirx.const(0.5, dtype) + erf * tirx.const(0.5, dtype))

    return bb.call_te(te_gelu, call.args[0], primfunc_name_hint="gelu")


@register_legalize("relax.nn.gelu_tanh")
def _nn_gelu_tanh(bb: BlockBuilder, call: Call) -> Expr:
    def te_gelu_tanh(x: te.Tensor):
        dtype = x.dtype
        return (
            tirx.const(0.5, dtype)
            * x
            * (
                tirx.const(1.0, dtype)
                + topi.tanh(
                    tirx.const(math.sqrt(2.0 / math.pi), dtype)
                    * x
                    * (1 + tirx.const(0.044715, dtype) * x * x)
                )
            )
        )

    return bb.call_te(te_gelu_tanh, call.args[0], primfunc_name_hint="gelu_tanh")


@register_legalize("relax.nn.selu")
def _nn_selu(bb: BlockBuilder, call: Call) -> Expr:
    def te_selu(x: te.Tensor):
        dtype = x.dtype
        alpha = tirx.const(1.6732632423543772848170429916717, dtype)
        scale = tirx.const(1.0507009873554804934193349852946, dtype)

        # Compute SELU
        # SELU(x) = scale*(max(0,x)+min(0,a*(exp(x)-1)))
        positive_part = topi.maximum(x, tirx.const(0, dtype))
        negative_part = topi.minimum(
            tirx.const(0, dtype), alpha * (topi.exp(x) - tirx.const(1, dtype))
        )
        return scale * (positive_part + negative_part)

    return bb.call_te(te_selu, call.args[0], primfunc_name_hint="selu")


@register_legalize("relax.nn.silu")
def _nn_silu(bb: BlockBuilder, call: Call) -> Expr:
    def te_silu(x: te.Tensor):
        return topi.multiply(x, topi.sigmoid(x))

    return bb.call_te(te_silu, call.args[0], primfunc_name_hint="silu")


@register_legalize("relax.nn.softplus")
def _nn_softplus(bb: BlockBuilder, call: Call) -> Expr:
    return bb.call_te(
        topi.nn.softplus,
        call.args[0],
        call.attrs.beta,
        call.attrs.threshold,
    )


@register_legalize("relax.nn.softmax")
def _nn_softmax(bb: BlockBuilder, call: Call) -> Expr:
    return bb.call_te(topi.nn.softmax, call.args[0], call.attrs.axis)


@register_legalize("relax.nn.log_softmax")
def _nn_log_softmax(bb: BlockBuilder, call: Call):
    return bb.call_te(topi.nn.log_softmax, call.args[0], call.attrs.axis)


@register_legalize("relax.nn.cross_entropy_with_logits")
def _nn_cross_entropy_with_logits(bb: BlockBuilder, call: Call):
    def te_cross_entropy_with_logits(x, y):
        if len(x.shape) > 1:
            return -topi.sum(x * y) / x.shape[0]
        return -topi.sum(x * y)

    return bb.call_te(
        te_cross_entropy_with_logits,
        call.args[0],
        call.args[1],
        primfunc_name_hint="cross_entropy_with_logits",
    )


@register_legalize("relax.nn.batch_norm")
def _nn_batch_norm(bb: BlockBuilder, call: Call) -> Expr:
    return bb.call_te(
        topi.nn.batch_norm,
        data=call.args[0],
        gamma=call.args[1],
        beta=call.args[2],
        moving_mean=call.args[3],
        moving_var=call.args[4],
        axis=call.attrs.axis,
        epsilon=call.attrs.epsilon,
        center=call.attrs.center,
        scale=call.attrs.scale,
        training=call.attrs.training,
        momentum=call.attrs.momentum,
    )


@register_legalize("relax.nn.layer_norm")
def _nn_layer_norm(bb: BlockBuilder, call: Call) -> Expr:
    return bb.call_te(
        topi.nn.layer_norm,
        call.args[0],
        call.args[1],
        call.args[2],
        axis=call.attrs.axes,
        epsilon=call.attrs.epsilon,
    )


@register_legalize("relax.nn.group_norm")
def _nn_group_norm(bb: BlockBuilder, call: Call) -> Expr:
    return bb.call_te(
        topi.nn.group_norm,
        call.args[0],
        call.args[1],
        call.args[2],
        call.attrs.num_groups,
        call.attrs.channel_axis,
        call.attrs.axes,
        call.attrs.epsilon,
    )


@register_legalize("relax.nn.instance_norm")
def _nn_instance_norm(bb: BlockBuilder, call: Call) -> Expr:
    return bb.call_te(
        topi.nn.instance_norm,
        data=call.args[0],
        gamma=call.args[1],
        beta=call.args[2],
        channel_axis=call.attrs.channel_axis,
        axis=call.attrs.axes,
        epsilon=call.attrs.epsilon,
    )


@register_legalize("relax.nn.rms_norm")
def _nn_rms_norm(bb: BlockBuilder, call: Call) -> Expr:
    return bb.call_te(
        topi.nn.rms_norm,
        call.args[0],
        call.args[1],
        axis=call.attrs.axes,
        epsilon=call.attrs.epsilon,
    )


@register_legalize("relax.nn.dropout")
def _nn_dropout(bb: BlockBuilder, call: Call) -> Expr:
    # Dropout is a no-op at inference: pass the input through and return an all-ones mask.
    return bb.call_te(
        lambda x: [topi.identity(x), topi.full_like(x, 1.0)],
        call.args[0],
        primfunc_name_hint="dropout",
    )


def _te_attention(
    q: te.Tensor,
    k: te.Tensor,
    v: te.Tensor,
    bias: te.Tensor,
    scale: tirx.FloatImm,
    causal_mask: str | None,
) -> te.Tensor:
    batch_size, seq_len, num_head, head_dim = q.shape
    _, seq_len_kv, _, head_dim_v = v.shape
    q = topi.transpose(q, [0, 2, 1, 3])
    k = topi.transpose(k, [0, 2, 1, 3])
    v = topi.transpose(v, [0, 2, 1, 3])
    bs = batch_size * num_head
    q = topi.reshape(q, [bs, seq_len, head_dim])
    k = topi.reshape(k, [bs, seq_len_kv, head_dim])
    v = topi.reshape(v, [bs, seq_len_kv, head_dim_v])
    p = topi.nn.batch_matmul(q, k, oshape=[bs, seq_len, seq_len_kv])
    if scale is not None:
        p = topi.multiply(p, scale)
    else:
        p = topi.divide(p, tirx.sqrt(tirx.Cast(p.dtype, head_dim)))
    if bias is not None:
        p = topi.reshape(p, [batch_size, num_head, seq_len, seq_len_kv])
        p = topi.add(p, bias)
        p = topi.reshape(p, [bs, seq_len, seq_len_kv])
    if causal_mask is None:
        s = topi.nn.softmax(p)
    else:
        if causal_mask == "TopLeft":
            offset = tirx.IntImm("int32", 0)
        elif causal_mask == "BottomRight":
            offset = tirx.abs(seq_len - seq_len_kv).astype("int32")
        else:
            raise NotImplementedError()
        p_masked = topi.trilu(p, k=offset, upper=False)
        p_masked_exp = topi.trilu(
            topi.exp(p_masked - topi.max(p_masked, axis=-1, keepdims=True)), k=offset, upper=False
        )
        p_masked_sum = topi.sum(p_masked_exp, axis=-1, keepdims=True)
        s = topi.divide(p_masked_exp, p_masked_sum)
    o = topi.nn.batch_matmul(s, v, transpose_b=False, oshape=[bs, seq_len, head_dim_v])
    o = topi.reshape(o, [batch_size, num_head, seq_len, head_dim_v])
    return topi.transpose(o, [0, 2, 1, 3])


@register_legalize("relax.nn.attention")
def _nn_attention(bb: BlockBuilder, call: Call) -> Expr:
    assert call.attrs.window_size is None, (
        "Legalization for sliding-window attention is not supported yet."
    )
    return bb.call_te(
        _te_attention,
        call.args[0],
        call.args[1],
        call.args[2],
        None,
        call.attrs.scale,
        call.attrs.causal_mask,
        primfunc_name_hint="attention",
    )


@register_legalize("relax.nn.attention_bias")
def _nn_attention_bias(bb: BlockBuilder, call: Call) -> Expr:
    assert call.attrs.window_size is None, (
        "Legalization for sliding-window attention is not supported yet."
    )
    return bb.call_te(
        _te_attention,
        call.args[0],
        call.args[1],
        call.args[2],
        call.args[3],
        call.attrs.scale,
        call.attrs.causal_mask,
        primfunc_name_hint="attention_bias",
    )


@register_legalize("relax.nn.attention_var_len")
def _nn_attention_var_len(bb: BlockBuilder, call: Call) -> Expr:
    raise RuntimeError("Legalization of attention_var_len op is not supported yet.")


@register_legalize("relax.nn.nll_loss")
def _nn_nll_loss(bb: BlockBuilder, call: Call) -> Expr:
    def nll_loss_without_weight(predictions, targets, reduction, ignore_index):
        weight = topi.full(
            (predictions.shape[1] if len(predictions.shape) > 1 else predictions.shape[0],),
            predictions.dtype,
            1.0,
        )
        return topi.nn.nll_loss(predictions, targets, weight, reduction, ignore_index)

    if len(call.args) == 2:
        return bb.call_te(
            nll_loss_without_weight,
            call.args[0],
            call.args[1],
            reduction=call.attrs.reduction,
            ignore_index=call.attrs.ignore_index,
        )

    return bb.call_te(
        topi.nn.nll_loss,
        call.args[0],
        call.args[1],
        call.args[2],
        reduction=call.attrs.reduction,
        ignore_index=call.attrs.ignore_index,
    )


@register_legalize("relax.nn.batch_flatten")
def _nn_batch_flatten(bb: BlockBuilder, call: Call) -> Expr:
    if call.ty.shape is None:
        return call
    return bb.call_te(topi.reshape, call.args[0], call.ty.shape.values)
