# 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, no-member
"""VM build logics"""

import tvm
from tvm import relax
from tvm.ir.module import IRModule
from tvm.runtime import Executable
from tvm.tirx.function import PrimFunc

from . import _ffi_api


class VMExecutable(Executable):
    """The virtual machine executable object emitted by the VM compiler or the ExecBuilder."""

    def __init__(self, mod: tvm.runtime.Module):
        super().__init__(mod)
        self._stats = self.mod["stats"]
        self._as_text = self.mod["as_text"]
        self._as_python = self.mod["as_python"]

    def stats(self) -> str:
        """print the detailed statistics of the executable."""
        return self._stats()

    def as_text(self) -> str:
        """print the instructions as text format."""
        return self._as_text()

    def as_python(self) -> str:
        """print the instructions as python program."""
        return self._as_python()


def _vmcodegen(
    builder: "relax.ExecBuilder",
    mod: tvm.IRModule,
    exec_mode: str = "bytecode",
) -> tvm.IRModule:
    """Running VM codegen.

    Parameters
    ----------
    builder: relax.ExecBuilder
        ExecBuilder to collect the vm executable.

    mod: IRModule
        The input IRModule to be built.

    exec_mode: {"bytecode", "compiled"}
        The execution mode.

    Return
    ------
    leftover: IRModule
        Left over IRModule that may contain extra functions.
    """

    if exec_mode == "bytecode":
        return _ffi_api.VMCodeGen(builder, mod)  # type:ignore
    if exec_mode == "compiled":
        return _ffi_api.VMTIRCodeGen(builder, mod)  # type: ignore
    raise ValueError(f"Unknown exec_mode {exec_mode}")


def _auto_attach_system_lib_prefix(
    tir_mod: tvm.IRModule,
    target: tvm.target.Target | None = None,
    system_lib: bool | None = None,
):
    """Automatically detect system lib req and attach prefix attr"""
    if target is not None:
        host = target if target.host is None else target.host
        if system_lib is None:
            system_lib = False
            if "wasm" in host.attrs.get("mtriple", ""):
                system_lib = True

    if system_lib:
        if tir_mod.get_attr("system_lib_prefix") is None:
            return tir_mod.with_attr("system_lib_prefix", "")
    return tir_mod


def _is_device_module(mod: tvm.runtime.Module) -> bool:
    return mod.kind in ["cuda", "opencl", "metal", "hip", "vulkan", "webgpu"]


def _vmlink(
    builder: "relax.ExecBuilder",
    target: str | tvm.target.Target | None,
    tir_mod: tvm.IRModule | None = None,
    tir_pipeline: str | tvm.transform.Pass | None = "default",
    ext_libs: list[tvm.runtime.Module] | None = None,
    params: dict[str, list] | None = None,
    *,
    system_lib: bool | None = None,
):
    """
    Internal codegen function to make executable.

    This function is only used for unit-testing purpoes.

    Use build instead.

    Parameters
    ----------
    builder: relax.ExecBuilder
        Builder used to collect executables.

    target : Optional[Union[str, tvm.target.Target]]
        A build target which can have optional host side compilation target.
        If the target is not specified, the target in the vdevice list will be used.
        For multi-target compilation, the vdevice should be annotated.

    tir_mod: IRModule
        The input TIR IRModule to be linked together.

    ext_libs:  List[tvm.runtime.Module]
        List of compiled external modules.

    params: Optional[Dict[str, list]]
        Extra parameter mappings.

    Returns
    -------
    ex: tvm.relax.Executable
        An executable that can be loaded by virtual machine.
    """
    if isinstance(target, str):
        target = tvm.target.Target(target)
    if params is None:
        params = {}
    if ext_libs is None:
        ext_libs = []
    lib = None
    relax_ext_libs = []
    tir_ext_libs = []
    if tir_mod is not None and len(tir_mod.get_global_vars()) > 0:
        tir_mod = _auto_attach_system_lib_prefix(tir_mod, target, system_lib)
        lib = tvm.tirx.build(tir_mod, target=target, pipeline=tir_pipeline)
    for ext_mod in ext_libs:
        if _is_device_module(ext_mod):
            tir_ext_libs.append(ext_mod)
        else:
            relax_ext_libs.append(ext_mod)
    if lib is not None:
        for mod in tir_ext_libs:
            lib.import_module(mod)
    elif len(tir_ext_libs) > 0:
        print("Warning: No TIR module is found, but external modules for TIR are provided.")
    lib = _ffi_api.VMLink(builder, target, lib, relax_ext_libs, params)  # type: ignore
    return VMExecutable(lib)


def build(
    mod: tvm.IRModule,
    target: str | tvm.target.Target | None = None,
    params: dict[str, list] | None = None,
    relax_pipeline: None | str | tvm.transform.Pass = "default",
    tir_pipeline: None | str | tvm.transform.Pass = "default",
    exec_mode: str = "bytecode",
    *,
    system_lib: bool | None = None,
) -> Executable:
    """
    Build an IRModule to VM executable.

    Parameters
    ----------
    mod: IRModule
        The input IRModule to be built.

    target : Optional[Union[str, tvm.target.Target]]
        A build target which can have optional host side compilation target.

        When TVM compiles device specific program such as CUDA,
        we also need host(CPU) side code to interact with the driver
        to setup the dimensions and parameters correctly.
        host is used to specify the host side codegen target.
        By default, llvm is used if it is enabled,
        otherwise a c backend is used.

    params: Optional[Dict[str, list]]
        Parameters for the input IRModule that will be bound.

    relax_pipeline : str = "default"
        The Relax compilation pipeline to use.

    tir_pipelinie : str = "default"
        The TIR compilation pipeline to use.

    exec_mode: {"bytecode", "compiled"}
        The execution mode.

    system_lib: Optional[bool]
        Whether to build system lib that is being packed statically and
        auto registers generated functions to the system.
        By default auto detects based on the target.

    Returns
    -------
    ex: tvm.relax.Executable
        An executable that can be loaded by virtual machine.

    Example
    -------

    .. code-block:: python

        class InputModule:
            @R.function
            def foo(x: Tensor((3, 4), "float32"), y: Tensor((3, 4), "float32")):
                z = R.add(x, y)
                return z

        mod = InputModule
        target = tvm.target.Target("llvm", host="llvm")
        ex = tvm.compile(mod, target)
    """

    def _extract_attrs(mod: tvm.IRModule):
        attrs = dict(mod.attrs) if mod.attrs else {}
        ext_libs = attrs.get("external_mods", [])
        constants = attrs.get("const_name_to_constant", {})
        return ext_libs, constants

    if isinstance(target, str):
        target = tvm.target.Target(target)
    if not params:
        params = {}

    if relax_pipeline is not None:
        if isinstance(relax_pipeline, str):
            # For GPU targets, prefer the target-specific pipeline which
            # includes DLight scheduling. Without it, TIR functions generated
            # from ops like Clip/ReLU6 lack thread bindings and fail
            # VerifyMemory. CPU targets continue to use the generic pipeline
            # since the CPU-specific pipeline applies fusion passes that can
            # incorrectly remove call_pure_packed calls whose results are
            # unused but whose side effects are relied upon.
            _is_gpu = target is not None and "gpu" in target.keys
            if relax_pipeline == "default" and _is_gpu:
                try:
                    relax_pipeline = relax.get_default_pipeline(target)
                except (ValueError, AttributeError):
                    relax_pipeline = relax.get_pipeline(relax_pipeline)
            else:
                relax_pipeline = relax.get_pipeline(relax_pipeline)
        if target is None:
            mod = relax_pipeline(mod)
        else:
            with target:
                mod = relax_pipeline(mod)

    ext_libs, constants = _extract_attrs(mod)
    params.update(dict(constants))
    builder = relax.ExecBuilder()
    mod = _vmcodegen(builder, mod, exec_mode)
    return _vmlink(
        builder=builder,
        target=target,
        tir_mod=_filter_tir(mod),
        tir_pipeline=tir_pipeline,
        ext_libs=ext_libs,
        params=params,
        system_lib=system_lib,
    )


def _filter_tir(mod: tvm.IRModule) -> tvm.IRModule | None:
    tir_mod = {gvar: func for gvar, func in mod.functions.items() if isinstance(func, PrimFunc)}

    if tir_mod:
        return IRModule(tir_mod, attrs=mod.attrs)
    else:
        return None
