/****************************************************************************
 * example.c 
 * Basic console application written in C to use NixUniversalSDK.Wrapper.dll
 * 
 * Created by James Strack on 2024-12-09.
 * Copyright © 2023-2024 Nix Sensor Ltd. All rights reserved.
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "NixUniversalSDK.Wrapper.h"

/*****************************************************************************
 * CONSTANTS 
 */
#define LOG_PREFIX "## "
#define STRING_ON "ON"
#define STRING_OFF "OFF"
#define STRING_TRUE "true"
#define STRING_FALSE "false"
#define DEFAULT_SEARCH_DURATION_MS 20000
#define DEFAULT_REPORT_INTERVAL_MS 250
#define DEFAULT_LISTED_DEVICE_COUNT 10
#define MAX_CONSOLE_READ_SIZE 512

// NixUniversalSDK license codes
// Replace these fields with the proper values for your license
#define LICENSE_OPTIONS "TODO_replace_with_your_license_options"
#define LICENSE_SIGNATURE "TODO_replace_with_your_license_signature"

/****************************************************************************
 * GLOBAL VARIABLES 
 */
static bool m_scanner_has_init = false;
static bool m_scanner_is_running = false;
static bool m_device_is_busy = false;

/*****************************************************************************
 * FUNCTION PROTOTYPES
 */
void InitScanner(void);
void StartMenu(void);
void ListAllDevicesMenu(void);
void ListUsbDevicesMenu(void);
void ConnectMenu(void);
void ConnectedMenu(void);
void PrintDeviceInfo(void);
void RunMeasurementMenu(void);
void PrintMeasurementInfo(void);
void OptionsMenu(void);
void RunFieldCalibration(void);
void PrintLicenseInfo(void);
void ChangeLicenseKey(void);
const char* LicenseManagerStateToString(int32_t stateVal);
const char* DeviceScannerStateToString(int32_t stateVal);

/****************************************************************************
 * FUNCTION DEFINITIONS
 */

/* @brief Read user input from stdin
 * @return string length read from stdin
 */
int console_read_line(char* p_buffer, size_t buffer_size) {    
    // Read string from stdin
    fgets(p_buffer, buffer_size, stdin);

    // Remove trailing newline
    p_buffer[strcspn(p_buffer, "\n")] = 0;

    // Return string length
    return strlen(p_buffer);
}

int main(void) {
    // Activate the license
    License_Activate(LICENSE_OPTIONS, LICENSE_SIGNATURE);

    // Initialize scanner
    InitScanner();

    // Enter the main menu
    StartMenu();

    // Nothing to do ...
    return 0;
}

void OnScannerCreated(const char* senderName, int32_t status) {
    printf(
        "%sOnScannerCreated with state %d (%s)\n", 
        LOG_PREFIX, 
        status, 
        DeviceScannerStateToString(status));
    m_scanner_has_init = true;
}

void InitScanner(void) {
    // Register callback for creation
    Scanner_RegisterCreated(*OnScannerCreated);

    // Reset the scanner
    m_scanner_has_init = false;
    Scanner_Reset();

    // Wait for DeviceScanner to initialize
    while (!m_scanner_has_init) {}
}

void StartMenu(void) {
    printf("\n");
    printf("Start Menu/Enter an option:\n");
    printf("1: Run search for Bluetooth and USB devices\n");
    printf("2: List USB devices only\n");
    printf("3: Reset the `DeviceScanner`\n");
    printf("4: Display license info\n");
    printf("5: Change license key\n");
    printf("Q: Quit\n");
    printf("> ");

    char input[MAX_CONSOLE_READ_SIZE];
    console_read_line(input, sizeof(input));

    switch (input[0]) {
        case '1':
            ListAllDevicesMenu();
            break;
        case '2':
            ListUsbDevicesMenu();
            break;
        case '3':
            InitScanner();
            StartMenu();
            break;
        case '4':
            PrintLicenseInfo();
            StartMenu();
            break;
        case '5':
            ChangeLicenseKey();
            break;
        case 'q':
        case 'Q':
            printf("Goodbye\n");
            break;
        default:
            printf("Invalid option!\n");
            StartMenu();
            break;
    }
}

void OnScannerStarted(const char* senderName) {
    printf("%sOnScannerStarted\n", LOG_PREFIX);
}
void OnScannerStopped(const char* senderName) {
    printf("%sOnScannerStopped\n", LOG_PREFIX);
    m_scanner_is_running = false;

    // Clear result callback
    Scanner_RegisterScanResult(NULL);
}
void OnScanResult(const char* senderName, const char* results) {
    printf("%s%s\n\n", LOG_PREFIX, results);
}

void ListAllDevicesMenu(void) {
    int32_t scannerState = Scanner_GetState();
    if (scannerState == DeviceScannerState_Idle) {
        // Register callback
        Scanner_RegisterStarted(*OnScannerStarted);
        Scanner_RegisterStopped(*OnScannerStopped);
        Scanner_RegisterScanResult(*OnScanResult);

        char user_input[MAX_CONSOLE_READ_SIZE];

        // User input: search duration
        int32_t scan_duration = DEFAULT_SEARCH_DURATION_MS;
        printf("\n");
        printf("Enter search duration in ms...\n");
        printf("> ");
        console_read_line(user_input, sizeof(user_input));
        sscanf(user_input, "%d", &scan_duration);


        // User input: report interval
        int32_t report_interval = DEFAULT_REPORT_INTERVAL_MS;
        printf("\n");
        printf("Enter max report interval in ms...\n");
        printf("> ");
        console_read_line(user_input, sizeof(user_input));
        sscanf(user_input, "%d", &report_interval);

        // User input: report count
        int32_t max_report_count = DEFAULT_LISTED_DEVICE_COUNT;
        printf("\n");
        printf("Enter max number of reported devices...\n");
        printf("> ");
        console_read_line(user_input, sizeof(user_input));
        sscanf(user_input, "%d", &max_report_count);

        // Start the scanner
        m_scanner_is_running = true;
        Scanner_Start(scan_duration, report_interval, max_report_count);

        // Actual scanner state can also be polled via Scanner_GetState()
        while(m_scanner_is_running) {}

        // Ask the user to enter a device id
        ConnectMenu();
    } else {
        // Scanner is not idle ... cannot start a scan
        printf(
            "Error: cannot search because the DeviceScannerState is %d (%s)\n", 
            scannerState, 
            DeviceScannerStateToString(scannerState));
        StartMenu();
    }
}

void OnScanResultUsb(const char* senderName, const char* results) {
    printf("%sUSB device listing:\n", LOG_PREFIX);
    printf("%s\n", results);
    m_scanner_is_running = false;
}

void ListUsbDevicesMenu(void) {
    m_scanner_is_running = true;

    // Register callback
    Scanner_RegisterScanResult(*OnScanResultUsb);    

    // List USB devices and wait for callback
    Scanner_ListUsbDevices();
    while (m_scanner_is_running) {}

    // Ask the user to enter a device id
    ConnectMenu();
}

void OnConnected(const char* senderName) {
    printf("%sOnConnected\n", LOG_PREFIX);
    m_device_is_busy = false;

}

void OnDisconnected(const char* senderName, int32_t status) {
    printf("%sOnDisconnected with status %d\n", LOG_PREFIX, status);
    m_device_is_busy = false;    
}

void OnBatteryStateChanged(const char* senderName, int32_t new_state) {
    printf("%sOnBatteryStateChanged to %d%%\n", LOG_PREFIX, new_state);
}

void OnExtPowerStateChanged(const char* senderName, bool new_state) {
    printf("%sExtPowerStateChanged to %s\n", 
        LOG_PREFIX, 
        new_state ? STRING_TRUE : STRING_FALSE);
}

void OnCommandCompleted(const char* senderName, int32_t status) {
    printf("%sOnCommandCompleted (%s) with status %d\n", 
        LOG_PREFIX, 
        senderName, 
        status);

    // Clear busy state
    m_device_is_busy = false;
}

void ConnectMenu(void) {
    printf("\n");
    printf("Enter a device ID to open a connection, ");
    printf("or 'Q' to return to the Start Menu\n");
    printf("> ");

    char entered_id[MAX_CONSOLE_READ_SIZE];
    console_read_line(entered_id, sizeof(entered_id));

    if (entered_id[0] == 'q' || entered_id[0] == 'Q') {
        StartMenu();
    } else if (!Scanner_HasFoundDevice(entered_id)) {
        printf("Error: '%s' has not been found yet. Try again.", 
            entered_id);
        ConnectMenu();
    } else {
        // Register callbacks
        Device_RegisterConnected(*OnConnected);
        Device_RegisterDisconnected(*OnDisconnected);
        Device_RegisterBatteryStateChanged(*OnBatteryStateChanged);
        Device_RegisterExtPowerStateChanged(*OnExtPowerStateChanged);
        Device_RegisterCommandCompleted(*OnCommandCompleted);
        
        // Start connection process
        printf("Connecting to %s...\n", entered_id);
        m_device_is_busy = true;
        Device_Connect(entered_id);

        // Wait for callback
        while (m_device_is_busy) {}

        // Go to next menu
        if (Device_GetState() != DeviceState_Idle) {
            // Connection was unsuccessful
            printf("Error: could not connect to '%s'. Try again.", 
                entered_id);
            ConnectMenu();
        } else {
            ConnectedMenu();
        }
    }
}

void ConnectedMenu(void) {
    printf("\n");
    printf("Connected Menu/%s (%s) has state %d\n", 
        Device_GetId(), 
        Device_GetName(), 
        Device_GetState());

    printf("Enter option:\n");
    printf("1. Display device info\n");
    printf("2. Run measurements for all supported modes\n");

    if (Device_HasOptions()) {
        printf("3. Toggle device options\n");
    }

    if (Device_GetSupportsFieldCalibration()) {
        printf("4. Run field calibration\n");
    }

    printf("B. Disconnect\n");
    printf("> ");

    char input[MAX_CONSOLE_READ_SIZE];
    console_read_line(input, sizeof(input));

    // Check device state before executing command
    if (Device_GetState() == DeviceState_Idle) {
        switch (input[0]) {
            case '1':
                PrintDeviceInfo();
                ConnectedMenu();
                break;
            case '2':
                RunMeasurementMenu();
                break;
            case '3':
                OptionsMenu();
                break;
            case '4': 
                RunFieldCalibration();
                ConnectedMenu();
                break;
            case 'b':
            case 'B':
                Device_Disconnect();
                StartMenu();
                break;
            default:
                printf("Invalid option!\n");
                ConnectedMenu();
                break;
        }    
    } else {
        printf("Error: device is not ready ... ");
        printf("returning to Start Menu...\n");
        Device_Disconnect();
        StartMenu();
    }    
}

void PrintDeviceInfo(void) {
    printf("\n");
    printf("Device Info\n");
    printf("-----------\n");

    // Device name
    printf("Device name: %s\n", Device_GetName());

    // Hardware version
    printf("Hardware version: %s\n", Device_GetHardwareVersion());

    // Firmware version
    printf("Firmware version: %s\n", Device_GetFirmwareVersion());

    // Serial number
    printf("Serial number: %s\n", Device_GetSerialNumber());

    // Battery level
    int battery_level = Device_GetBatteryLevel();
    char battery_string[12] = "unavailable";
    if (battery_level > 0) {
        sprintf(battery_string, "%d", battery_level);
    }
    printf("Battery level: %s\n", battery_string);

    // External power connected
    printf("External power connected: %s\n", 
        Device_GetExtPowerState() ? STRING_TRUE : STRING_FALSE);

    // Optional device properties
    // Scan counter
    uint32_t scan_count = Device_GetScanCount();
    if (scan_count > 0) {
        printf("Scan counter: %lu\n", scan_count);
    }

    // Field calibration info
    if (Device_GetSupportsFieldCalibration()) {
        printf("Field calibration date (Java ticks / ms): %llu\n", 
            Device_GetReferenceJavaTicks());
        printf("Field calibration due: %s\n", 
            Device_GetFieldCalibrationDue() ? STRING_TRUE : STRING_FALSE);
    }
}

void RunMeasurementMenu(void) {
    // Run measurement command
    printf("Running measurement...\n");
    m_device_is_busy = true;
    Device_Measure("");

    // Wait for command to complete
    while (m_device_is_busy) {}

    // Fetch and display measurement data if successful
    if (Measurement_GetLastStatus() == CommandStatus_Success) {
        PrintMeasurementInfo();
    } else {
        printf("Error: measurement exited with status %d\n", 
            Measurement_GetLastStatus());
    }

    // Return to connected menu
    ConnectedMenu();
}

void PrintMeasurementInfo(void) {
    printf("\n");
    printf("Enter measurement mode to query (available modes: %s)\n", 
        Measurement_GetLastModesJson());
    printf("> ");
    char mode_input[MAX_CONSOLE_READ_SIZE];
    console_read_line(mode_input, sizeof(mode_input));

    printf("Enter reference white for color data (available: %s)\n", 
        Device_GetSupportedReferences());
    printf("> ");
    char ref_input[MAX_CONSOLE_READ_SIZE];
    console_read_line(ref_input, sizeof(ref_input));

    // Print results
    printf("\n");
    printf("\n%s metadata:\n%s\n", 
        mode_input, 
        Measurement_GetLastMetadataJson(mode_input));
    printf("\n%s spectral:\n%s\n", 
        mode_input, 
        Measurement_GetLastSpectralJson(mode_input));
    printf("\n%s color (%s):\n%s\n", 
        mode_input, 
        ref_input, 
        Measurement_GetLastColorJson(mode_input, ref_input, "CIELAB"));
    printf("\n%s sRGB/HEX (%s):\n%s\n", 
        mode_input, 
        ref_input, 
        Measurement_GetLastHexCode(mode_input, ref_input));

    printf("\n");
    printf("Enter option:\n");
    printf("1. Request more details for last measurement\n");
    printf("2. Return to Connected Menu\n");
    printf("> ");

    char input[MAX_CONSOLE_READ_SIZE] = {0};
    console_read_line(input, sizeof(input));

    switch (input[0]) {
        case '1':
            PrintMeasurementInfo();
            break;
        default:            
            break;
    }
}

void OptionsMenu(void) {
    printf("\n");

    if (Device_GetState() != DeviceState_Idle) {
        printf("Options Menu/Error: device is not busy ... ");
        ConnectedMenu();
        return;
    }

    printf("Options Menu/Select an option to toggle its state:\n");
    if (Device_GetSupportsRgbFeedback()) {
        printf("1. Toggle RGB feedback (currently %s)\n", 
            Device_GetRgbFeedbackEnabled() ? STRING_ON : STRING_OFF);
    }
    if (Device_GetSupportsHapticFeedback()) {
        printf("2. Toggle haptic feedback (currently %s)\n", 
            Device_GetHapticFeedbackEnabled() ? STRING_ON : STRING_OFF);
    }
    if (Device_GetSupportsFieldCalibration()) {
        printf("3. Toggle tile normalization (currently %s)\n", 
            Device_GetFieldCalibrationEnabled() ? STRING_ON : STRING_OFF);
    }
    if (Device_GetSupportsTemperatureCompensation()) {
        printf("4. Toggle temperature compensation (currently %s)\n", 
            Device_GetTemperatureCompensationEnabled() ? 
                STRING_ON : 
                STRING_OFF);
    }
    printf("B. Go back to Connected Menu\n");
    printf("> ");

    char input[MAX_CONSOLE_READ_SIZE];
    console_read_line(input, sizeof(input));
    switch (input[0]) {
        case 'b':
        case 'B':
            // Return to connected menu
            ConnectedMenu();                        
            return;
        case '1':
            // Toggle RGB feedback state
            m_device_is_busy = true;
            Device_SetRgbFeedbackEnabled(
                !Device_GetRgbFeedbackEnabled());            
            break;
        case '2':
            // Toggle haptic feedback state
            m_device_is_busy = true;
            Device_SetHapticFeedbackEnabled(
                !Device_GetHapticFeedbackEnabled());            
            break;
        case '3':
            // Toggle tile normalization state
            m_device_is_busy = true;
            Device_SetFieldCalibrationEnabled(
                !Device_GetFieldCalibrationEnabled());            
            break;
        case '4':
            // Toggle temperature compensation state
            m_device_is_busy = true;
            Device_SetTemperatureCompensationEnabled(
                !Device_GetTemperatureCompensationEnabled());            
            break;
        default:
            printf("Invalid option!\n");
            break;
    }
    while (m_device_is_busy) {}
    OptionsMenu();
}

void RunFieldCalibration() {    
    printf("Enter tile string: ");
    char tile_string[MAX_CONSOLE_READ_SIZE];
    console_read_line(tile_string, sizeof(tile_string));    

    // Check entered tile string
    if (!Device_IsTileStringValid(tile_string)) {
        printf("Error: Entered tile string is invalid\n");        
    } else {
        printf("Tile string is valid. ");
        printf("Place the unit on the reference tile scanning area.\n");
        printf("Press any key to continue...\n");

        // Wait for input
        getch();

        // Run field calibration command
        printf("Running calibration command...\n");
        m_device_is_busy = true;
        Device_RunFieldCalibration(tile_string);

        // Wait for command to finish
        while (m_device_is_busy) {}

        // Display status
        printf("Field calibration result had status %d\n", 
            Device_GetLastCalibrationStatus());
    }
}

void PrintLicenseInfo() {
    printf("\n");
    printf("License Info\n");
    printf("------------\n");

    // License state
    int licenseState = License_GetState();
    printf(
        "State: %d (%s)\n", 
        licenseState, 
        LicenseManagerStateToString(licenseState));

    // UUID
    printf("UUID: %s\n", License_GetUuid());

    // Expiry
    printf("Expiry (Java ticks): %llu\n", License_GetExpiryJavaTicks());

    // Allocations
    printf("Allocation codes: %s\n", License_GetAllocations());

    // Allowed devices
    printf("Device types: %s\n", License_GetAllowedDeviceTypes());

    // Features
    printf("Features: %s\n", License_GetFeatures());

    // SDK version
    printf("NixUniversalSDK version: %s\n", License_GetLibraryVersion());

    // Wrapper version
    printf("Wrapper version: %s\n", License_GetLibraryWrapperVersion());
}

void ChangeLicenseKey(void) {
    char options[MAX_CONSOLE_READ_SIZE];
    char signature[MAX_CONSOLE_READ_SIZE];

    // Prompt the user for new license key info
    printf("\n");
    printf("Enter options string, or blank for default...\n");
    printf("> ");
    console_read_line(options, sizeof(options));
    if (strlen(options) == 0) strcpy(options, LICENSE_OPTIONS);

    printf("\n");
    printf("Enter signature string, or blank for default...\n");
    printf("> ");
    console_read_line(signature, sizeof(signature));
    if (strlen(signature) == 0) strcpy(signature, LICENSE_SIGNATURE);

    printf("\n");
    printf("Using:\n");
    printf("Options: '%s'\n", options);
    printf("Signature: '%s'\n", signature);

    // Run the activation command
    int32_t statusVal = License_Activate(options, signature);

    // Show the new license status
    PrintLicenseInfo();
    printf("\n");

    // Reset the DeviceScanner, since the feature set has changed
    InitScanner();

    // Return to the start menu
    StartMenu();
}

const char* LicenseManagerStateToString(int32_t stateVal) {
    switch (stateVal) {
        case LicenseManagerState_Inactive:
            return "Inactive";
        case LicenseManagerState_Active:
            return "Active";
        case LicenseManagerState_ErrorLicenseBadSignature:
            return "ErrorLicenseBadSignature";
        case LicenseManagerState_ErrorLicenseInvalidOptions:
            return "ErrorLicenseInvalidOptions";
        case LicenseManagerState_ErrorLicenseExpired:
            return "ErrorLicenseExpired";
        default:
            return "ErrorInternal";
    }
}

const char* DeviceScannerStateToString(int32_t stateVal) {
    switch (stateVal) {
        case DeviceScannerState_Cold:
            return "Cold";
        case DeviceScannerState_Idle:
            return "Idle";
        case DeviceScannerState_Scanning:
            return "Scanning";
        case DeviceScannerState_ErrorBluetoothPermissions:
            return "ErrorBluetoothPermissions";
        case DeviceScannerState_ErrorBluetoothDisabled:
            return "ErrorBluetoothDisabled";
        case DeviceScannerState_ErrorBluetoothUnavailable:
            return "ErrorBluetoothUnavailable";
        case DeviceScannerState_ErrorInvalidHardwareId:
            return "ErrorInvalidHardwareId";
        case DeviceScannerState_ErrorLicense:
            return "ErrorLicense";
        default:
            return "ErrorInternal";
    }
}