#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <dos.h>

#if defined(__WATCOMC__)
#include <i86.h>
#include <malloc.h>
#endif

#include "pastfetch.h"

#define HINT_COUNT 4

// Remove leading/trailing whitespace from a string in-place.
static void trim_whitespace(char *text) {
    char *start = text;
    char *end;

    if (!text) {
        return;
    }

    while (*start && isspace((unsigned char)*start)) {
        start++;
    }

    if (start != text) {
        memmove(text, start, strlen(start) + 1);
    }

    end = text + strlen(text);
    while (end > text && isspace((unsigned char)*(end - 1))) {
        end--;
    }
    *end = '\0';
}

// Detect PC DOS naming variants
static int indicates_pc_dos(const char *text) {
    if (!text || !*text) {
        return 0;
    }
    return str_has_ci(text, "PCDOS") || str_has_ci(text, "PC-DOS") || str_has_ci(text, "PC DOS") ||
           str_has_ci(text, "IBMDOS") || str_has_ci(text, "IBM DOS");
}

// Detect Novell DOS naming variants
static int indicates_novell_dos(const char *text) {
    if (!text || !*text) {
        return 0;
    }
    return str_has_ci(text, "NWDOS") || str_has_ci(text, "NOVELL") || str_has_ci(text, "NETWARE");
}

// Detect Concurrent DOS naming variants
static int indicates_concurrent_dos(const char *text) {
    if (!text || !*text) {
        return 0;
    }
    if (str_has_ci(text, "CONCURRENT") || str_has_ci(text, "C-DOS") || str_has_ci(text, "C DOS")) {
        return 1;
    }

    {
        const char *match = str_find_ci(text, "CDOS");
        while (match) {
            char before = (match == text) ? '\0' : match[-1];
            char after = match[4];
            if (!isalnum((unsigned char)before) && !isalnum((unsigned char)after)) {
                return 1;
            }
            match = str_find_ci(match + 4, "CDOS");
        }
    }
    return 0;
}

// Detect 86-DOS naming variants in a text hint
static int indicates_86_dos(const char *text) {
    if (!text || !*text) {
        return 0;
    }
    return str_has_ci(text, "86-DOS") || str_has_ci(text, "86DOS");
}

// Record the detected flavour.
static int set_flavor(SystemInfo *info, const char *name) {
    str_copy(info->dos_flavor, sizeof(info->dos_flavor), name);
    return 1;
}

static void set_logo_hint(SystemInfo *info, const char *hint) {
    str_copy(info->logo_hint, sizeof(info->logo_hint), hint);
}

// Capture the first non-empty output line from VER into info->ver_string
static void detect_ver_output(SystemInfo *info) {
    static const char tmp_name[] = "drfver.tmp";
    FILE *file;
    char line[128];

    if (!info) {
        return;
    }

    remove(tmp_name);
    if (system("ver > drfver.tmp") != 0) {
        remove(tmp_name);
        return;
    }

    file = fopen(tmp_name, "rt");
    if (!file) {
        remove(tmp_name);
        return;
    }

    while (fgets(line, sizeof(line), file)) {
        size_t len;
        size_t start = 0;
        while (line[start] && isspace((unsigned char)line[start])) {
            ++start;
        }
        if (!line[start]) {
            continue;
        }
        len = strcspn(line + start, "\r\n");
        if (len >= sizeof(info->ver_string)) {
            len = sizeof(info->ver_string) - 1;
        }
        memcpy(info->ver_string, line + start, len);
        info->ver_string[len] = '\0';
        break;
    }
    fclose(file);
    remove(tmp_name);
}

typedef struct {
    const char *needle;
    const char *display_name;
    const char *logo_hint;
    int include_version;
    int force_ansi;
} VerFlavorPattern;

static int extract_version_text(const char *ver_line, const char *needle, const char *match_ptr,
                                char *buffer, size_t buffer_len) {
    /* Pull the version substring following "version" or a matched needle. */
    const char *start = NULL;
    const char *version_ptr;

    if (!ver_line || !buffer || buffer_len == 0) {
        return 0;
    }

    version_ptr = str_find_ci(ver_line, "version");
    if (version_ptr) {
        version_ptr += 7;
        while (*version_ptr && (isspace((unsigned char)*version_ptr) || *version_ptr == ':' ||
                                *version_ptr == '-' || *version_ptr == '=')) {
            version_ptr++;
        }
        start = version_ptr;
    }

    if (!start && needle && match_ptr) {
        size_t needle_len = strlen(needle);
        start = match_ptr + needle_len;
        while (*start && (isspace((unsigned char)*start) || *start == ':' || *start == '-' ||
                          *start == '=')) {
            start++;
        }
    }

    if (!start || !*start) {
        return 0;
    }

    {
        size_t copy_len = 0;
        while (start[copy_len] && start[copy_len] != '\r' && start[copy_len] != '\n') {
            if (start[copy_len] == '.' && start[copy_len + 1] == ' ') {
                unsigned char next_char = (unsigned char)start[copy_len + 2];
                if (next_char && isupper(next_char)) {
                    break;
                }
            }
            if (start[copy_len] == ' ' && str_starts_ci(start + copy_len + 1, "Reported")) {
                break;
            }
            copy_len++;
        }

        while (copy_len > 0 && isspace((unsigned char)start[copy_len - 1])) {
            copy_len--;
        }
        if (copy_len == 0) {
            return 0;
        }
        if (copy_len >= buffer_len) {
            copy_len = buffer_len - 1;
        }
        memcpy(buffer, start, copy_len);
        buffer[copy_len] = '\0';
    }
    return 1;
}

static void set_flavor_from_pattern(SystemInfo *info, const VerFlavorPattern *pattern,
                                    const char *ver_line, const char *match_ptr) {
    /* Apply a matched pattern: set flavor, optional version, logo, and ANSI. */
    char flavor[MAX_FLAVOR_LEN];
    const char *base = pattern->display_name ? pattern->display_name : pattern->needle;

    str_copy(flavor, sizeof(flavor), base);

    if (pattern->include_version) {
        char version_text[MAX_FLAVOR_LEN];
        if (extract_version_text(ver_line, pattern->needle, match_ptr, version_text,
                                 sizeof(version_text))) {
            size_t len = strlen(flavor);
            if (version_text[0] && len + 1 < sizeof(flavor)) {
                flavor[len++] = ' ';
                flavor[len] = '\0';
                strncat(flavor, version_text, sizeof(flavor) - len - 1);
            }
        }
    }

    set_flavor(info, flavor);
    if (pattern->logo_hint && *pattern->logo_hint) {
        set_logo_hint(info, pattern->logo_hint);
    }
    if (pattern->force_ansi) {
        info->ansi_available = 1;
    }
}

static int detect_flavor_from_ver(SystemInfo *info) {

    static const VerFlavorPattern patterns[] = {
        {"DOSBOX-X", "DOSBox-X", "dosbox_x", 1, 1},
        {"DOSBOX STAGING", "DOSBox Staging", "dosbox_staging", 1, 1},
        {"DOSBOX", "DOSBox", "dosbox", 1, 1},
        {"DOSBIAN", "Dosbian", "dosbian", 0, 0},
        {"SVARDOS", "SvarDOS", NULL, 0, 0},
        {"DOSEMU", "DOSEMU", NULL, 1, 1},
        {"DR-DOS", "DR-DOS", "dr_dos", 1, 0},
        {"DRDOS", "DR-DOS", "dr_dos", 0, 0},
        {"NOVELL", "Novell DOS", "novell_dos", 0, 0},
        {"FREEDOS", "FreeDOS", "freedos", 1, 0},
        {"PC DOS", "PC DOS", "pc_dos", 1, 0},
        {"PC-DOS", "PC DOS", "pc_dos", 0, 0},
        {"PCDOS", "PC DOS", "pc_dos", 0, 0},
        {"IBM DOS", "IBM DOS", "pc_dos", 1, 0},
        {"MS-DOS", "MS-DOS", "ms_dos", 1, 0},
        {"MSDOS", "MS-DOS", "ms_dos", 0, 0},
        {"COMPAQ", "Compaq DOS", NULL, 0, 0},
        {"ROM-DOS", "ROM-DOS", NULL, 1, 0},
        {"86-DOS", "86-DOS", "86_dos", 0, 0},
        {"CONCURRENT", "Concurrent DOS", "concurrentdos", 0, 0},
    };
    size_t i;
    const char *ver_line;

    if (!info) {
        return 0;
    }
    ver_line = info->ver_string;
    if (!ver_line[0]) {
        return 0;
    }

    for (i = 0; i < sizeof(patterns) / sizeof(patterns[0]); ++i) {
        const VerFlavorPattern *pattern = &patterns[i];
        const char *match_ptr = str_find_ci(ver_line, pattern->needle);
        if (match_ptr) {
            set_flavor_from_pattern(info, pattern, ver_line, match_ptr);
            return 1;
        }
    }
    return 0;
}

static int detect_drdos_signature(char *buffer, size_t buffer_len) {
    /* INT 21h AX=4452h DR-DOS signature (RBIL R61: inter61d/INTERRUP.R) */
    union REGS regs;
    memset(&regs, 0, sizeof(regs));
    regs.x.ax = 0x4452;
    intdos(&regs, &regs);
    if (!(regs.x.cflag & 1)) {
        unsigned char version_code = regs.h.al;
        unsigned char major = (unsigned char)((version_code >> 4) & 0x0F);
        unsigned char minor = (unsigned char)(version_code & 0x0F);
        if (major || minor) {
            snprintf(buffer, buffer_len, "DR-DOS %u.%u", major, minor);
        } else {
            str_copy(buffer, buffer_len, "DR-DOS");
        }
        return 1;
    }
    return 0;
}

static const char *getenv_ci(const char *name) {
    char buffer[32];
    const char *value;
    size_t i;
    size_t len;

    if (!name) {
        return NULL;
    }
    value = getenv(name);
    if (value) {
        return value;
    }

    len = strlen(name);
    if (len >= sizeof(buffer)) {
        return NULL;
    }
    for (i = 0; i < len; ++i) {
        buffer[i] = (char)tolower((unsigned char)name[i]);
    }
    buffer[len] = '\0';
    value = getenv(buffer);
    if (value) {
        return value;
    }

    for (i = 0; i < len; ++i) {
        buffer[i] = (char)toupper((unsigned char)name[i]);
    }
    return getenv(buffer);
}

static int any_hint_matches(const char *const *hints, int (*pred)(const char *)) {
    size_t i;
    for (i = 0; i < HINT_COUNT; ++i) {
        if (pred(hints[i])) {
            return 1;
        }
    }
    return 0;
}

/* DOSBox-X and DOSBox Staging both report a version through one of two
   environment variables, falling back to the VER string. A hint that already
   names DOSBox is taken verbatim; anything else gets the label prefixed. */
static void set_emulator_flavor(SystemInfo *info, const char *label, const char *logo_hint,
                                const char *primary, const char *fallback) {
    const char *version = NULL;

    if (primary && *primary) {
        version = primary;
    } else if (fallback && *fallback) {
        version = fallback;
    } else if (info->ver_string[0]) {
        version = info->ver_string;
    }

    if (version && *version) {
        if (str_has_ci(version, "DOSBOX")) {
            str_copy(info->dos_flavor, sizeof(info->dos_flavor), version);
        } else {
            snprintf(info->dos_flavor, sizeof(info->dos_flavor), "%s %s", label, version);
        }
    } else {
        str_copy(info->dos_flavor, sizeof(info->dos_flavor), label);
    }
    set_logo_hint(info, logo_hint);
}

/* Novell DOS and Concurrent DOS sit on top of another DOS rather than
   replacing it, so a detected flavour is kept in parentheses. marker is the
   text that means "already attributed to this family". */
static int apply_family_hint(SystemInfo *info, const char *family, const char *marker,
                             const char *logo_hint, int flavor_set) {
    set_logo_hint(info, logo_hint);
    if (!flavor_set) {
        return set_flavor(info, family);
    }
    if (!str_has_ci(info->dos_flavor, marker)) {
        char original[MAX_FLAVOR_LEN];
        str_copy(original, sizeof(original), info->dos_flavor);
        snprintf(info->dos_flavor, sizeof(info->dos_flavor), "%s (%s)", family, original);
    }
    return 1;
}

// Core DOS version/flavor heuristics
static void detect_dos_version(SystemInfo *info) {
    union REGS regs;
    union REGS regs_version_hint;
    const char *hints[HINT_COUNT];
    const char *os_env;
    const char *comspec_env;
    const char *dosdir_env;
    const char *path_env;
    const char *svardos_env;
    const char *svardos_version;
    const char *dosbox_env;
    const char *dosbox_version;
    const char *dosbox_staging_env;
    const char *dosbox_staging_version;
    const char *dosbox_x_env;
    const char *dosbox_x_version;
    const char *dosbian_env;
    const char *dosemu_env;
    int flavor_set;
    int dosbox_hint;
    int dosbox_x_hint;
    int dosbox_staging_hint;
    unsigned char oem_hint = 0xFF;

    info->dos_major = 0;
    info->dos_minor = 0;
    info->dos_revision = 0;
    info->dos_flags = 0;

    // INT 21h AX=3306h (true version/flags), RBIL R61: inter61b/INTERRUP.H
    memset(&regs, 0, sizeof(regs));
    regs.x.ax = 0x3306;
    intdos(&regs, &regs);
    if (!(regs.x.cflag & 1) && regs.h.al != 0xFF) {
        info->dos_major = regs.h.bl;
        info->dos_minor = regs.h.bh;
        info->dos_revision = (unsigned char)(regs.h.dl & 0x07);
        info->dos_flags = regs.h.dh;
    }

    // OEM hint via INT 21h AX=3000h, RBIL R61: inter61b/INTERRUP.H
    memset(&regs_version_hint, 0, sizeof(regs_version_hint));
    regs_version_hint.x.ax = 0x3000;
    intdos(&regs_version_hint, &regs_version_hint);
    if (!(regs_version_hint.x.cflag & 1)) {
        if (info->dos_major == 0 && info->dos_minor == 0) {
            info->dos_major = regs_version_hint.h.al;
            info->dos_minor = regs_version_hint.h.ah;
        }
        oem_hint = regs_version_hint.h.bh;
    }

    os_env = getenv_ci("OS");
    comspec_env = getenv_ci("COMSPEC");
    if (!comspec_env) {
        comspec_env = getenv("ComSpec"); // dafuq
    }
    dosdir_env = getenv_ci("DOSDIR");
    path_env = getenv_ci("PATH");
    svardos_env = getenv_ci("SVARDOS");
    svardos_version = getenv_ci("SVARDOS_VERSION");
    dosbox_env = getenv_ci("DOSBOX");
    dosbox_version = getenv_ci("DOSBOX_VERSION");
    dosbox_staging_env = getenv_ci("DOSBOX_STAGING");
    dosbox_staging_version = getenv_ci("DOSBOX_STAGING_VERSION");
    dosbox_x_env = getenv_ci("DOSBOX_X");
    dosbox_x_version = getenv_ci("DOSBOX_X_VERSION");
    dosbian_env = getenv_ci("DOSBIAN");
    dosemu_env = getenv_ci("DOSEMU");

    hints[0] = os_env;
    hints[1] = comspec_env;
    hints[2] = dosdir_env;
    hints[3] = path_env;

    dosbox_x_hint = ((dosbox_x_env && *dosbox_x_env) || (dosbox_x_version && *dosbox_x_version) ||
                     str_has_ci(dosbox_version, "DOSBOX-X") || str_has_ci(os_env, "DOSBOX-X") ||
                     str_has_ci(info->ver_string, "DOSBOX-X"));

    dosbox_staging_hint = ((dosbox_staging_env && *dosbox_staging_env) ||
                           (dosbox_staging_version && *dosbox_staging_version) ||
                           str_has_ci(dosbox_version, "STAGING") ||
                           (str_has_ci(os_env, "DOSBOX") && str_has_ci(os_env, "STAGING")) ||
                           str_has_ci(info->ver_string, "STAGING"));

    dosbox_hint = dosbox_x_hint || dosbox_staging_hint || str_has_ci(os_env, "DOSBOX") ||
                  (dosbox_env && *dosbox_env) || (dosbox_version && *dosbox_version) ||
                  (comspec_env && (comspec_env[0] == 'Z' || comspec_env[0] == 'z') &&
                   comspec_env[1] == ':') ||
                  str_has_ci(path_env, "Z:\\");

    // detect_flavor_from_ver()
    flavor_set = (info->dos_flavor[0] != '\0');

    if (!flavor_set) {
        if ((svardos_env && *svardos_env) || (svardos_version && *svardos_version) ||
            (os_env && strstr(os_env, "SVARDOS")) || str_has_ci(dosdir_env, "SVARDOS") ||
            str_has_ci(path_env, "SVARDOS")) {
            flavor_set = set_flavor(info, "SvarDOS");
        } else if (dosbian_env && *dosbian_env) {
            flavor_set = set_flavor(info, "Dosbian");
        } else if (dosbox_hint) {
            if (dosbox_x_hint) {
                set_emulator_flavor(info, "DOSBox-X", "dosbox_x", dosbox_x_version, dosbox_version);
            } else if (dosbox_staging_hint) {
                set_emulator_flavor(info, "DOSBox Staging", "dosbox_staging",
                                    dosbox_staging_version, dosbox_version);
            } else if (dosbox_version && *dosbox_version) {
                snprintf(info->dos_flavor, sizeof(info->dos_flavor), "DOSBox %s", dosbox_version);
            } else {
                set_flavor(info, "DOSBox");
            }
            info->ansi_available = 1;
            flavor_set = 1;
        } else if (detect_drdos_signature(info->dos_flavor, sizeof(info->dos_flavor))) {
            flavor_set = 1;
        } else if (os_env && (strstr(os_env, "DRDOS") || strstr(os_env, "DR-DOS"))) {
            flavor_set = set_flavor(info, "DR-DOS");
        }
    }

    if (!flavor_set && str_has_ci(comspec_env, "SVARDOS")) {
        flavor_set = set_flavor(info, "SvarDOS");
    }

    if (!flavor_set && dosemu_env && *dosemu_env) {
        const char *emuver = getenv_ci("EMUVER");
        if (emuver && *emuver) {
            snprintf(info->dos_flavor, sizeof(info->dos_flavor), "DOSEMU %s", emuver);
        } else {
            set_flavor(info, "DOSEMU environment");
        }
        info->ansi_available = 1;
        flavor_set = 1;
    }

    if (!flavor_set) {
        const char *freedos_env = getenv_ci("FREEDOS");
        if (freedos_env && *freedos_env) {
            flavor_set = set_flavor(info, "FreeDOS");
        }
    }

    if (!flavor_set && os_env) {
        if (strncmp(os_env, "FreeDOS", 7) == 0) {
            flavor_set = set_flavor(info, "FreeDOS");
        } else if (strncmp(os_env, "PCDOS", 5) == 0) {
            flavor_set = set_flavor(info, "PC DOS");
        }
    }

    if (!flavor_set && any_hint_matches(hints, indicates_pc_dos)) {
        flavor_set = set_flavor(info, "PC DOS");
    }

    if (!flavor_set && oem_hint == 0xFD) {
        flavor_set = set_flavor(info, "FreeDOS");
    } else if (!flavor_set && oem_hint == 0) {
        flavor_set = set_flavor(info, "MS-DOS");
    }

    if (any_hint_matches(hints, indicates_novell_dos)) {
        flavor_set = apply_family_hint(info, "Novell DOS", "Novell DOS", "novell_dos", flavor_set);
    }

    if (any_hint_matches(hints, indicates_concurrent_dos)) {
        flavor_set =
            apply_family_hint(info, "Concurrent DOS", "Concurrent", "concurrentdos", flavor_set);
    }

    if (!flavor_set && any_hint_matches(hints, indicates_86_dos)) {
        set_logo_hint(info, "86_dos");
        flavor_set = set_flavor(info, "86-DOS");
    }

    if (!info->logo_hint[0] &&
        (strstr(info->dos_flavor, "DR-DOS") || strstr(info->dos_flavor, "DR DOS"))) {
        set_logo_hint(info, "dr_dos");
    }

    if (!info->dos_flavor[0]) {
        set_flavor(info, "MS-DOS compat.");
    } else if (oem_hint == 0x08 && (strcmp(info->dos_flavor, "MS-DOS") == 0 ||
                                    strcmp(info->dos_flavor, "MS-DOS compat.") == 0)) {
        set_flavor(info, "Compaq DOS");
    }
}

// Extract filename from a DOS-style path
static const char *basename_from_path(const char *path) {
    const char *last_slash = NULL;
    size_t i;
    if (!path) {
        return NULL;
    }
    for (i = 0; path[i] != '\0'; ++i) {
        if (path[i] == '\\' || path[i] == '/' || path[i] == ':') {
            last_slash = path + i + 1;
        }
    }
    return last_slash ? last_slash : path;
}

static void detect_shell(SystemInfo *info) {
    const char *comspec = getenv("COMSPEC");
    const char *shell = basename_from_path(comspec);
    if (!shell || !*shell) {
        shell = "Unknown";
    }
    str_copy(info->shell, sizeof(info->shell), shell);
}

// Query conventional and extended memory via BIOS interrupts
static void detect_memory(SystemInfo *info) {
    union REGS regs_in, regs_out;
    unsigned long extended_kb;

    memset(&regs_in, 0, sizeof(regs_in));
    memset(&regs_out, 0, sizeof(regs_out));

    int86(0x12, &regs_in, &regs_out);
    info->conventional_kb = regs_out.x.ax;

    /* Extended memory. INT 15h AX=E801h reports what is installed and is not
       claimed by XMS managers; AH=88h is the older call, but once HIMEM.SYS or
       HIMEMX owns extended memory several implementations answer it with 0.
       That is why this used to read "0 KB" on PC DOS and FreeDOS while MS-DOS
       6.22 reported the real figure. E801h returns the result in AX/BX; some
       BIOSes fill CX/DX instead. */
    extended_kb = 0;
    memset(&regs_in, 0, sizeof(regs_in));
    memset(&regs_out, 0, sizeof(regs_out));
    regs_in.x.ax = 0xE801;
    int86(0x15, &regs_in, &regs_out);
    if (!(regs_out.x.cflag & 1)) {
        unsigned int below_16m = regs_out.x.ax; /* KB in the 1M-16M range */
        unsigned int above_16m = regs_out.x.bx; /* 64 KB blocks above 16M */
        if (below_16m == 0 && above_16m == 0) {
            below_16m = regs_out.x.cx;
            above_16m = regs_out.x.dx;
        }
        extended_kb = (unsigned long)below_16m + (unsigned long)above_16m * 64UL;
    }

    if (extended_kb == 0) {
        memset(&regs_in, 0, sizeof(regs_in));
        memset(&regs_out, 0, sizeof(regs_out));
        regs_in.x.ax = 0x8800;
        int86(0x15, &regs_in, &regs_out);
        if (!(regs_out.x.cflag & 1)) {
            extended_kb = regs_out.x.ax;
        }
    }

    info->extended_kb = extended_kb;
}

// Get drive C: capacity/free space using INT 21h/36h.
static void detect_disk(SystemInfo *info) {
    union REGS regs_in, regs_out;

    memset(&regs_in, 0, sizeof(regs_in));
    memset(&regs_out, 0, sizeof(regs_out));

    regs_in.h.ah = 0x36;
    regs_in.h.dl = 3;
    intdos(&regs_in, &regs_out);
    if (regs_out.x.ax != 0xFFFF) {
        unsigned long sectors_per_cluster = regs_out.x.ax;
        unsigned long bytes_per_sector = regs_out.x.cx;
        unsigned long total_clusters = regs_out.x.dx;
        unsigned long free_clusters = regs_out.x.bx;
        unsigned long cluster_size = sectors_per_cluster * bytes_per_sector;
        info->disk_total_bytes = cluster_size * total_clusters;
        info->disk_free_bytes = cluster_size * free_clusters;
    } else {
        info->disk_total_bytes = 0;
        info->disk_free_bytes = 0;
    }
}

// Detect current video mode, VESA presence, and palette data
static void detect_display(SystemInfo *info) {
    union REGS regs_in, regs_out;
    /* Zeroed because the INT 10h read below may report success without
       filling every entry; this was file-scope (and so BSS-zeroed) before. */
    unsigned char bios_palette[48];
    unsigned char mode = 0;
    unsigned char columns = 0;
    unsigned char rows = 0;
    const char far *bda_rows = (const char far *)MK_FP(0x40, 0x84);
    static const struct {
        unsigned char mode;
        const char *description;
    } mode_table[] = {
        {0x00, "40x25 (B/W)"},           {0x01, "40x25 (col.)"},
        {0x02, "80x25 (B/W)"},           {0x03, "80x25 (col.)"},
        {0x04, "320x200 CGA 4-col.)"},   {0x05, "320x200 CGA 4-col.)"},
        {0x06, "640x200 CGA 2-col.)"},   {0x07, "80x25 (mono)"},
        {0x0D, "320x200 EGA 16-col.)"},  {0x0E, "640x200 EGA 16-col.)"},
        {0x0F, "640x350 EGA mono)"},     {0x10, "640x350 EGA 16-col.)"},
        {0x11, "640x480 VGA mono)"},     {0x12, "640x480 VGA 16-col.)"},
        {0x13, "320x200 VGA 256-col.)"},
    };
    size_t i;
    const char *mode_desc = NULL;
    unsigned char dac_width = 6;
    int palette_ok = 0;

    memset(bios_palette, 0, sizeof(bios_palette));
    memset(&regs_in, 0, sizeof(regs_in));
    memset(&regs_out, 0, sizeof(regs_out));
    info->display_vbe[0] = '\0';

    /* INT 10h AH=0Fh: get video mode -> AL = mode, AH = column count.
       This must not go through intdos(), which issues INT 21h, where AH=0Fh
       is "open file with FCB" and clobbers the result. */
    regs_in.h.ah = 0x0F;
    int86(0x10, &regs_in, &regs_out);
    mode = regs_out.h.al;
    columns = regs_out.h.ah;

    rows = 25;
    if (bda_rows) {
        unsigned char bios_rows = *bda_rows;
        if (bios_rows) {
            rows = bios_rows + 1;
        }
    }

    for (i = 0; i < sizeof(mode_table) / sizeof(mode_table[0]); ++i) {
        if (mode_table[i].mode == mode) {
            mode_desc = mode_table[i].description;
            break;
        }
    }
    if (!mode_desc) {
        mode_desc = "Unknown video mode";
    }

    snprintf(info->display_mode, sizeof(info->display_mode), "%ux%u text (color)", columns, rows);
    str_copy(info->display_adapter, sizeof(info->display_adapter), mode_desc);

    {
        struct SREGS sregs;
        unsigned char vbe_info[256];
        memset(&sregs, 0, sizeof(sregs));
        memset(&regs_in, 0, sizeof(regs_in));
        regs_in.x.ax = 0x4F00;
        regs_in.x.di = FP_OFF(vbe_info);
        sregs.es = FP_SEG(vbe_info);
        int86x(0x10, &regs_in, &regs_out, &sregs);
        if (!(regs_out.x.cflag & 1) && regs_out.h.al == 0x4F) {
            str_copy(info->display_vbe, sizeof(info->display_vbe), "VESA BIOS Extensions");
        }
    }

    regs_in.x.ax = 0x1017;
    regs_in.x.bx = 0x0000;
    int86(0x10, &regs_in, &regs_out);
    if (!(regs_out.x.cflag & 1)) {
        unsigned char reported = regs_out.h.bl;
        if (reported >= 1 && reported <= 8) {
            dac_width = reported;
        }
    }

#if defined(__WATCOMC__)
    {
        struct SREGS sregs;
        unsigned char far *far_palette = _fmalloc(sizeof(bios_palette));
        if (far_palette) {
            memset(&sregs, 0, sizeof(sregs));
            regs_in.x.ax = 0x1015;
            regs_in.x.bx = 0x0000;
            regs_in.x.cx = sizeof(bios_palette) / 3;
            regs_in.x.dx = FP_OFF(far_palette);
            sregs.es = FP_SEG(far_palette);
            if (int86x(0x10, &regs_in, &regs_out, &sregs) == 0 && !(regs_out.x.cflag & 1)) {
                _fmemcpy(bios_palette, far_palette, sizeof(bios_palette));
                palette_ok = 1;
            }
            _ffree(far_palette);
        }
    }
#else
    {
        struct SREGS sregs;
        memset(&sregs, 0, sizeof(sregs));
        regs_in.x.ax = 0x1015;
        regs_in.x.bx = 0x0000;
        regs_in.x.cx = sizeof(bios_palette) / 3;
        regs_in.x.dx = FP_OFF(bios_palette);
        sregs.es = FP_SEG(bios_palette);
        if (int86x(0x10, &regs_in, &regs_out, &sregs) == 0 && !(regs_out.x.cflag & 1)) {
            palette_ok = 1;
        }
    }
#endif

    if (palette_ok) {
        char buffer[256];
        size_t offset = 0;
        size_t limit = sizeof(info->palette_info) - 1;
        int max_value = (dac_width >= 1 && dac_width <= 8) ? ((1 << dac_width) - 1) : 63;
        if (max_value <= 0) {
            max_value = 1;
        }
        for (i = 0; i + 2 < sizeof(bios_palette) && offset < limit; i += 3) {
            int written;
            int r = bios_palette[i];
            int g = bios_palette[i + 1];
            int b = bios_palette[i + 2];
            r = (r * 255) / max_value;
            g = (g * 255) / max_value;
            b = (b * 255) / max_value;
            written = snprintf(buffer + offset, sizeof(buffer) - offset, "%s%02X%02X%02X",
                               (offset == 0 ? "" : " "), r, g, b);
            /* snprintf reports the length it would have written, so adding it
               blindly can push offset past the buffer and make the next
               sizeof(buffer) - offset wrap. Stop instead. */
            if (written < 0 || (size_t)written >= sizeof(buffer) - offset) {
                break;
            }
            offset += (size_t)written;
        }
        str_copy(info->palette_info, sizeof(info->palette_info), buffer);
    } else {
        info->palette_info[0] = '\0';
    }
}

// Probe BIOS
static void detect_computer_model(SystemInfo *info) {
#if defined(__WATCOMC__)
    union REGS regs;
    struct SREGS sregs;
    unsigned char far *table;

    memset(&regs, 0, sizeof(regs));
    memset(&sregs, 0, sizeof(sregs));

    info->computer_model[0] = '\0';
    regs.h.ah = 0xC0;
    int86x(0x15, &regs, &regs, &sregs);
    if (!(regs.x.cflag & 1)) {
        table = (unsigned char far *)MK_FP(sregs.es, regs.x.bx);
        if (table) {
            unsigned char model = table[1];
            unsigned char submodel = table[2];
            sprintf(info->computer_model, "Model 0x%02X Submodel 0x%02X", model, submodel);
            return;
        }
    }
#endif
    str_copy(info->computer_model, sizeof(info->computer_model), "Generic DOS PC");
}

// Rough CPU class detection via flags (8086/286/386+)
static void detect_cpu(SystemInfo *info) {
#if defined(__WATCOMC__)
    unsigned short flags_original = 0;
    unsigned short flags_test = 0;
    unsigned short flags_diff = 0;
    int is_8086 = 0;
    int is_286 = 0;

    _asm {
        pushf
        pop ax
        mov flags_original, ax
        mov bx, ax
        xor ax, 0xF000
        push ax
        popf
        pushf
        pop ax
        mov flags_test, ax
        push bx
        popf
    }
    if (((flags_test ^ flags_original) & 0xF000) == 0) {
        is_8086 = 1;
    }

    if (!is_8086) {
        _asm {
            mov ax, flags_original
            xor ax, 0x4000
            push ax
            popf
            pushf
            pop ax
            mov flags_diff, ax
            push flags_original
            popf
        }
        if (((flags_diff ^ flags_original) & 0x4000) == 0) {
            is_286 = 1;
        }
    }

    if (is_8086) {
        str_copy(info->cpu, sizeof(info->cpu), "8086/8088 class");
    } else if (is_286) {
        str_copy(info->cpu, sizeof(info->cpu), "80286 class");
    } else {
        str_copy(info->cpu, sizeof(info->cpu), "80386 or newer");
    }
#else
    str_copy(info->cpu, sizeof(info->cpu), "Unknown CPU");
#endif
}

/* Drive DOS booted from, as a letter. INT 21h AX=3305h is DOS 4+; regs are
   zeroed first so an older DOS leaves DL out of range and we fall back to C. */
static char boot_drive_letter(void) {
    union REGS regs;
    memset(&regs, 0, sizeof(regs));
    regs.x.ax = 0x3305;
    intdos(&regs, &regs);
    if (regs.h.dl >= 1 && regs.h.dl <= 26) {
        return (char)('A' + regs.h.dl - 1);
    }
    return 'C';
}

static void read_config_sys(SystemInfo *info, StringList *drivers) {
    /* Parse the config file to capture drivers and detect ANSI availability.
       FreeDOS reads FDCONFIG.SYS in preference to CONFIG.SYS and DR-DOS uses
       DCONFIG.SYS, so all three are tried.

       Probing is confined to the drive DOS actually booted from, then the
       current one. The old list hard-coded "A:\\CONFIG.SYS" as a fallback,
       which on any machine without a floppy raised a DOS critical error --
       "Error reading from drive A: ... (A)bort, (I)gnore, (R)etry, (F)ail?" --
       and blocked there. FreeDOS hit this every time, since it has no
       C:\\CONFIG.SYS for the first probe to find. */
    static const char *const names[] = {"FDCONFIG.SYS", "CONFIG.SYS", "DCONFIG.SYS"};
    char path[32];
    char boot = boot_drive_letter();
    size_t i;
    FILE *file = NULL;
    char line[256];

    for (i = 0; i < sizeof(names) / sizeof(names[0]) && !file; ++i) {
        snprintf(path, sizeof(path), "%c:\\%s", boot, names[i]);
        file = fopen(path, "rt");
    }
    for (i = 0; i < sizeof(names) / sizeof(names[0]) && !file; ++i) {
        file = fopen(names[i], "rt");
    }

    if (!file) {
        return;
    }

    while (fgets(line, sizeof(line), file)) {
        char *content = line;
        char *driver_path;
        char buffer[256];
        size_t j;

        for (j = 0; line[j] != '\0'; ++j) {
            if (line[j] == '\r' || line[j] == '\n') {
                line[j] = '\0';
                break;
            }
        }

        while (*content && isspace((unsigned char)*content)) {
            content++;
        }

        if (*content == '\0' || *content == ';') {
            continue;
        }

        /* Longest prefix first: "DEVICEHIGH" starts with "DEVICE", so testing
           the short one first leaves "HIGH=..." glued to the driver path. */
        if (str_starts_ci(content, "DEVICEHIGH")) {
            driver_path = content + 10;
        } else if (str_starts_ci(content, "DEVICE")) {
            driver_path = content + 6;
        } else if (str_starts_ci(content, "INSTALLHIGH")) {
            driver_path = content + 11;
        } else if (str_starts_ci(content, "INSTALL")) {
            driver_path = content + 7;
        } else {
            continue;
        }

        while (*driver_path && (isspace((unsigned char)*driver_path) || *driver_path == '=')) {
            driver_path++;
        }

        str_copy(buffer, sizeof(buffer), driver_path);
        trim_whitespace(buffer);
        if (*buffer != '\0') {
            if (!info->ansi_available &&
                (str_has_ci(buffer, "ANSI.SYS") || str_has_ci(buffer, "NANSI.SYS") ||
                 str_has_ci(buffer, "ANSIPLUS") || str_has_ci(buffer, "ANSI.COM"))) {
                info->ansi_available = 1;
            }
            strlist_add(drivers, buffer);
        }
    }

    fclose(file);
}

void detect_system(SystemInfo *info, StringList *drivers) {
    detect_ver_output(info);
    detect_flavor_from_ver(info);
    detect_dos_version(info);
    detect_shell(info);
    detect_computer_model(info);
    detect_cpu(info);
    detect_memory(info);
    detect_disk(info);
    detect_display(info);
    read_config_sys(info, drivers);
}
