# 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
"""Common patterns used in BYOC"""

from collections.abc import Mapping

from tvm.relax.dpl.pattern import (
    DFPattern,
    GlobalVarPattern,
    TuplePattern,
    is_const,
    is_op,
    is_tuple_get_item,
    wildcard,
)
from tvm.script import relax as R
from tvm.script import tirx as T


def _with_bias_activation_pattern(
    out: DFPattern,
    annotations: dict[str, DFPattern],
    with_bias: bool = False,
    activation: str | None = None,
    allow_reshape: bool = False,
) -> tuple[DFPattern, Mapping[str, DFPattern]]:
    if with_bias:
        annotations["bias"] = bias = wildcard()
        if allow_reshape:
            reshaped_bias = is_op("relax.reshape")(bias, wildcard(), varg_default_wildcard=True)
            out = is_op("relax.add")(out, reshaped_bias, varg_default_wildcard=True)
        else:
            out = is_op("relax.add")(out, bias)

    if activation:
        out = is_op(activation)(out)

    return out, annotations


def make_fused_bias_activation_pattern(
    op_name: str,
    with_bias: bool = False,
    activation: str | None = None,
    allow_reshape: bool = False,
) -> tuple[DFPattern, Mapping[str, DFPattern]]:
    """
    A simple utility to create patterns for an operation fused with bias addition and activation.

    Parameters
    ----------
    op_name: str
        The name of a Relax op, such as "relax.nn.conv2d"

    with_bias: bool
        Whether or not to include bias addition

    activation: str
        The name of an activation Relax op, such as "relax.nn.relu"

    Returns
    -------
    pattern: DFPattern
        The resulting pattern describing a fused operation

    annotations: Mapping[str, DFPattern]
        A mapping from name to sub pattern. It can be used to extract
        important expressions from match result, to power the partition
        check function and codegen.
    """
    lhs = wildcard()
    rhs = wildcard()
    out = is_op(op_name)(lhs, rhs)
    annotations = {"lhs": lhs, "rhs": rhs, "root": out}

    return _with_bias_activation_pattern(out, annotations, with_bias, activation, allow_reshape)


def make_residual_block_pattern(
    node_output: DFPattern | tuple[DFPattern, Mapping[str, DFPattern]],
    binary_op="relax.add",
    activation=None,
) -> tuple[DFPattern, Mapping[str, DFPattern]]:
    """
    Create pattern for residual block.

    Parameters
    ----------
    node_output: Union[DFPattern, Tuple[DFPattern, Mapping[str, DFPattern]]]
        The output of previous node.

    binary_op: str
        The op used to combine previous node output and residual input.

    activation: str
        The activation function of this residual block. It should be a name of
        activation Relax op, such as "relax.nn.relu".

    Returns
    -------
    pattern: DFPattern
        The resulting pattern describing a matrix multiplication.

    annotations: Mapping[str, DFPattern]
        A mapping from name to sub pattern. It can be used to extract
        important expressions from match result, to power the partition
        check function and codegen.
    """

    if isinstance(node_output, tuple):
        node_output, arg_patterns = node_output
    else:
        arg_patterns = {}

    residual_input = wildcard()
    op = is_op(binary_op)
    output = op(node_output, residual_input) | op(residual_input, node_output)

    if activation is not None:
        output = is_op(activation)(output)

    return output, {**arg_patterns, "residual": residual_input}


def make_conv2d_pattern(
    with_bias: bool = False,
    activation: str | None = None,
) -> tuple[DFPattern, Mapping[str, DFPattern]]:
    """
    Create pattern for 2D convolution.

    Parameters
    ----------
    with_bias: bool
        Whether or not to include bias addition

    activation: str
        The name of an activation Relax op, such as "relax.nn.relu"

    Returns
    -------
    pattern: DFPattern
        The resulting pattern describing a 2D convolution.

    annotations: Mapping[str, DFPattern]
        A mapping from name to sub pattern. It can be used to extract
        important expressions from match result, to power the partition
        check function and codegen.
    """

    input_tensor = wildcard()
    kernel = wildcard()
    annotations = {"input": input_tensor, "weight": kernel}

    conv2d = is_op("relax.nn.conv2d")(input_tensor, kernel)
    annotations["root"] = conv2d

    return _with_bias_activation_pattern(conv2d, annotations, with_bias, activation)


def make_matmul_pattern(
    with_bias: bool = False,
    activation: str | None = None,
    transposed_rhs: bool = False,
) -> tuple[DFPattern, Mapping[str, DFPattern]]:
    """
    Create pattern for matrix multiplication.

    Parameters
    ----------
    with_bias: bool
        Whether or not to include bias addition

    activation: str
        The name of an activation Relax op, such as "relax.nn.relu"

    transposed_rhs: bool
        Whether the right hand side of multiplication is transposed.

    Returns
    -------
    pattern: DFPattern
        The resulting pattern describing a matrix multiplication.

    annotations: Mapping[str, DFPattern]
        A mapping from name to sub pattern. It can be used to extract
        important expressions from match result, to power the partition
        check function and codegen.
    """

    lhs = wildcard()
    rhs = wildcard()
    annotations = {"lhs": lhs, "rhs": rhs}

    if transposed_rhs:
        rhs = is_op("relax.permute_dims")(rhs)

    out = is_op("relax.matmul")(lhs, rhs)
    annotations["root"] = out

    return _with_bias_activation_pattern(out, annotations, with_bias, activation)


def make_attention_pattern(with_bias: bool = False, var_len: bool = False):
    """
    Create pattern for fused multi head attention.

    Parameters
    ----------
    with_bias: bool
        Whether or not to include bias addition.

    var_len: bool
        Whether or not to make a pattern for batched attention with variable sequence lengths.

    Returns
    -------
    pattern: DFPattern
        The resulting pattern describing a fused multi head attention.

    annotations: Mapping[str, DFPattern]
        A mapping from name to sub pattern. It can be used to extract
        important expressions from match result, to power the partition
        check function and codegen.
    """
    query = wildcard()
    key = wildcard()
    value = wildcard()
    annotations = {"query": query, "key": key, "value": value}
    if with_bias:
        bias = wildcard()
        annotations["bias"] = bias
        out = is_op("relax.nn.attention_bias")(query, key, value, bias)
    elif var_len:
        seqstart_q = wildcard()
        seqstart_k = wildcard()
        max_seqlen_q = wildcard()
        max_seqlen_k = wildcard()
        annotations.update(
            {
                "seqstart_q": seqstart_q,
                "seqstart_k": seqstart_k,
                "max_seqlen_q": max_seqlen_q,
                "max_seqlen_k": max_seqlen_k,
            }
        )
        out = is_op("relax.nn.attention_var_len")(
            query, key, value, seqstart_q, seqstart_k, max_seqlen_q, max_seqlen_k
        )
    else:
        out = is_op("relax.nn.attention")(query, key, value)

    return out, annotations


def make_stacked_attention_pattern(start_op: str, with_bias: bool = False, layout="BS3NH"):
    """
    Create pattern for fused multi head attention with stacked input.

    Parameters
    ----------
    start_op: str
        The starting op for pattern, i.e. `R.split` or `R.strided_slice`.

    with_bias: bool
        Whether or not to include bias addition

    layout: str
        The layout of the stacked input tensor.

    Returns
    -------
    pattern: DFPattern
        The resulting pattern describing a fused multi head attention.

    annotations: Mapping[str, DFPattern]
        A mapping from name to sub pattern. It can be used to extract
        important expressions from match result, to power the partition
        check function and codegen.
    """
    stacked_qkv = wildcard()
    ops = {}
    if start_op == "split":
        ops["split"] = qkv_tuple = is_op("relax.split")(stacked_qkv)
        query_raw = is_tuple_get_item(qkv_tuple, 0)
        key_raw = is_tuple_get_item(qkv_tuple, 1)
        value_raw = is_tuple_get_item(qkv_tuple, 2)
    elif start_op == "strided_slice":
        ops["strided_slice_query"] = query_raw = is_op("relax.strided_slice")(
            stacked_qkv, varg_default_wildcard=True
        )
        ops["strided_slice_key"] = key_raw = is_op("relax.strided_slice")(
            stacked_qkv, varg_default_wildcard=True
        )
        ops["strided_slice_value"] = value_raw = is_op("relax.strided_slice")(
            stacked_qkv, varg_default_wildcard=True
        )
    else:
        raise NotImplementedError()
    query_reshape_list = wildcard()
    key_reshape_list = wildcard()
    value_reshape_list = wildcard()
    if layout == "BS3NH":
        query = is_op("relax.reshape")(query_raw, query_reshape_list)
        key = is_op("relax.reshape")(key_raw, key_reshape_list)
        value = is_op("relax.reshape")(value_raw, value_reshape_list)
    elif layout == "SBN3H":
        ops["q_transpose"] = query = is_op("relax.permute_dims")(query_raw)
        ops["k_transpose"] = key = is_op("relax.permute_dims")(key_raw)
        ops["v_transpose"] = value = is_op("relax.permute_dims")(value_raw)
    annotations = {
        "stacked_qkv": stacked_qkv,
        "query_reshape_list": query_reshape_list,
        "key_reshape_list": key_reshape_list,
        "value_reshape_list": value_reshape_list,
        **ops,
    }
    if with_bias:
        bias = wildcard()
        annotations["bias"] = bias
        out = is_op("relax.nn.attention_bias")(query, key, value, bias)
    else:
        out = is_op("relax.nn.attention")(query, key, value)

    if layout == "SBN3H":
        out = is_op("relax.permute_dims")(out)

    return out, annotations


def make_layer_norm_pattern():
    """Create a layer norm pattern."""
    inp = wildcard()
    gamma = wildcard()
    beta = wildcard()

    return is_op("relax.nn.layer_norm")(inp, gamma, beta), {}


def make_rms_norm_pattern():
    """Create a layer norm pattern."""
    inp = wildcard()
    weight = wildcard()
    gv = GlobalVarPattern()
    out = is_op("relax.call_tir")(gv, TuplePattern([inp, weight]))
    annotations = {"gv": gv, "inp": inp, "rms_norm": out}
    return out, annotations


def make_matmul_dequantize_pattern(
    transposed_rhs: bool = False,
) -> tuple[DFPattern, Mapping[str, DFPattern]]:
    """
    Create pattern for matrix multiplication and dequantize operation.

    Parameters
    ----------
    transposed_rhs: bool
        Whether the right hand side of multiplication is transposed.

    Returns
    -------
    pattern: DFPattern
        The resulting pattern describing a matrix multiplication.

    annotations: Mapping[str, DFPattern]
        A mapping from name to sub pattern. It can be used to extract important expressions from
        match result, to power the partition check function and codegen.
    """

    lhs = wildcard()
    rhs = wildcard()
    annotations = {"lhs": lhs, "rhs": rhs}

    if transposed_rhs:
        rhs = is_op("relax.permute_dims")(rhs)

    out = is_op("relax.matmul")(lhs, rhs)
    annotations["root"] = out

    scale = is_const()
    zp = is_const()
    annotations.update({"scale": scale, "zp": zp})

    out = is_op("relax.dequantize")(out, scale, zp)

    return out, annotations


def make_matmul_multiply_pattern(
    transposed_rhs: bool = False,
) -> tuple[DFPattern, Mapping[str, DFPattern]]:
    """
    Create pattern for matrix multiplication and multiply operation.

    Parameters
    ----------
    transposed_rhs: bool
        Whether the right hand side of multiplication is transposed.

    Returns
    -------
    pattern: DFPattern
        The resulting pattern describing a matrix multiplication.

    annotations: Mapping[str, DFPattern]
        A mapping from name to sub pattern. It can be used to extract important expressions from
        match result, to power the partition check function and codegen.
    """

    lhs = wildcard()
    rhs = wildcard()
    scaleA = wildcard()
    scaleB = wildcard()
    annotations = {"lhs": lhs, "rhs": rhs, "scaleA": scaleA, "scaleB": scaleB}

    if transposed_rhs:
        rhs = is_op("relax.permute_dims")(rhs)
    out = is_op("relax.matmul")(lhs, rhs)
    annotations["root"] = out
    scale = is_op("relax.multiply")(scaleA.has_shape((1,)), scaleB.has_shape((1,)))
    out = is_op("relax.multiply")(out, scale)
    out = is_op("relax.astype")(out)

    return out, annotations


def make_attention_rewrite_pattern(
    qkv_layout: str, out_layout: str, with_bias: bool, with_cast: bool, with_kv_repeat: bool = False
):
    """
    Create pattern for implicit fused multi head attention rewriting.

    Parameters
    ----------
    qkv_layout: str
        The layout of the query, key and value tensor, i.e. BSNH or BSH.

    out_layout: str
        The layout of the output tensor, i.e. BSNH or BSH.

    with_bias: bool
        Whether or not to include bias addition.

    with_cast: bool
        Whether or not rewriting is intended to be applied to a module after the FP16 conversion
        pass.

    with_kv_repeat: bool
        Whether or not to include the Relax repeat op in the pattern, which is typically used
        in a Relax module to support multi-query attention.

    Returns
    -------
    pattern: DFPattern
        The resulting pattern describing an implicit fused multi head attention.

    rewriter: Callable[[Expr, Dict[DFPattern, Expr]], Expr]
        The rewriter for the pattern. It will check the matched patterns, and rewrite.
        If the matched pattern is not able to be rewritten to `R.nn.attention`, the rewriter
        returns the original IR.
    """

    # pylint: disable=invalid-name
    def handle_input(tensor, layout, transpose, repeat=False):
        if repeat:
            tensor = is_op("relax.repeat")(tensor)

        if layout == "BSNH":
            permuted = is_op("relax.permute_dims")(tensor)
            shape = wildcard()
            reshaped = is_op("relax.reshape")(permuted, shape)
            if transpose:
                transposed = is_op("relax.permute_dims")(reshaped)

            def rewriter(matchings, x):
                if matchings[tensor].ty.ndim != 4:
                    return None
                if list(matchings[permuted].attrs.axes) != [0, 2, 1, 3]:
                    return None
                before_reshape = matchings[permuted].ty.shape.values
                after_reshape = matchings[shape].ty.values
                if not (
                    len(before_reshape) == 4
                    and len(after_reshape) == 3
                    and before_reshape[-2:] == after_reshape[-2:]
                ):
                    return None
                if transpose and list(matchings[transposed].attrs.axes) != [0, 2, 1]:
                    return None
                return x, x.ty.shape

            if transpose:
                return transposed, rewriter
            else:
                return reshaped, rewriter
        elif layout == "BSH":
            if transpose:
                transposed = is_op("relax.permute_dims")(tensor)

            def rewriter(matchings, x):
                if matchings[tensor].ty.ndim != 3:
                    return None
                if transpose and list(matchings[transposed].attrs.axes) != [0, 2, 1]:
                    return None
                before_reshape = x.ty.shape.values
                after_reshape = [before_reshape[0], before_reshape[1], 1, before_reshape[2]]
                return R.reshape(x, after_reshape), after_reshape

            if transpose:
                return transposed, rewriter
            else:
                return tensor, rewriter
        else:
            raise NotImplementedError()

    def handle_output(tensor, layout):
        if layout == "BSNH":
            shape = wildcard()
            reshaped = is_op("relax.reshape")(tensor, shape)
            permuted = is_op("relax.permute_dims")(reshaped)

            def rewriter(matchings, x):
                if matchings[tensor].ty.ndim != 3:
                    return None
                before_reshape = matchings[tensor].ty.shape.values
                after_reshape = matchings[shape].ty.values
                if not (
                    len(before_reshape) == 3
                    and len(after_reshape) == 4
                    and before_reshape[-2:] == after_reshape[-2:]
                ):
                    return None
                if list(matchings[permuted].attrs.axes) != [0, 2, 1, 3]:
                    return None
                return x

            return permuted, rewriter
        elif layout == "BSH":

            def rewriter(matchings, x):
                if matchings[tensor].ty.ndim != 3:
                    return None
                return R.reshape(x, matchings[tensor].ty.shape.values)

            return tensor, rewriter
        else:
            raise NotImplementedError()

    q_raw, k_raw, v_raw = wildcard(), wildcard(), wildcard()
    q, q_rewriter = handle_input(q_raw, qkv_layout, False)
    k, k_rewriter = handle_input(k_raw, qkv_layout, True, repeat=with_kv_repeat)
    v, v_rewriter = handle_input(v_raw, qkv_layout, False, repeat=with_kv_repeat)
    matmul_1 = is_op("relax.matmul")(q, k)
    scale = is_const()

    if with_cast:
        multiply = is_op("relax.multiply")(matmul_1, is_op("relax.astype")(scale))
    else:
        multiply = is_op("relax.multiply")(matmul_1, scale)

    if with_bias:
        bias_raw = wildcard()
        add = is_op("relax.add")(multiply, bias_raw)
        softmax_input = add
    else:
        softmax_input = multiply

    if with_cast:
        softmax_input = is_op("relax.astype")(softmax_input)

    softmax = is_op("relax.nn.softmax")(softmax_input)

    if with_cast:
        softmax_output = is_op("relax.astype")(softmax)
    else:
        softmax_output = softmax

    matmul_2 = is_op("relax.matmul")(softmax_output, v)

    out, out_rewriter = handle_output(matmul_2, out_layout)

    def rewriter(original, matchings):
        query, query_shape = q_rewriter(matchings, matchings[q_raw])
        key, key_shape = k_rewriter(matchings, matchings[k_raw])
        value, _ = v_rewriter(matchings, matchings[v_raw])
        if query is None or key is None or value is None:
            return original
        softmax_axis = matchings[softmax].attrs.axis
        softmax_input_rank = len(matchings[softmax].ty.shape)
        if softmax_axis == -1:
            softmax_axis += softmax_input_rank
        if softmax_axis != softmax_input_rank - 1:
            return original
        b, s, n, _ = query_shape
        _, s_kv, _, _ = key_shape
        if with_bias:
            bias = matchings[bias_raw]
            bias_shape = list(bias.ty.shape)
            if bias_shape == [b * n, s, s_kv]:
                bias = R.reshape(bias, [b, n, s, s_kv])
            elif bias_shape == [b * n, 1, s_kv]:
                bias = R.reshape(bias, [b, n, 1, s_kv])
            elif bias_shape == [b, s, s_kv]:
                bias = R.reshape(bias, [b, 1, s, s_kv])
            elif bias_shape == [b, 1, s_kv]:
                bias = R.reshape(bias, [b, 1, 1, s_kv])
            elif bias_shape in [[1, s, s_kv], [s, s_kv]]:
                bias = R.reshape(bias, [1, 1, s, s_kv])
            elif bias_shape in [[1, 1, s_kv], [1, s_kv], [s_kv]]:
                bias = R.reshape(bias, [1, 1, 1, s_kv])
            else:
                return original
        else:
            bias = None
        out = out_rewriter(
            matchings,
            R.nn.attention(
                query,
                key,
                value,
                bias,
                T.FloatImm(matchings[scale].data.dtype, float(matchings[scale].data.numpy())),
            ),
        )
        return out

    return out, rewriter
