#define LOG_MODULE "aciomgr-manager"

#include <windows.h>

#include <stdatomic.h>

#include "aciomgr/manager-init.h"

#include "aciomgr/manager.h"

#include "acio/acio.h"

#include "aciodrv/device.h"
#include "util/array.h"
#include "util/log.h"

#define MAX_PORT_PATH_LENGTH 256

struct aciomgr_port_dispatcher {
    CRITICAL_SECTION cs;
    atomic_size_t references;
    struct aciodrv_device_ctx *device;
    char path[MAX_PORT_PATH_LENGTH];
    int baud;
    bool has_failure;
};

static void _aciomgr_setup_port_dispatcher(
    struct aciomgr_port_dispatcher *dispatcher, const char *path, int baud);
static void
_aciomgr_destroy_port_dispatcher(struct aciomgr_port_dispatcher *dispatcher);

// DLL-globals
static atomic_bool running;
static CRITICAL_SECTION mgr_cs;
static struct array active_ports;

static void _aciomgr_setup_port_dispatcher(
    struct aciomgr_port_dispatcher *dispatcher, const char *path, int baud)
{
    InitializeCriticalSection(&dispatcher->cs);

    strcpy(dispatcher->path, path);
    dispatcher->baud = baud;

    dispatcher->device = aciodrv_device_open_path(path, baud);
    dispatcher->has_failure = false;

    if (dispatcher->device == NULL) {
        log_info("Opening ACIO device on %s failed", path);
        dispatcher->has_failure = true;
    }

    dispatcher->references = 0;
}

static void
_aciomgr_destroy_port_dispatcher(struct aciomgr_port_dispatcher *dispatcher)
{
    aciodrv_device_close(dispatcher->device);
    DeleteCriticalSection(&dispatcher->cs);
}

void _aciomgr_init()
{
    if (running) {
        log_warning("_aciomgr_init called when already running");
        _aciomgr_fini();
    }
    InitializeCriticalSection(&mgr_cs);
    array_init(&active_ports);

    running = true;
}

void _aciomgr_fini()
{
    if (!running) {
        log_warning("_aciomgr_fini called when not running");
    }

    running = false;
    DeleteCriticalSection(&mgr_cs);
    array_fini(&active_ports);
}

void aciomgr_set_loggers(
    log_formatter_t misc,
    log_formatter_t info,
    log_formatter_t warning,
    log_formatter_t fatal)
{
    log_to_external(misc, info, warning, fatal);
}

struct aciomgr_port_dispatcher *aciomgr_port_init(const char *path, int baud)
{
    if (!running) {
        log_warning("aciomgr_port_init: called when not running");
        return NULL;
    }

    if (strlen(path) >= MAX_PORT_PATH_LENGTH) {
        log_warning("aciomgr_port_init: path too long");
        return NULL;
    }

    struct aciomgr_port_dispatcher *entry;

    EnterCriticalSection(&mgr_cs);

    for (size_t i = 0; i < active_ports.nitems; i++) {
        entry = array_item(struct aciomgr_port_dispatcher, &active_ports, i);

        if (strcmp(entry->path, path) == 0) {
            // found
            if (entry->baud != baud) {
                log_warning(
                    "Device at %s opened with baud of %d, but requested %d",
                    path,
                    entry->baud,
                    baud);
            }
            goto done;
        }
    }

    entry = array_append(struct aciomgr_port_dispatcher, &active_ports);
    _aciomgr_setup_port_dispatcher(entry, path, baud);

done:
    if (!entry->has_failure) {
        entry->references++;
    }

    LeaveCriticalSection(&mgr_cs);

    if (entry->has_failure) {
        return NULL;
    }

    return entry;
}

void aciomgr_port_fini(struct aciomgr_port_dispatcher *dispatcher)
{
    if (!running) {
        log_warning("aciomgr_port_fini: called when not running");
        return;
    }

    EnterCriticalSection(&mgr_cs);
    dispatcher->references--;
    LeaveCriticalSection(&mgr_cs);

    if (dispatcher->references == 0) {
        _aciomgr_destroy_port_dispatcher(dispatcher);
    }
}

// this function don't require the lock
uint8_t aciomgr_get_node_count(struct aciomgr_port_dispatcher *dispatcher)
{
    return aciodrv_device_get_node_count(dispatcher->device);
}

// this function don't require the lock
bool aciomgr_get_node_product_ident(
    struct aciomgr_port_dispatcher *dispatcher,
    uint8_t node_id,
    char product[ACIOMGR_NODE_PRODUCT_CODE_LEN])
{
    return aciodrv_device_get_node_product_ident(
        dispatcher->device, node_id, product);
}

bool aciomgr_port_submit_packet(
    struct aciomgr_port_dispatcher *dispatcher,
    struct ac_io_message *msg,
    int max_resp_size)
{
    // CS's although lightweight, may still be a burden, short circuit
    if (dispatcher->references > 1) {
        EnterCriticalSection(&dispatcher->cs);
        bool response =
            aciodrv_send_and_recv(dispatcher->device, msg, max_resp_size);
        LeaveCriticalSection(&dispatcher->cs);
        return response;
    }

    return aciodrv_send_and_recv(dispatcher->device, msg, max_resp_size);
}

struct aciodrv_device_ctx *
aciomgr_port_checkout(struct aciomgr_port_dispatcher *dispatcher)
{
    if (dispatcher->references > 1) {
        EnterCriticalSection(&dispatcher->cs);
    }

    return dispatcher->device;
}

void aciomgr_port_checkin(struct aciomgr_port_dispatcher *dispatcher)
{
    if (dispatcher->references > 1) {
        LeaveCriticalSection(&dispatcher->cs);
    }
}
