//
// Copyright 2013,2015-2016 Ettus Research LLC
// Copyright 2018 Ettus Research, a National Instruments Company
//
// SPDX-License-Identifier: GPL-3.0-or-later
//

#include <uhdlib/transport/nirio/nirio_driver_iface.h>
#include <process.h>

#define NIRIO_IOCTL_MAP_MEMORY    \
    CTL_CODE(FILE_DEVICE_UNKNOWN, \
        0xF00,                    \
        METHOD_BUFFERED,          \
        (FILE_READ_ACCESS | FILE_WRITE_ACCESS))
#define NIRIO_IOCTL_UNMAP_MEMORY  \
    CTL_CODE(FILE_DEVICE_UNKNOWN, \
        0xF01,                    \
        METHOD_BUFFERED,          \
        (FILE_READ_ACCESS | FILE_WRITE_ACCESS))

namespace nirio_driver_iface {

nirio_status rio_open(const std::string& device_path, rio_dev_handle_t& device_handle)
{
    device_handle = CreateFileA(device_path.c_str(),
        GENERIC_READ | GENERIC_WRITE,
        FILE_SHARE_READ | FILE_SHARE_WRITE,
        NULL, /* default security */
        OPEN_EXISTING,
        FILE_FLAG_OVERLAPPED,
        NULL /* template file */);

    return (device_handle == INVALID_HANDLE_VALUE) ? NiRio_Status_InvalidParameter
                                                   : NiRio_Status_Success;
}

void rio_close(rio_dev_handle_t& device_handle)
{
    ::CloseHandle(device_handle);
    device_handle = INVALID_HANDLE_VALUE;
}

bool rio_isopen(rio_dev_handle_t device_handle)
{
    return (device_handle != INVALID_HANDLE_VALUE);
}

nirio_status rio_ioctl(rio_dev_handle_t device_handle,
    uint32_t ioctl_code,
    const void* write_buf,
    size_t write_buf_len,
    void* read_buf,
    size_t read_buf_len)
{
    if (!rio_isopen(device_handle))
        return NiRio_Status_ResourceNotInitialized;

    /* Note, if the file handle was opened with the OVERLAPPED flag,  you must
     * supply an OVERLAPPED structure to ReadFile, WriteFile, and
     * DeviceIoControl, even when doing synchronous IO. */
    OVERLAPPED zeroedOverlapped = {0};
    DWORD outLen                = 0;

    if (!(DeviceIoControl(device_handle,
            ioctl_code,
            const_cast<void*>(write_buf),
            static_cast<DWORD>(write_buf_len),
            read_buf,
            static_cast<DWORD>(read_buf_len),
            &outLen,
            &zeroedOverlapped))) {
        UHD_UNUSED(int_fast32_t lastError) = GetLastError();
        return NiRio_Status_SoftwareFault;
    }

    return NiRio_Status_Success;
}

unsigned int __stdcall memory_map_thread_routine(void* context)
{
    rio_mmap_threadargs_t* args = (rio_mmap_threadargs_t*)context;
    args->status                = rio_ioctl(args->device_handle,
        NIRIO_IOCTL_MAP_MEMORY,
        &(args->params),
        sizeof(args->params),
        NULL,
        0);
    if (nirio_status_fatal(args->status)) {
        SetEvent(reinterpret_cast<HANDLE>(args->params.map_ready_event_handle));
    }
    return 0;
}

nirio_status rio_mmap(rio_dev_handle_t device_handle,
    uint16_t memory_type,
    size_t size,
    bool writable,
    rio_mmap_t& map)
{
    if (!rio_isopen(device_handle))
        return NiRio_Status_ResourceNotInitialized;

    access_mode_t access_mode = writable ? ACCESS_MODE_WRITE : ACCESS_MODE_READ;

    uint64_t mapped_addr                     = 0;
    map.map_thread_args.device_handle        = device_handle;
    map.map_thread_args.status               = NiRio_Status_Success;
    map.map_thread_args.params.memoryType    = memory_type;
    map.map_thread_args.params.size          = (uint32_t)size;
    map.map_thread_args.params.mapped_va_ptr = reinterpret_cast<uintptr_t>(&mapped_addr);
    map.map_thread_args.params.access_mode   = (uint8_t)access_mode;
    HANDLE map_ready_event_handle            = CreateEventA(NULL, TRUE, FALSE, NULL);
    if (map_ready_event_handle == NULL) {
        map.addr = NULL;
        return NiRio_Status_SoftwareFault;
    }
    map.map_thread_args.params.map_ready_event_handle =
        reinterpret_cast<uint64_t>(map_ready_event_handle);
    map.map_thread_handle = (HANDLE)_beginthreadex(
        NULL, 0, memory_map_thread_routine, &(map.map_thread_args), 0, NULL);

    nirio_status status = NiRio_Status_Success;
    if (map.map_thread_handle == NULL) {
        map.addr = NULL;
        return NiRio_Status_SoftwareFault;
    } else {
        WaitForSingleObject(map_ready_event_handle, INFINITE);
        map.addr = reinterpret_cast<void*>(mapped_addr);
        if (map.addr == NULL) {
            WaitForSingleObject(map.map_thread_handle, INFINITE);
            CloseHandle(map.map_thread_handle);
            nirio_status_chain(map.map_thread_args.status, status);
        }
    }
    CloseHandle(map_ready_event_handle);
    return status;
}

nirio_status rio_munmap(rio_mmap_t& map)
{
    if (!rio_isopen(map.map_thread_args.device_handle))
        return NiRio_Status_ResourceNotInitialized;

    nirio_status status = NiRio_Status_Success;
    if (map.addr != NULL) {
        uint64_t mapped_addr = reinterpret_cast<uintptr_t>(map.addr);
        status               = rio_ioctl(map.map_thread_args.device_handle,
            NIRIO_IOCTL_UNMAP_MEMORY,
            &mapped_addr,
            sizeof(mapped_addr),
            NULL,
            0);
        if (nirio_status_not_fatal(status)) {
            WaitForSingleObject(map.map_thread_handle, INFINITE);
        }
        CloseHandle(map.map_thread_handle);
        map.addr = NULL;
    }
    return status;
}

} // namespace nirio_driver_iface
