# 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=unused-argument
"""Tools to compare libraries."""

from collections.abc import Iterable

import tvm
import tvm.testing


class LibCompareVMInstrument:
    """Instrument class to compare libs.

    This class build an instrument function that
    pair tests an existing compiled relax vm implementation
    and an extra module, which can sits in another backend
    but offers a same subset of compiled TIR functions.

    The instrumentation enables us to automatically
    check and compare each ops being called in the pipeline
    by looking up the same name in the provided mod and run testing.

    Parameters
    ----------
    mod: runtime.Module
        The module of interest to be validated.

    device: runtime.Device
        The device to run the target module on.

    verbose: bool
        Whether print out messages.

    rtol: float
        rtol used in validation

    atol: float
        atol used in validation
    """

    def __init__(self, mod, device, verbose=True, rtol=1e-5, atol=1e-5):
        self.mod = mod
        self.device = device
        self.verbose = verbose
        self.counter = 0
        self.rtol = rtol
        self.atol = atol

    def compare(
        self,
        name: str,
        ref_args: list[tvm.runtime.Tensor] | tuple[tvm.runtime.Tensor, ...],
        new_args: list[tvm.runtime.Tensor] | tuple[tvm.runtime.Tensor, ...],
        ret_indices: Iterable[int],
    ):
        """Comparison function, can be overloaded.

        Parameters
        ----------
        name: str
            Name of the function.

        ref_args:
            The reference arguments.

        new_args:
            The args to be passed to the comparison function.

        ret_indices:
            List of indices to validate return values.
        """
        my_func = self.mod.get_function(name, query_imports=True)
        if self.verbose:
            print(f"[{self.counter}] Validating {name} ...")
        my_func(*new_args)
        for rindex in ret_indices:
            tvm.testing.assert_allclose(
                new_args[rindex].numpy(), ref_args[rindex].numpy(), atol=self.atol, rtol=self.rtol
            )
        if self.verbose:
            print(f"[{self.counter}] Validating {name}, passed.")
        self.counter += 1

    def skip_instrument(self, func, name, before_run, ret_val, *args):
        return False

    def __call__(self, func, name, before_run, ret_val, *args):
        if before_run:
            return
        if name.startswith("vm.builtin."):
            return
        if any(not isinstance(x, tvm.runtime.Tensor) for x in args):
            return
        try:
            self.mod.get_function(name, query_imports=True)
        except AttributeError:
            if self.verbose:
                print(f"Cannot find {name}, skip...")
            return

        if self.skip_instrument(func, name, before_run, ret_val, *args):
            return

        new_args = []
        # not always true, true for most ops.
        ret_indices = (len(args) - 1,)
        temp_args = []
        for i, arg in enumerate(args):
            arr = tvm.runtime.empty(arg.shape, arg.dtype, device=self.device)
            # copy from cpu since we look at different device
            if i not in ret_indices:
                temp_cpu = arg.copyto(tvm.cpu())
                temp_args.append(temp_cpu)
                arr.copyfrom(temp_cpu)
            new_args.append(arr)
        # wait until all copy complete before we release temp_cpu
        self.device.sync()
        self.compare(name, args, new_args, ret_indices)
