# 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.
# ruff: noqa: E501, F401, F841
"""CLML integration operator tests."""

import inspect
import json
import os

import numpy as np
import pytest
from mod_utils import (
    get_avgpool_expected_codegen,
    get_batchnorm_mod,
    get_binary_op_mod,
    get_clml_conv2d_codegen,
    get_conv2d_transpose_expected_codegen,
    get_dequant_matmul_module,
    get_dequant_vec_matmul_module,
    get_global_avgpool_expected_codegen,
    get_global_maxpool_expected_codegen,
    get_maxpool_expected_codegen,
    get_relax_avgpool_mod,
    get_relax_conv2d_mod,
    get_relax_conv2d_transpose_mod,
    get_relax_global_avgpool_mod,
    get_relax_global_maxpool_mod,
    get_relax_maxpool_mod,
    get_relax_reshape_codegen,
    get_relax_reshape_mod,
    get_unary_op_mod,
)
from utils import skip_unless_adreno_clml, verify_results

import tvm
import tvm.testing
from tvm import relax, rpc
from tvm.relax.backend.adreno import clml
from tvm.relax.backend.adreno.clml import OpenCLMLOffLoad, OpenCLMLOffLoadForLLM
from tvm.script import ir as I
from tvm.script import relax as R
from tvm.script import tirx as T
from tvm.script.ir_builder import IRBuilder
from tvm.script.ir_builder import relax as relax_builder

CLML_VERSION = clml.clml_sdk_version()
TARGET_CLML_VERSION = int(os.environ.get("ADRENO_TARGET_CLML_VERSION", 4))
clml_target = tvm.target.Target("qcom/adreno-opencl-clml")
ref_target = tvm.target.Target("opencl")


def verify_clml_codegen(clml_mod, clml_codegen):
    clml_mod = OpenCLMLOffLoadForLLM(clml_target)(clml_mod)
    clml_mod = OpenCLMLOffLoad()(clml_mod)

    source = clml_mod.attrs["external_mods"][0].inspect_source()
    codegen = json.loads(source)["nodes"]
    for node in range(len(codegen)):
        if codegen[node]["op"] == "input" or codegen[node]["op"] == "const":
            codegen[node]["name"] = ""
        if codegen[node]["op"] == "kernel":
            codegen[node]["name"] = ""

    codegen_str = json.dumps(codegen, sort_keys=True, indent=2)
    known_good_codegen_str = json.dumps(clml_codegen, sort_keys=True, indent=2)
    assert codegen_str == known_good_codegen_str, (
        f"The JSON produced by codegen does not match the expected result. \n"
        f"Actual={codegen_str} \n"
        f"Expected={known_good_codegen_str}"
    )


def verify(
    mod, clml_codegen, inputs_np, params_np, target_minimum_clml_version=None, target_test=True
):
    mod = tvm.relax.transform.BindParams("main", params_np)(mod)
    codegen_mod, clml_mod = mod.clone(), mod
    verify_clml_codegen(codegen_mod, clml_codegen)

    if (
        target_minimum_clml_version is not None
        and TARGET_CLML_VERSION < target_minimum_clml_version
    ):
        print(f"Skipped Eval Tests for {inspect.stack()[1].function} function", flush=True)
        return

    if "ADRENO_TARGET" not in os.environ:
        return

    if target_test:
        verify_results(clml_mod, target=clml_target, ref_target=ref_target)


@pytest.mark.gpu
@skip_unless_adreno_clml
@pytest.mark.parametrize("dtype", ["float32"])
@pytest.mark.parametrize(
    "kernel_h, kernel_w, padding, stride, dilation, out_channels, shape, has_bias, has_bn, has_activation, has_pad, is_depthwise",
    [
        (3, 3, (1, 1), (1, 1), (1, 1), 64, (3, 224, 224), False, True, False, True, False),
        (3, 3, (1, 1), (1, 1), (1, 1), 64, (3, 224, 224), False, True, False, False, False),
        # (5, 5, (2, 2), (1, 1), (1, 1), 16, (16, 64, 64), False, True, True, False, False),
        # (7, 7, (3, 3), (2, 2), (1, 1), 32, (3, 224, 224), True, False, True, True, False),
        (3, 3, (0, 0), (1, 1), (1, 1), 512, (256, 14, 14), True, False, True, False, False),
        (1, 1, (0, 0), (1, 1), (1, 1), 1024, (512, 7, 7), True, False, True, False, False),
        (1, 3, (0, 0), (1, 1), (1, 1), 64, (64, 7, 7), True, False, True, False, False),
        (3, 1, (0, 0), (1, 1), (1, 1), 64, (64, 7, 7), False, True, True, True, False),
    ],
)
def test_conv2d_offload(
    kernel_h,
    kernel_w,
    padding,
    stride,
    dilation,
    out_channels,
    shape,
    has_bias,
    has_bn,
    has_activation,
    has_pad,
    is_depthwise,
    dtype,
):
    low, high = -0.01, 0.01
    rtol, atol = 1e-3, 1e-3
    if CLML_VERSION > 3:
        rtol, atol = 1e-2, 1e-2  # @clml precision

    data_shape = (1, *shape)
    if is_depthwise:
        groups = data_shape[1] // out_channels
    else:
        groups = 1
    padding = (padding[0], padding[1], padding[0], padding[1])

    weight_format = "IOHW" if is_depthwise else "OIHW"
    weight_shape = (out_channels, data_shape[1] // groups, kernel_h, kernel_w)

    data = np.random.uniform(low, high, size=data_shape).astype(dtype)
    weight = np.random.uniform(low, high, size=weight_shape).astype(dtype)
    bias = np.random.uniform(low, high, size=(1, weight_shape[0], 1, 1)).astype(dtype)

    gamma = np.random.uniform(low, high, size=(weight_shape[0],)).astype(dtype)
    beta = np.random.uniform(low, high, size=(weight_shape[0],)).astype(dtype)
    mean = np.random.uniform(low, high, size=(weight_shape[0],)).astype(dtype)
    variance = np.random.uniform(low, high, size=(weight_shape[0],)).astype(dtype)

    inputs_np = [data]
    params_np = {"weight": weight}
    if has_bias:
        params_np["bias"] = bias
    if has_bn:
        params_np.update({"gamma": gamma, "beta": beta, "mean": mean, "variance": variance})

    mod = get_relax_conv2d_mod(
        data_shape,
        weight_shape,
        stride=stride,
        dilation=dilation,
        padding=padding,
        weight_layout=weight_format,
        groups=groups,
        dtype=dtype,
        has_bias=has_bias,
        has_bn=has_bn,
        has_activation=has_activation,
        has_pad=has_pad,
        is_depthwise=is_depthwise,
    )
    clml_codegen = get_clml_conv2d_codegen(
        data_shape,
        weight_shape,
        stride=stride,
        dilation=dilation,
        padding=padding,
        weight_layout=weight_format,
        groups=groups,
        dtype=dtype,
        has_bias=has_bias,
        has_bn=has_bn,
        has_activation=has_activation,
        has_pad=has_pad,
        is_depthwise=is_depthwise,
    )
    verify(mod, clml_codegen, inputs_np, params_np)


@pytest.mark.gpu
@skip_unless_adreno_clml
@pytest.mark.parametrize("dtype", ["float32"])
@pytest.mark.parametrize(
    "dshape, kshape, channels, kernel_size, strides, padding, out_shape",
    [
        ((1, 256, 100, 100), (64, 256, 4, 4), 64, (4, 4), (2, 2), (0, 0, 0, 0), (1, 64, 202, 202)),
        ((1, 64, 200, 200), (64, 64, 4, 4), 64, (4, 4), (2, 2), (1, 1, 1, 1), (1, 64, 400, 400)),
        ((1, 64, 200, 200), (64, 64, 4, 4), 64, (4, 4), (2, 2), (1, 1, 1, 1), (1, 64, 400, 400)),
        ((1, 64, 400, 400), (16, 64, 4, 4), 16, (4, 4), (2, 2), (1, 1, 1, 1), (1, 16, 800, 800)),
    ],
)
def test_conv2d_transpose(
    dshape, kshape, channels, kernel_size, strides, padding, dtype, out_shape
):
    low, high = -1, 1

    data = np.random.uniform(low, high, size=dshape).astype(dtype)
    weight = np.random.uniform(low, high, size=kshape).astype(dtype)

    inputs_np = [data]
    params_np = {"weight": weight}

    mod = get_relax_conv2d_transpose_mod(
        dshape,
        kshape,
        channels=channels,
        stride=strides,
        padding=padding,
        dtype=dtype,
    )

    clml_codegen = get_conv2d_transpose_expected_codegen(
        dshape=dshape,
        kshape=kshape,
        channels=channels,
        kernel_size=kernel_size,
        strides=strides,
        padding=padding,
        dilation=(1, 1),
        dtype=dtype,
        output_shape=out_shape,
    )
    verify(mod, clml_codegen, inputs_np, params_np, target_test=False)


@pytest.mark.gpu
@skip_unless_adreno_clml
@pytest.mark.skipif(
    CLML_VERSION < 3,
    reason="Requires compiler supporting CLML v5 or above",
)
@pytest.mark.parametrize("dtype", ["float32"])
@pytest.mark.parametrize(
    "trials",
    [
        [(1, 64, 14, 14), 1, 3e-4],
        [(1, 14, 256, 256), 1, 3e-4],
        [(1, 14, 256, 256), 1, 3e-4],
        [(1, 256, 1, 1), 1, 3e-4],
    ],
)
def test_batchnorm(dtype, trials):
    low, high = 0, 1
    (input_shape, axis, epsilon) = trials
    channels = input_shape[axis]

    def _get_axis_tuple(axis):
        if axis == 0:
            return (1, 2, 3)
        elif axis == 1:
            return (0, 2, 3)
        elif axis == 2:
            return (0, 1, 3)
        else:
            return (0, 1, 2)

    data = np.random.uniform(low, high, size=(input_shape)).astype(dtype)
    gamma = np.random.uniform(low, high, size=(channels)).astype(dtype)
    beta = np.random.uniform(low, high, size=(channels)).astype(dtype)
    mean = np.mean(data, _get_axis_tuple(axis), keepdims=False)
    variance = np.var(data, _get_axis_tuple(axis), keepdims=False)

    inputs_np = [data]
    params_np = {"gamma": gamma, "beta": beta, "moving_mean": mean, "moving_var": variance}
    mod = get_batchnorm_mod(input_shape, channels, axis, epsilon, dtype)
    clml_codegen = [
        {
            "attrs": {"dtype": [dtype], "shape": [input_shape]},
            "name": "",
            "op": "input",
        },
        {"attrs": {"dtype": [dtype], "shape": [[channels]]}, "name": "", "op": "const"},
        {"attrs": {"dtype": [dtype], "shape": [[channels]]}, "name": "", "op": "const"},
        {"attrs": {"dtype": [dtype], "shape": [[channels]]}, "name": "", "op": "const"},
        {"attrs": {"dtype": [dtype], "shape": [[channels]]}, "name": "", "op": "const"},
        {
            "attrs": {
                "axis": axis,
                "center": 1,
                "dtype": [dtype],
                "momentum": 0.10000000000000001,
                "epsilon": 0.00029999999999999997,
                "num_inputs": 5,
                "num_outputs": 1,
                "scale": 1,
                "training": 1,
                "shape": [input_shape],
            },
            "inputs": [[0, 0, 0], [1, 0, 0], [2, 0, 0], [3, 0, 0], [4, 0, 0]],
            "name": "",
            "op": "kernel",
        },
    ]
    verify(mod, clml_codegen, inputs_np, params_np)


@pytest.mark.gpu
@skip_unless_adreno_clml
@pytest.mark.parametrize("dtype", ["float32"])
@pytest.mark.parametrize(
    "a_shape, b_shape, op",
    [
        ((1, 64, 14, 14), (1, 64, 14, 14), R.add),
        ((1, 256), (1, 256), R.add),
        ((1, 64, 14, 14), (1, 64, 14, 14), R.subtract),
        ((1, 256), (1, 256), R.subtract),
        ((1, 64, 14, 14), (1, 64, 14, 14), R.multiply),
        ((1, 256), (1, 256), R.multiply),
        ((1, 64, 14, 14), (1, 64, 14, 14), R.divide),
        ((1, 256), (1, 256), R.divide),
        ((1, 64, 14, 14), (1, 64, 14, 14), R.minimum),
        ((1, 256), (1, 256), R.minimum),
        ((1, 64, 14, 14), (1, 64, 14, 14), R.maximum),
        ((1, 256), (1, 256), R.maximum),
    ],
)
@pytest.mark.gpu
@skip_unless_adreno_clml
def test_binary_ops(a_shape, b_shape, op, dtype):
    (mod, inputs_np) = get_binary_op_mod(a_shape, b_shape, op, dtype)
    clml_codegen = [
        {
            "attrs": {
                "dtype": [dtype],
                "shape": [a_shape],
            },
            "name": "",
            "op": "input",
        },
        {
            "attrs": {
                "dtype": [dtype],
                "shape": [b_shape],
            },
            "name": "",
            "op": "input",
        },
        {
            "attrs": {
                "dtype": [dtype],
                "num_inputs": 2,
                "num_outputs": 1,
                "shape": [a_shape],
            },
            "inputs": [[0, 0, 0], [1, 0, 0]],
            "name": "",
            "op": "kernel",
        },
    ]
    verify(mod, clml_codegen, inputs_np, {})


@pytest.mark.gpu
@skip_unless_adreno_clml
@pytest.mark.parametrize(
    "dtype",
    [
        "float32",
    ],
)
@pytest.mark.parametrize(
    "a_shape, op",
    [
        ((1, 64, 14, 14), R.nn.relu),
        ((1, 256, 1, 1), R.nn.relu),
        ((1, 14, 256, 256), R.nn.relu),
        ((1, 14, 14, 256), R.nn.relu),
    ],
)
@pytest.mark.gpu
@skip_unless_adreno_clml
def test_unary_ops(a_shape, op, dtype):
    (mod, inputs_np) = get_unary_op_mod(a_shape, op, dtype)
    clml_codegen = [
        {
            "attrs": {
                "dtype": [dtype],
                "shape": [a_shape],
            },
            "name": "",
            "op": "input",
        },
        {
            "attrs": {
                "activation_type": "relu",
                "dtype": [dtype],
                "num_inputs": 1,
                "num_outputs": 1,
                "shape": [a_shape],
            },
            "inputs": [[0, 0, 0]],
            "name": "",
            "op": "kernel",
        },
    ]
    verify(mod, clml_codegen, inputs_np, {})


@pytest.mark.gpu
@skip_unless_adreno_clml
@pytest.mark.parametrize("dtype", ["float32"])
@pytest.mark.parametrize(
    "trials",
    [
        [(1, 64, 147, 147), (3, 3), (2, 2), (1, 1), (0, 0, 0, 0), False],
        [(1, 256, 17, 17), (3, 3), (1, 1), (1, 1), (0, 0, 0, 0), False],
        [(1, 1024, 14, 14), (3, 3), (1, 1), (1, 1), (0, 0, 0, 0), False],
        # With padding is realized as nn.pad + pool
        # [(1, 32, 256, 256), (3, 3), (2, 2), (1, 1), (1, 1, 1, 1), True],
        # [(1, 32, 256, 256), (3, 3), (2, 2), (1, 1), (0, 1, 0, 1), True],
        # [(1, 32, 256, 256), (2, 2), (2, 2), (1, 1), (1, 1, 1, 1), True],
        # [(1, 32, 256, 256), (2, 2), (2, 2), (1, 1), (1, 0, 1, 0), True],
    ],
)
def test_max_pool(dtype, trials):
    low, high = -1, 1
    (input_shape, pool_size, stride, dilation, padding, has_pad) = trials
    mod = get_relax_maxpool_mod(input_shape, dtype, pool_size, stride, dilation, padding, has_pad)
    clml_codegen = get_maxpool_expected_codegen(
        input_shape, pool_size, stride, padding, "maxpool2d", dtype
    )

    inputs_np = [np.random.uniform(low, high, size=input_shape).astype(dtype)]
    verify(mod, clml_codegen, inputs_np, {})


@pytest.mark.gpu
@skip_unless_adreno_clml
@pytest.mark.parametrize("dtype", ["float32"])
@pytest.mark.parametrize(
    "trials",
    [
        [(1, 64, 147, 147), (3, 3), (2, 2), (1, 1), (0, 0, 0, 0), False],
        [(1, 256, 17, 17), (3, 3), (1, 1), (1, 1), (0, 0, 0, 0), False],
        [(1, 1024, 14, 14), (3, 3), (1, 1), (1, 1), (0, 0, 0, 0), False],
        # With padding is realized as nn.pad + pool
        # [(1, 32, 256, 256), (3, 3), (2, 2), (1, 1), (1, 1, 1, 1), True],
        # [(1, 32, 256, 256), (3, 3), (2, 2), (1, 1), (0, 1, 0, 1), True],
        # [(1, 32, 256, 256), (2, 2), (2, 2), (1, 1), (1, 1, 1, 1), True],
        # [(1, 32, 256, 256), (2, 2), (2, 2), (1, 1), (1, 0, 1, 0), True],
    ],
)
def test_avg_pool(dtype, trials):
    low, high = -1, 1
    (input_shape, pool_size, stride, dilation, padding, has_pad) = trials
    mod = get_relax_avgpool_mod(input_shape, dtype, pool_size, stride, dilation, padding, has_pad)
    clml_codegen = get_avgpool_expected_codegen(
        input_shape, pool_size, stride, padding, "avg_pool2d", dtype
    )

    inputs_np = [np.random.uniform(low, high, size=input_shape).astype(dtype)]
    params_np = {}
    verify(mod, clml_codegen, inputs_np, {})


@pytest.mark.gpu
@skip_unless_adreno_clml
@pytest.mark.parametrize("dtype", ["float32"])
@pytest.mark.parametrize(
    "trials",
    [
        [(1, 3, 32, 32), (1, 4, -1, 32)],
        [(1, 4, 8, 32), (1, 4, -1, 16)],
        [(1, 64, 3, 3), (1, 32, 3, -1)],
    ],
)
def test_reshape(dtype, trials):
    low, high = -1, 1
    (input_shape, output_shape) = trials
    mod = get_relax_reshape_mod(input_shape, output_shape, dtype)
    clml_codegen = get_relax_reshape_codegen(input_shape, output_shape, dtype)

    inputs_np = [np.random.uniform(low, high, size=input_shape).astype(dtype)]
    verify(mod, clml_codegen, inputs_np, {})


@pytest.mark.skip(reason="Codegen Comparision Failing")
@pytest.mark.gpu
@skip_unless_adreno_clml
@pytest.mark.parametrize("dtype", ["float32"])
@pytest.mark.parametrize(
    "trials",
    [
        [(1, 64, 147, 147), True],
        [(1, 256, 17, 17), False],
        [(1, 1024, 14, 14), True],
        [(1, 32, 256, 256), False],
    ],
)
def test_global_avg_pool(dtype, trials):
    """Test function for global average pooling."""
    low, high = -1, 1
    (input_shape, keep_dims) = trials
    N, C, H, W = input_shape
    pool_size, stride, padding = (H, W), (1, 1), (0, 0, 0, 0)
    mod = get_relax_global_avgpool_mod(input_shape, keep_dims, dtype)
    clml_codegen = get_global_maxpool_expected_codegen(
        input_shape, pool_size, stride, padding, "global_max", dtype
    )

    inputs_np = [np.random.uniform(low, high, size=input_shape).astype(dtype)]
    verify(mod, clml_codegen, inputs_np, {})


@pytest.mark.gpu
@skip_unless_adreno_clml
@pytest.mark.parametrize("dtype", ["float32"])
@pytest.mark.parametrize(
    "trials",
    [
        [(1, 64, 147, 147), True],
        [(1, 256, 17, 17), False],
        [(1, 1024, 14, 14), True],
        [(1, 32, 256, 256), False],
    ],
)
def test_global_max_pool(dtype, trials):
    """Test function for global average pooling."""
    low, high = -1, 1
    (input_shape, keep_dims) = trials
    N, C, H, W = input_shape
    pool_size, stride, padding = (H, W), (1, 1), (0, 0, 0, 0)
    mod = get_relax_global_maxpool_mod(input_shape, keep_dims, dtype)
    clml_codegen = get_global_maxpool_expected_codegen(
        input_shape, pool_size, stride, padding, "global_max", dtype
    )

    inputs_np = [np.random.uniform(low, high, size=input_shape).astype(dtype)]
    verify(mod, clml_codegen, inputs_np, {})


@pytest.mark.skipif(
    CLML_VERSION < 5,
    reason="Requires target device with CLML v5 or above",
)
@pytest.mark.parametrize(
    "K, N, M",
    [
        (4096, 11008, 256),
        (2048, 32768, 128),
        (4096, 4096, 512),
        (4096, 22016, 64),
        (16384, 2048, 128),
        (2048, 2560, 1024),
        (3072, 9216, 256),
        (14336, 4096, 128),
        (1536, 17920, 128),
        (8960, 1536, 1024),
    ],
)
def test_dequant_matmul(K, N, M):
    x_data = np.random.uniform(-0.1, 0.1, size=(1, M, K)).astype("float16")
    weight = np.random.randint(0, 100, size=(K // 8, N)).astype("uint32")
    scale = np.random.uniform(-0.1, 0.1, size=(K // 32, N)).astype("float16")

    mod = get_dequant_matmul_module(K, N)
    clml_codegen = [
        {
            "op": "input",
            "name": "",
            "attrs": {"shape": [[[K // 8, N]]], "dtype": [["uint32"]]},
        },
        {
            "op": "input",
            "name": "",
            "attrs": {"shape": [[[K // 32, N]]], "dtype": [["float16"]]},
        },
        {
            "op": "input",
            "name": "",
            "attrs": {"shape": [[[1, -1, K]]], "dtype": [["float16"]]},
        },
        {
            "op": "kernel",
            "name": "",
            "inputs": [[0, 0, 0], [1, 0, 0], [2, 0, 0]],
            "attrs": {
                "dtype": ["float16"],
                "num_inputs": 3,
                "num_outputs": 1,
                "out_dtype": ["float16"],
                "shape": [[1, -1, N]],
            },
        },
    ]

    inputs_np = [x_data, weight, scale]
    verify(mod, clml_codegen, inputs_np, {}, target_minimum_clml_version=5)


@pytest.mark.skipif(
    CLML_VERSION < 5,
    reason="Requires compiler supporting CLML v5 or above",
)
@pytest.mark.parametrize(
    "K, N",
    [
        (4096, 11008),
        (2048, 32768),
        (4096, 4096),
        (4096, 22016),
        (16384, 2048),
        (2048, 2560),
        (3072, 9216),
        (4096, 28672),
        (14336, 4096),
        (1536, 17920),
        (8960, 1536),
    ],
)
def test_dequant_vec_matmul(K, N):
    x_data = np.random.uniform(-0.1, 0.1, size=(1, 1, K)).astype("float16")
    weight = np.random.randint(0, 100, size=(K // 8, N)).astype("uint32")
    scale = np.random.uniform(-0.1, 0.1, size=(K // 32, N)).astype("float16")

    mod = get_dequant_vec_matmul_module(K, N)
    clml_codegen = [
        {
            "op": "input",
            "name": "",
            "attrs": {"shape": [[[K // 8, -1]]], "dtype": [["uint32"]]},
        },
        {
            "op": "input",
            "name": "",
            "attrs": {"shape": [[[K // 32, -1]]], "dtype": [["float16"]]},
        },
        {
            "op": "input",
            "name": "",
            "attrs": {"shape": [[[1, 1, K]]], "dtype": [["float16"]]},
        },
        {
            "op": "kernel",
            "name": "",
            "inputs": [[0, 0, 0], [1, 0, 0], [2, 0, 0]],
            "attrs": {
                "dtype": ["float16"],
                "num_inputs": 3,
                "num_outputs": 1,
                "out_dtype": ["float16"],
                "shape": [[1, 1, -1]],
            },
        },
    ]

    inputs_np = (x_data, weight, scale)
    verify(mod, clml_codegen, inputs_np, {}, target_minimum_clml_version=5)


if __name__ == "__main__":
    tvm.testing.main()
