# 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.
"""Optional module to support faster DLPack conversion.

This is an optional module to support faster DLPack conversion for torch.
Some of the changes are merged but not yet released, so it is used
as a stop gap to support faster DLPack conversion.

This file contains source code from PyTorch:
License: licenses/LICENSE.pytorch.txt

This module only serves as temp measure and will
likely be phased away and deleted after changes landed and released in pytorch.

This module will load slowly at first time due to JITing,
subsequent calls will be much faster.
"""

from __future__ import annotations

import ctypes
import logging
import os
import subprocess
import sys
import warnings
from pathlib import Path
from typing import Any

logger = logging.getLogger(__name__)


def _create_dlpack_exchange_api_capsule(ptr_as_int: int) -> Any:
    """Create a PyCapsule wrapping the DLPack exchange API pointer.

    Parameters
    ----------
    ptr_as_int : int
        The pointer to the DLPack exchange API as an integer.

    Returns
    -------
    capsule : PyCapsule
        A PyCapsule object wrapping the pointer with name "dlpack_exchange_api".

    """
    capsule_name = b"dlpack_exchange_api"
    pythonapi = ctypes.pythonapi
    pythonapi.PyCapsule_New.restype = ctypes.py_object
    pythonapi.PyCapsule_New.argtypes = [
        ctypes.c_void_p,
        ctypes.c_char_p,
        ctypes.c_void_p,
    ]
    capsule = pythonapi.PyCapsule_New(ctypes.c_void_p(ptr_as_int), capsule_name, None)
    return capsule


def _check_and_update_dlpack_c_exchange_api(tensor_cls: object) -> bool:
    """Check if the DLPack exchange API is available and update the __dlpack_c_exchange_api__ attribute."""
    if hasattr(tensor_cls, "__dlpack_c_exchange_api__"):
        return True
    # legacy path compactibility handling
    if hasattr(tensor_cls, "__c_dlpack_exchange_api__"):
        c_dlpack_attribute = tensor_cls.__c_dlpack_exchange_api__
        if isinstance(c_dlpack_attribute, int):
            setattr(
                tensor_cls,
                "__dlpack_c_exchange_api__",
                _create_dlpack_exchange_api_capsule(c_dlpack_attribute),
            )
        else:
            setattr(tensor_cls, "__dlpack_c_exchange_api__", c_dlpack_attribute)
        return True
    return False


def load_torch_c_dlpack_extension() -> Any:  # noqa: PLR0912, PLR0915
    try:
        import torch  # noqa: PLC0415
        import torch.version  # noqa: PLC0415

        prefer_rocm_override = bool(torch.cuda.is_available() and torch.version.hip is not None)
        if _check_and_update_dlpack_c_exchange_api(torch.Tensor) and not prefer_rocm_override:
            # skip loading the extension if the __dlpack_c_exchange_api__
            # attribute is already set so we don't have to do it in
            # newer version of PyTorch
            return None
    except ImportError:
        return None

    """Load the torch c dlpack extension."""
    try:
        import torch_c_dlpack_ext  # noqa: PLC0415, F401

        if _check_and_update_dlpack_c_exchange_api(torch.Tensor) and not prefer_rocm_override:
            return None
    except ImportError:
        pass
    except AttributeError:
        # When torch_c_dlpack_ext and torch have different ABI
        # `ctypes.CDLL` will raise an `AttributeError`.
        # Keep trying JIT
        pass

    try:
        # check whether a JIT shared library is built in cache
        cache_dir = Path(os.environ.get("TVM_FFI_CACHE_DIR", "~/.cache/tvm-ffi")).expanduser()
        addon_output_dir = cache_dir
        major, minor = torch.__version__.split(".")[:2]
        # First use "torch.cuda.is_available()" to check whether GPU environment
        # is available. Then determine the GPU type.
        if torch.cuda.is_available():
            if torch.version.cuda is not None:
                device = "cuda"
            elif torch.version.hip is not None:
                device = "rocm"
            else:
                raise ValueError("Cannot determine whether to build with CUDA or ROCm.")
        else:
            device = "cpu"
        suffix = ".dll" if sys.platform.startswith("win") else ".so"
        libname = f"libtorch_c_dlpack_addon_torch{major}{minor}-{device}{suffix}"
        lib_path = addon_output_dir / libname
        if not lib_path.exists():
            logger.debug("JIT-compiling torch-c-dlpack-ext to cache...")
            build_script_path = (
                Path(__file__).parent / "utils" / "_build_optional_torch_c_dlpack.py"
            )
            args = [
                sys.executable,
                str(build_script_path),
                "--output-dir",
                str(cache_dir),
                "--libname",
                libname,
            ]
            if device == "cuda":
                args.append("--build-with-cuda")
            elif device == "rocm":
                args.append("--build-with-rocm")

            # use capture_output to reduce noise when building the torch c dlpack addon
            result = subprocess.run(args, check=False, capture_output=True)
            if result.returncode != 0:
                msg = [f"Build failed with status {result.returncode}"]
                if result.stdout:
                    msg.append(f"stdout:\n{result.stdout.decode('utf-8')}")
                if result.stderr:
                    msg.append(f"stderr:\n{result.stderr.decode('utf-8')}")
                raise RuntimeError("\n".join(msg))
            if not lib_path.exists():
                raise RuntimeError("Failed to build torch c dlpack addon.")

        lib = ctypes.CDLL(str(lib_path))
        func = lib.TorchDLPackExchangeAPIPtr
        func.restype = ctypes.c_uint64
        func.argtypes = []
        # Create a PyCapsule from the pointer
        capsule = _create_dlpack_exchange_api_capsule(func())
        # Set the DLPackExchangeAPI pointer on the class
        setattr(torch.Tensor, "__dlpack_c_exchange_api__", capsule)
        return lib
    except ImportError:
        pass
    except Exception:
        warnings.warn(
            "Failed to JIT torch c dlpack extension, EnvTensorAllocator will not be enabled.\n"
            "We recommend installing via `pip install torch-c-dlpack-ext`"
        )
    return None


def patch_torch_cuda_stream_protocol() -> None:
    """Load the torch cuda stream protocol for older versions of torch."""
    try:
        import torch  # noqa: PLC0415

        if not torch.cuda.is_available():
            return
        if not hasattr(torch.cuda.Stream, "__cuda_stream__"):

            def __torch_cuda_stream__(self: torch.cuda.Stream) -> tuple[int, int]:
                """Return the version number and the cuda stream."""
                return (0, self.cuda_stream)

            setattr(torch.cuda.Stream, "__cuda_stream__", __torch_cuda_stream__)
    except ImportError:
        pass


if os.environ.get("TVM_FFI_DISABLE_TORCH_C_DLPACK", "0") == "0":
    _LIB = load_torch_c_dlpack_extension()  # keep a reference to the loaded shared library
    patch_torch_cuda_stream_protocol()
