/*
 * File				: main.c
 * Author			: David Patry
 * Description		: Read serial output from modem and log it in a txt file
 * Created on 		: June, 2026
 *
 */

// TODO : Do not include NON-ASCII char at the begining of the file

// TODO : Have the filename of the log to be the MAC address of the current device under test
//      : Open the serial port
//      : Perform a reset of device under test

// TODO : Have the full config report to be in the log file
// TODO : Do hard drive disk monitoring
// TODO : Integrate API calls of EMQX and Hologram

// DONE : SQLinkLogger to be able to pick up new devices during run time and setup automaticly
// DONE : Each SQ-Link logs into its own file ({serial.txt})

#include <iostream>
#include <string>
#include <cstdint>
#include <sstream>
#include <cstdio>
#include <cstdlib>
#include <cstdint>
#include <cstring>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <termios.h>
#include <poll.h>
#include <dirent.h>
#include <sys/stat.h>
#include <sys/ioctl.h>

#define PRINT_LOGS      0

// General device config
#define MAX_DEVICES         32
#define POLL_TIMEOUT_MS     500
#define BUF_SIZE            512
#define MAX_LOG_PATH_SIZE   80

using namespace std;

typedef struct {
	int       deviceNumber;	    // serial number of connected device 
	int		  fd;				    // device's file descriptor
	int		  baud;			    // Number of symbols per seconds
	char      mac[6];			    // MAC Address of the ESP32S3 wifi interface
    char      serial[122];	    // {serial} parameter of the USB chip of ESP32S3
    char      fullpath[512];      // /dev/ESP32S3_{serial} to access serial port
    char      logFilePath[MAX_LOG_PATH_SIZE];    // /home/recomputer/server/Logs/{serial}.txt
    FILE      *log;               // File object of this device's log file

    // Serial & System
    string    installedSystem;
    string    serialDriver;
    uint32_t  uartBaudrate;
    uint8_t   uartTxPin;
    uint8_t   uartRxPin;

    // Uplink & Versions
    string   sqLinkUplink;
    string   cellProvider;
    string   simIccid;
    string   sqLinkImei;
    string   sqLinkFirmware;
    string   sqLinkHardware;

    // MQTT Configuration
    string   mqttTopic;
    string   mqttClientId;
    string   mqttBrokerUrl;
    uint16_t mqttBrokerPort;

}DeviceInfo; // 4 + 4 + 4 + 6 + 126 + 256 + 80 = 480 bytes
// Current filename format : {serial}.txt
// Desired filename format : {serial}_{mac}_{deviceNumber}.txt

// General device manager for connected, logging devices
uint32_t numberOfDevice = 0;
int device_count = 0;
DeviceInfo deviceManager[MAX_DEVICES];

/**
 * Searches for an ESP32S3 device in /dev/
 * @param deviceid   Buffer to store the part after /dev/ESP32S3_ (e.g. "ABC123")
 * @param id_size    Size of the deviceid buffer
 * @param fullPath   Buffer to store the full device path (e.g. "/dev/ESP32S3_ABC123")
 * @param path_size  Size of the fullPath buffer
 * @return 0 on success, -1 on failure
 */
int searchForDevices(char* deviceid, size_t id_size, char* fullPath, size_t path_size);
int set_interface_attribs(int fd, int speed);
int serial_hangup(int fd, int drop_ms);             // Drop DTR for 'drop_ms' milliseconds (like minicom Hang Up)
bool parseSQLinkInit(const string& log, DeviceInfo& info);

int open_serial(const char *path);
void scan_for_new_devices(void);
void remove_device(int index);
void handle_device_read(int index);

// ==================== Function Prototypes ====================
//int searchForDevices_once(char *deviceid, size_t id_size, char *fullPath, size_t path_size); // kept for compatibility


// Folders name and paths
const char* serverPath		  = "/home/recomputer/server";
const char* logDirPath  	  = "/home/recomputer/server/logs/";

// Files
const char* defaultLogFile    = "/home/recomputer/server/Logs/SQLink.txt";

// Device
const char* defaultDevicePath = "/dev/ESP32S3_5A7A048466";


int main(int argc, char** argv){

	char devicePort[64] = {0};
    char device_id[128] = {0};
    char full_path[256] = {0};
	char buf[256] = {0};
    char keyboard;
    struct pollfd fds[MAX_DEVICES];

    // Add keyboard (stdin)
    fds[0].fd = STDIN_FILENO;
    fds[0].events = POLLIN;
    device_count++;

	mkdir(logDirPath, 0777);
    chmod(logDirPath, 0777);

/*
	if (argc > 1)
		memcpy(devicePort, argv[1], strlen(argv[1])+1);
	else
		memcpy(devicePort, defaultDevicePath, strlen(defaultDevicePath)+1);
*/
    printf("GMR Safety Modem Logger!\n");  

	while (1) {

		// collect deviceInfo of devices enumerated as /dev/ESP32S3_*
		// Compare with existing device list whether to add a new device or continue
		scan_for_new_devices();

        // Populate poll structure
        for (int i = 1; i < device_count; i++) {
            fds[i].fd = deviceManager[i].fd;
            fds[i].events = POLLIN;
            fds[i].revents = 0;
        }

        int ret = poll(fds, device_count, POLL_TIMEOUT_MS);
        if (ret < 0) {
            if (errno != EINTR) perror("poll");
            continue;
        }

        // Process devices that have data
        for (int i = 1; i < device_count; i++) {
            if (fds[i].revents & POLLIN) {
                handle_device_read(i);
            }
            if (fds[i].revents & (POLLHUP | POLLERR | POLLNVAL)) {
                printf("[DISCONNECT] %s\n", deviceManager[i].fullpath);
                remove_device(i);
            }
        }

    }


    for (int i = 0; i < device_count; i++) {
	    close(deviceManager[i].fd);
    }

	return 0;
}


// ==================== Scan /dev for new ESP32S3 devices ====================
void scan_for_new_devices(void) {
    DIR *deviceDir = opendir("/dev");
    if (!deviceDir) return;

    struct dirent *entry;
    while ((entry = readdir(deviceDir)) != NULL) {
        // Only stay in loop for devices with ESP32S3_ prefix
        if (strncmp(entry->d_name, "ESP32S3_", 8) != 0)
            continue;

        char fullDevicePath[256];
        snprintf(fullDevicePath, sizeof(fullDevicePath), "/dev/%s", entry->d_name);

        // Check if current /dev/ESP32S3_ device has already an instance of its path in the deviceManager 
        int already_open = 0;
        for (int i = 0; i < device_count; i++) {
            if (strcmp(deviceManager[i].fullpath, fullDevicePath) == 0) {
                already_open = 1;
                break;
            }
        }

        if(already_open)
            continue;
        
        // Attempt to open serial port of the current unlisted device
        int fd = open_serial(fullDevicePath);
        if (fd >= 0) {

            // Todo : replace fullpath with Mqtt client ID, MAC, ICCID 
            // Todo : some housekeeping to reuse memory 

            printf("[NEW DEVICE]    %s (fd=%d)\n", fullDevicePath, fd);
                    
            strncpy(deviceManager[device_count].fullpath, fullDevicePath, 256);
            strncpy(deviceManager[device_count].serial, entry->d_name + 8, 120); // after "ESP32S3_"
            deviceManager[device_count].fd = fd;
            deviceManager[device_count].deviceNumber = device_count + 1;
            

            strncat(deviceManager[device_count].logFilePath, logDirPath, MAX_LOG_PATH_SIZE - strlen(logDirPath));  
            strncat(deviceManager[device_count].logFilePath, deviceManager[device_count].serial, MAX_LOG_PATH_SIZE - strlen(deviceManager[device_count].serial));    
            strncat(deviceManager[device_count].logFilePath, ".txt\0",6);

            printf("[LOG FILE PATH] %s\n", deviceManager[device_count].logFilePath);
            printf("\n");

            // Open logging File
            deviceManager[device_count].log = fopen(deviceManager[device_count].logFilePath, "a"); 
            if(deviceManager[device_count].log == NULL){
                fprintf(stderr, "Error opening %s: %s\n", deviceManager[device_count].logFilePath, strerror(errno));
                close(deviceManager[device_count].fd);
                return;
            }

            // Send reset signal and extract the client ID
            serial_hangup(deviceManager[device_count].fd, 2000);
            device_count++;

        } else {
            // The unlisted ESP32S3_ couldnt be opened, verify if its still available in kernel, wait and reattempt, send DTR ?
            printf("[ERROR] Failed to open %s\n", fullDevicePath);
        }
        
    }
    closedir(deviceDir);
}

// ==================== Open serial port (non-blocking) ====================
int open_serial(const char *path) {
    int fd = open(path, O_RDWR | O_NOCTTY | O_NONBLOCK);
    if (fd < 0) {
        perror(path);
        return -1;
    }

    if (set_interface_attribs(fd, B115200) != 0) {
        close(fd);
        return -1;
    }
    return fd;
}


// ==================== Read from one device ====================
void handle_device_read(int index) {
    
    if(index >= MAX_DEVICES)
        return;

    char buf[BUF_SIZE];
    ssize_t n = read(deviceManager[index].fd, buf, sizeof(buf) - 1);

    if (n > 0) {
        buf[n] = '\0';

#if PRINT_LOGS
        printf("[%s] %s", deviceManager[index].fullpath, buf);
        fflush(stdout);
#endif

        // TODO: Write to per-device log file here
        fprintf(deviceManager[index].log, buf);
        //fprintf(deviceManager[index].fullpath, "%s", buf);
    }
    else if (n < 0) {
        if (errno == EIO || errno == ENODEV || errno == EBADF) {
            printf("[DISCONNECTED] %s\n", deviceManager[index].fullpath);
            remove_device(index);
        } else if (errno != EAGAIN && errno != EWOULDBLOCK) {
            perror("read");
        }
    }
}

void remove_device(int index) {
    close(deviceManager[index].fd);
    printf("[REMOVED] %s\n", deviceManager[index].fullpath);

    for (int i = index; i < device_count - 1; i++) {
        deviceManager[i] = deviceManager[i + 1];
    }
    device_count--;
}




int serial_hangup(int fd, int drop_ms)
{
    int mcs;   // modem control status

    if (ioctl(fd, TIOCMGET, &mcs) == -1) {
        perror("TIOCMGET");
        return -1;
    }

    mcs &= ~TIOCM_DTR;         // Drop DTR (clear the bit)
    if (ioctl(fd, TIOCMSET, &mcs) == -1) {
        perror("TIOCMSET (drop DTR)");
        return -1;
    }

    usleep(drop_ms * 1000);   // Wait drop_ms milliseconds

    mcs |= TIOCM_DTR;        // Raise DTR again
    if (ioctl(fd, TIOCMSET, &mcs) == -1) {
        perror("TIOCMSET (raise DTR)");
        return -1;
    }

    return 0;
}


int set_interface_attribs(int fd, int speed) {
    struct termios tty;
    if (tcgetattr(fd, &tty) != 0) {
        perror("tcgetattr");
        return -1;
    }

    cfsetospeed(&tty, speed);
    cfsetispeed(&tty, speed);

    // 8N1 (8 bits, no parity, 1 stop bit)
    tty.c_cflag = (tty.c_cflag & ~CSIZE) | CS8;     // 8-bit chars
    tty.c_iflag &= ~IGNBRK;                         // disable break processing
    tty.c_lflag = 0;                                // no signaling chars, no echo
    tty.c_oflag = 0;                                // no remapping, no delays

    tty.c_iflag &= ~(IXON | IXOFF | IXANY);         // shut off xon/xoff ctrl
    tty.c_cflag |= (CLOCAL | CREAD);                // ignore modem controls, enable reading
    tty.c_cflag &= ~(PARENB | PARODD);              // shut off parity
    tty.c_cflag &= ~CSTOPB;                         // 1 stop bit
    tty.c_cflag &= ~CRTSCTS;                        // no hardware flow control

    if (tcsetattr(fd, TCSANOW, &tty) != 0) {
        perror("tcsetattr");
        return -1;
    }
    return 0;
}


bool parseSQLinkInit(const string& log, DeviceInfo& info) {
    istringstream stream(log);
    string line;
    bool foundConfig = false;

    while (getline(stream, line)) {
        if (!line.empty() && line.back() == '\r')
            line.pop_back();

        if (line.find("SQ-Link Uplink") != string::npos)
            foundConfig = true;

        size_t colonPos = line.find(':');
        if (colonPos == string::npos) continue;

        string key = line.substr(0, colonPos);
        string value = line.substr(colonPos + 1);

        key.erase(0, key.find_first_not_of(" \t"));
        key.erase(key.find_last_not_of(" \t") + 1);
        value.erase(0, value.find_first_not_of(" \t"));
        value.erase(value.find_last_not_of(" \t") + 1);

        if (key.empty()) continue;

        if (key == "Installed System")      info.installedSystem = value;
        else if (key == "Serial Driver")    info.serialDriver = value;
        else if (key == "UART Baudrate")    info.uartBaudrate = stoul(value);
        else if (key == "UART TX Pin")      info.uartTxPin = static_cast<uint8_t>(stoi(value));
        else if (key == "UART RX Pin")      info.uartRxPin = static_cast<uint8_t>(stoi(value));
        else if (key == "SQ-Link Uplink")   info.sqLinkUplink = value;
        else if (key == "Cell Provider")    info.cellProvider = value;
        else if (key == "SIMCard ICCID")    info.simIccid = value;
        else if (key == "SQ-Link IMEI")     info.sqLinkImei = value;
        else if (key == "SQ-Link Firmware") info.sqLinkFirmware = value;
        else if (key == "SQ-Link Hardware") info.sqLinkHardware = value;
        else if (key == "MQTT Topic")       info.mqttTopic = value;
        else if (key == "MQTT Client ID")   info.mqttClientId = value;
        else if (key == "MQTT Broker URL")  info.mqttBrokerUrl = value;
        else if (key == "MQTT Broker Port") info.mqttBrokerPort = static_cast<uint16_t>(stoi(value));
    }

    return foundConfig;   // Return true only if we saw the config section
}
