# 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.

"""Pattern table for NNAPI backend"""

from collections.abc import Mapping

from tvm.ir import IRModule
from tvm.relax.dpl.pattern import (
    DFPattern,
    is_op,
    wildcard,
)
from tvm.relax.transform import FuseOpsByPattern, MergeCompositeFunctions

from ..pattern_registry import get_patterns_with_prefix, register_patterns


def elementwise_binary_patterns() -> list[tuple[str, DFPattern, Mapping[str, DFPattern]]]:
    """
    Returns a list of tuples representing elementwise binary operation patterns mapped
    between NNAPI and Relax frameworks.
    """

    def _elementwise_binary_pattern(
        pattern_name: str,
        op_name: str,
    ) -> tuple[str, DFPattern, Mapping[str, DFPattern]]:
        input0 = wildcard()
        input1 = wildcard()

        pattern = is_op(op_name)(input0, input1)

        return (pattern_name, pattern, {})

    return [
        _elementwise_binary_pattern("nnapi.add", "relax.add"),
        _elementwise_binary_pattern("nnapi.mul", "relax.multiply"),
        _elementwise_binary_pattern("nnapi.div", "relax.divide"),
        _elementwise_binary_pattern("nnapi.sub", "relax.subtract"),
        _elementwise_binary_pattern("nnapi.pow", "relax.power"),
        _elementwise_binary_pattern("nnapi.equal", "relax.equal"),
        _elementwise_binary_pattern("nnapi.greater", "relax.greater"),
        _elementwise_binary_pattern("nnapi.greater_equal", "relax.greater_equal"),
        _elementwise_binary_pattern("nnapi.less", "relax.less"),
        _elementwise_binary_pattern("nnapi.less_equal", "relax.less_equal"),
        _elementwise_binary_pattern("nnapi.not_equal", "relax.not_equal"),
        _elementwise_binary_pattern("nnapi.maximum", "relax.maximum"),
        _elementwise_binary_pattern("nnapi.minimum", "relax.minimum"),
    ]


def unary_patterns() -> list[tuple[str, DFPattern, Mapping[str, DFPattern]]]:
    """
    Returns a list of tuples representing unary operation patterns mapped
    between NNAPI and Relax frameworks.
    """

    def _unary_pattern(
        pattern_name: str, op_name: str
    ) -> tuple[str, DFPattern, Mapping[str, DFPattern]]:
        input0 = wildcard()
        pattern = is_op(op_name)(input0)
        return (pattern_name, pattern, {})

    return [
        _unary_pattern("nnapi.floor", "relax.floor"),
        _unary_pattern("nnapi.relu", "relax.nn.relu"),
        _unary_pattern("nnapi.logistic", "relax.sigmoid"),
        _unary_pattern("nnapi.softmax", "relax.nn.softmax"),
        _unary_pattern("nnapi.tanh", "relax.tanh"),
        _unary_pattern("nnapi.abs", "relax.abs"),
        _unary_pattern("nnapi.exp", "relax.exp"),
        _unary_pattern("nnapi.log", "relax.log"),
        _unary_pattern("nnapi.neg", "relax.negative"),
        _unary_pattern("nnapi.cast", "relax.astype"),
        _unary_pattern("nnapi.sqrt", "relax.sqrt"),
        _unary_pattern("nnapi.rsqrt", "relax.rsqrt"),
    ]


def matmul_pattern() -> tuple[str, DFPattern, Mapping[str, DFPattern]]:
    """
    Returns a tuple representing matmul operation patterns mapped
    between NNAPI and Relax frameworks.
    """
    input0 = wildcard()
    input1 = wildcard()
    pattern = is_op("relax.matmul")(input0, input1)
    return ("nnapi.batch_matmul", pattern, {})


def permute_dims_pattern() -> tuple[str, DFPattern, Mapping[str, DFPattern]]:
    """
    Returns a tuple representing permute operation patterns mapped
    between NNAPI and Relax frameworks.
    """
    input0 = wildcard()
    pattern = is_op("relax.permute_dims")(input0)
    return ("nnapi.transpose", pattern, {})


def astype_pattern() -> tuple[str, DFPattern, Mapping[str, DFPattern]]:
    """
    Returns a tuple representing astype operation patterns mapped
    between NNAPI and Relax frameworks.
    """
    input0 = wildcard().has_dtype("float16") | wildcard().has_dtype("float32")
    pattern = is_op("relax.astype")(input0).has_dtype("float16") | is_op("relax.astype")(
        input0
    ).has_dtype("float32")

    return ("nnapi.cast", pattern, {})


def mean_pattern() -> tuple[str, DFPattern, Mapping[str, DFPattern]]:
    """
    Returns a tuple representing mean operation patterns mapped
    between NNAPI and Relax frameworks.
    """
    input0 = wildcard()
    pattern = is_op("relax.mean")(input0)

    return ("nnapi.mean", pattern, {})


def conv2d_pattern() -> tuple[str, DFPattern, Mapping[str, DFPattern]]:
    """
    Returns a tuple representing conv2d operation patterns mapped
    between NNAPI and Relax frameworks.
    """
    input0 = wildcard()
    input1 = wildcard()
    input2 = wildcard()
    conv = is_op("relax.nn.conv2d")(input0, input1)
    pattern = is_op("relax.add")(conv, input2)
    return ("nnapi.conv2d", pattern, {})


def max_pool2d_pattern() -> tuple[str, DFPattern, Mapping[str, DFPattern]]:
    """
    Returns a tuple representing max_pool2d operation patterns mapped
    between NNAPI and Relax frameworks.
    """
    input0 = wildcard()
    pattern = is_op("relax.nn.max_pool2d")(input0)
    return ("nnapi.max_pool_2d", pattern, {})


register_patterns(
    [
        *elementwise_binary_patterns(),
        *unary_patterns(),
        matmul_pattern(),
        permute_dims_pattern(),
        astype_pattern(),
        mean_pattern(),
        conv2d_pattern(),
        max_pool2d_pattern(),
    ]
)


def min_feature_level(pattern_name: str) -> int:
    """
    Returns the minimum feature level required to support a given NNAPI operation pattern.

    Args:
        pattern_name (str): The name of the NNAPI operation pattern
        (e.g., "nnapi.add", "nnapi.conv2d").

    Returns:
        int: The minimum feature level for the specified pattern, or 1 if the pattern is not found.
    """

    levels = {
        "nnapi.add": 1,
        "nnapi.average_pool_2d": 1,
        "nnapi.concatenation": 1,
        "nnapi.conv2d": 1,
        "nnapi.depthwise_conv_2d": 1,
        "nnapi.depth_to_space": 1,
        "nnapi.dequantize": 1,
        "nnapi.embedding_lookup": 1,
        "nnapi.floor": 1,
        "nnapi.fully_connected": 1,
        "nnapi.hashtable_lookup": 1,
        "nnapi.l2_normalization": 1,
        "nnapi.l2_pool_2d": 1,
        "nnapi.local_response_normalization": 1,
        "nnapi.logistic": 1,
        "nnapi.lsh_projection": 1,
        "nnapi.lstm": 1,
        "nnapi.max_pool_2d": 1,
        "nnapi.mul": 1,
        "nnapi.relu": 1,
        "nnapi.relu1": 1,
        "nnapi.relu6": 1,
        "nnapi.reshape": 1,
        "nnapi.resize_bilinear": 1,
        "nnapi.rnn": 1,
        "nnapi.softmax": 1,
        "nnapi.space_to_depth": 1,
        "nnapi.svdf": 1,
        "nnapi.tanh": 1,
        "nnapi.batch_to_space_nd": 2,
        "nnapi.div": 2,
        "nnapi.mean": 2,
        "nnapi.pad": 2,
        "nnapi.space_to_batch_nd": 2,
        "nnapi.squeeze": 2,
        "nnapi.strided_slice": 2,
        "nnapi.sub": 2,
        "nnapi.transpose": 2,
        "nnapi.abs": 3,
        "nnapi.argmax": 3,
        "nnapi.argmin": 3,
        "nnapi.axis_aligned_bbox_transform": 3,
        "nnapi.bidirectional_sequence_lstm": 3,
        "nnapi.bidirectional_sequence_rnn": 3,
        "nnapi.box_with_nms_limit": 3,
        "nnapi.cast": 3,
        "nnapi.channel_shuffle": 3,
        "nnapi.detection_postprocessing": 3,
        "nnapi.equal": 3,
        "nnapi.exp": 3,
        "nnapi.expand_dims": 3,
        "nnapi.gather": 3,
        "nnapi.generate_proposals": 3,
        "nnapi.greater": 3,
        "nnapi.greater_equal": 3,
        "nnapi.grouped_conv_2d": 3,
        "nnapi.heatmap_max_keypoint": 3,
        "nnapi.instance_normalization": 3,
        "nnapi.less": 3,
        "nnapi.less_equal": 3,
        "nnapi.log": 3,
        "nnapi.logical_and": 3,
        "nnapi.logical_not": 3,
        "nnapi.logical_or": 3,
        "nnapi.log_softmax": 3,
        "nnapi.maximum": 3,
        "nnapi.minimum": 3,
        "nnapi.neg": 3,
        "nnapi.not_equal": 3,
        "nnapi.pad_v2": 3,
        "nnapi.pow": 3,
        "nnapi.prelu": 3,
        "nnapi.quantize": 3,
        "nnapi.quantized_16bit_lstm": 3,
        "nnapi.random_multinomial": 3,
        "nnapi.reduce_all": 3,
        "nnapi.reduce_any": 3,
        "nnapi.reduce_max": 3,
        "nnapi.reduce_min": 3,
        "nnapi.reduce_prod": 3,
        "nnapi.reduce_sum": 3,
        "nnapi.roi_align": 3,
        "nnapi.roi_pooling": 3,
        "nnapi.rsqrt": 3,
        "nnapi.select": 3,
        "nnapi.sin": 3,
        "nnapi.slice": 3,
        "nnapi.split": 3,
        "nnapi.sqrt": 3,
        "nnapi.tile": 3,
        "nnapi.topk_v2": 3,
        "nnapi.transpose_conv_2d": 3,
        "nnapi.unidirectional_sequence_lstm": 3,
        "nnapi.unidirectional_sequence_rnn": 3,
        "nnapi.resize_nearest_neighbor": 3,
        "nnapi.quantized_lstm": 4,
        "nnapi.if": 4,
        "nnapi.while": 4,
        "nnapi.elu": 4,
        "nnapi.hard_swish": 4,
        "nnapi.fill": 4,
        "nnapi.rank": 4,
        "nnapi.batch_matmul": 6,
        "nnapi.pack": 6,
        "nnapi.mirror_pad": 7,
        "nnapi.reverse": 7,
    }
    return levels[pattern_name]


def partition_for_nnapi(mod: IRModule, feature_level: int | None = None) -> IRModule:
    """Partition the graph greedily offloading supported operators to NNAPI.

    Parameters
    ----------
    mod : tvm.ir.IRModule
        The module to run passes on.
    feature_level : Optional[int]
        The maximum NNAPI feature level.

    Returns
    -------
    mod : tvm.ir.IRModule
        Annotated and partitioned module.
    """
    patterns = get_patterns_with_prefix("nnapi")
    if feature_level is not None:
        patterns = [pat for pat in patterns if feature_level >= min_feature_level(pat.name)]
    mod = FuseOpsByPattern(patterns, bind_constants=False, annotate_codegen=False)(mod)
    mod = MergeCompositeFunctions()(mod)
    return mod
