"""Cudatoolkit libraries lookup utilities.

Cudatoolkit libraries can be available via either:

- the `cudatoolkit` conda package,
- a user supplied location from CUDA_HOME,
- a system wide location,
- package-specific locations (e.g. the Debian NVIDIA packages),
- or can be discovered by the system loader.
"""

import os
import sys
import ctypes

from numba.misc.findlib import find_lib
from numba.cuda.cuda_paths import get_cuda_paths
from numba.cuda.cudadrv.driver import locate_driver_and_loader, load_driver
from numba.cuda.cudadrv.error import CudaSupportError


if sys.platform == 'win32':
    _dllnamepattern = '%s.dll'
    _staticnamepattern = '%s.lib'
elif sys.platform == 'darwin':
    _dllnamepattern = 'lib%s.dylib'
    _staticnamepattern = 'lib%s.a'
else:
    _dllnamepattern = 'lib%s.so'
    _staticnamepattern = 'lib%s.a'


def get_libdevice():
    d = get_cuda_paths()
    paths = d['libdevice'].info
    return paths


def open_libdevice():
    with open(get_libdevice(), 'rb') as bcfile:
        return bcfile.read()


def get_cudalib(lib, static=False):
    """
    Find the path of a CUDA library based on a search of known locations. If
    the search fails, return a generic filename for the library (e.g.
    'libnvvm.so' for 'nvvm') so that we may attempt to load it using the system
    loader's search mechanism.
    """
    if lib == 'nvvm':
        return get_cuda_paths()['nvvm'].info or _dllnamepattern % 'nvvm'
    else:
        dir_type = 'static_cudalib_dir' if static else 'cudalib_dir'
        libdir = get_cuda_paths()[dir_type].info

    candidates = find_lib(lib, libdir, static=static)
    namepattern = _staticnamepattern if static else _dllnamepattern
    return max(candidates) if candidates else namepattern % lib


def open_cudalib(lib):
    path = get_cudalib(lib)
    return ctypes.CDLL(path)


def check_static_lib(path):
    if not os.path.isfile(path):
        raise FileNotFoundError(f'{path} not found')


def _get_source_variable(lib, static=False):
    if lib == 'nvvm':
        return get_cuda_paths()['nvvm'].by
    elif lib == 'libdevice':
        return get_cuda_paths()['libdevice'].by
    else:
        dir_type = 'static_cudalib_dir' if static else 'cudalib_dir'
        return get_cuda_paths()[dir_type].by


def test():
    """Test library lookup.  Path info is printed to stdout.
    """
    failed = False

    # Check for the driver
    try:
        dlloader, candidates = locate_driver_and_loader()
        print('Finding driver from candidates:')
        for location in candidates:
            print(f'\t{location}')
        print(f'Using loader {dlloader}')
        print('\tTrying to load driver', end='...')
        dll, path = load_driver(dlloader, candidates)
        print('\tok')
        print(f'\t\tLoaded from {path}')
    except CudaSupportError as e:
        print(f'\tERROR: failed to open driver: {e}')
        failed = True

    # Find the absolute location of the driver on Linux. Various driver-related
    # issues have been reported by WSL2 users, and it is almost always due to a
    # Linux (i.e. not- WSL2) driver being installed in a WSL2 system.
    # Providing the absolute location of the driver indicates its version
    # number in the soname (e.g. "libcuda.so.530.30.02"), which can be used to
    # look up whether the driver was intended for "native" Linux.
    if sys.platform == 'linux' and not failed:
        pid = os.getpid()
        mapsfile = os.path.join(os.path.sep, 'proc', f'{pid}', 'maps')
        try:
            with open(mapsfile) as f:
                maps = f.read()
        # It's difficult to predict all that might go wrong reading the maps
        # file - in case various error conditions ensue (the file is not found,
        # not readable, etc.) we use OSError to hopefully catch any of them.
        except OSError:
            # It's helpful to report that this went wrong to the user, but we
            # don't set failed to True because this doesn't have any connection
            # to actual CUDA functionality.
            print(f'\tERROR: Could not open {mapsfile} to determine absolute '
                  'path to libcuda.so')
        else:
            # In this case we could read the maps, so we can report the
            # relevant ones to the user
            locations = set(s for s in maps.split() if 'libcuda.so' in s)
            print('\tMapped libcuda.so paths:')
            for location in locations:
                print(f'\t\t{location}')

    # Checks for dynamic libraries
    libs = 'nvvm nvrtc cudart'.split()
    for lib in libs:
        path = get_cudalib(lib)
        print('Finding {} from {}'.format(lib, _get_source_variable(lib)))
        print('\tLocated at', path)

        try:
            print('\tTrying to open library', end='...')
            open_cudalib(lib)
            print('\tok')
        except OSError as e:
            print('\tERROR: failed to open %s:\n%s' % (lib, e))
            failed = True

    # Check for cudadevrt (the only static library)
    lib = 'cudadevrt'
    path = get_cudalib(lib, static=True)
    print('Finding {} from {}'.format(lib, _get_source_variable(lib,
                                                                static=True)))
    print('\tLocated at', path)

    try:
        print('\tChecking library', end='...')
        check_static_lib(path)
        print('\tok')
    except FileNotFoundError as e:
        print('\tERROR: failed to find %s:\n%s' % (lib, e))
        failed = True

    # Check for libdevice
    where = _get_source_variable('libdevice')
    print(f'Finding libdevice from {where}')
    path = get_libdevice()
    print('\tLocated at', path)

    try:
        print('\tChecking library', end='...')
        check_static_lib(path)
        print('\tok')
    except FileNotFoundError as e:
        print('\tERROR: failed to find %s:\n%s' % (lib, e))
        failed = True

    return not failed
