/*
 * imd_cpmtools_vfii.c
 *
 * Port of imd_cpmtools_vfii.py to a single C file (ANSI C / MS-DOS 5.x portable).
 *
 * This tool operates on VFII-style IMD images (CP/M 2.2) to list/extract/insert/delete
 * files and patch VFII BIOS tables inside the IMD.
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <errno.h>
#include <time.h>

/* ---------------- Utilities ---------------- */

#define RECORD_SIZE 128
#define DIR_ENTRY_SIZE 32

static void die(const char *msg) {
    fprintf(stderr, "%s\n", msg);
    exit(1);
}

static void dief(const char *fmt, const char *arg) {
    fprintf(stderr, fmt, arg);
    fprintf(stderr, "\n");
    exit(1);
}

static int strieq(const char *a, const char *b) {
    while (*a && *b) {
        if (tolower((unsigned char)*a) != tolower((unsigned char)*b)) {
            return 0;
        }
        a++; b++;
    }
    return *a == '\0' && *b == '\0';
}

static int starts_with(const char *s, const char *prefix) {
    size_t ls = strlen(s);
    size_t lp = strlen(prefix);
    if (lp > ls) return 0;
    return strncmp(s, prefix, lp) == 0;
}

static unsigned char *read_file_bytes(const char *path, size_t *out_len) {
    FILE *f = fopen(path, "rb");
    unsigned char *buf;
    long len;
    if (!f) {
        perror(path);
        exit(1);
    }
    if (fseek(f, 0, SEEK_END) != 0) {
        perror("fseek");
        fclose(f);
        exit(1);
    }
    len = ftell(f);
    if (len < 0) {
        perror("ftell");
        fclose(f);
        exit(1);
    }
    if (sizeof(size_t) < 4 && len > 65535L) {
        fclose(f);
        die("File too large for 16-bit build; rebuild as 32-bit (e.g., dos4g).");
    }
    if (fseek(f, 0, SEEK_SET) != 0) {
        perror("fseek");
        fclose(f);
        exit(1);
    }
    buf = (unsigned char *)malloc((size_t)len);
    if (!buf) {
        fclose(f);
        die("Out of memory.");
    }
    if (len > 0 && fread(buf, 1, (size_t)len, f) != (size_t)len) {
        perror("fread");
        fclose(f);
        exit(1);
    }
    fclose(f);
    *out_len = (size_t)len;
    return buf;
}

static void write_file_bytes(const char *path, const unsigned char *data, size_t len) {
    FILE *f = fopen(path, "wb");
    if (!f) {
        perror(path);
        exit(1);
    }
    if (len > 0 && fwrite(data, 1, len, f) != len) {
        perror("fwrite");
        fclose(f);
        exit(1);
    }
    fclose(f);
}

static const char *mode_desc(int m) {
    switch (m) {
        case 0: return "FM 500kbps";
        case 1: return "FM 300kbps";
        case 2: return "FM 250kbps";
        case 3: return "MFM 500kbps";
        case 4: return "MFM 300kbps";
        case 5: return "MFM 250kbps";
        default: return "unknown";
    }
}

static int find_byte(const unsigned char *buf, size_t len, unsigned char b, size_t *out_idx) {
    size_t i;
    for (i = 0; i < len; i++) {
        if (buf[i] == b) {
            *out_idx = i;
            return 1;
        }
    }
    return 0;
}

static int parse_u8_auto(const char *s) {
    char *endp = NULL;
    long v;
    if (!s || !*s) die("Empty byte value.");

    /* If it's exactly 1-2 hex digits, treat as hex. */
    if (strlen(s) <= 2) {
        int i, hex = 1;
        for (i = 0; s[i]; i++) {
            if (!isxdigit((unsigned char)s[i])) {
                hex = 0;
                break;
            }
        }
        if (hex) {
            v = strtol(s, &endp, 16);
        } else {
            v = strtol(s, &endp, 0);
        }
    } else {
        v = strtol(s, &endp, 0);
    }
    if (endp == s || *endp != '\0') die("Invalid byte value.");
    if (v < 0 || v > 255) die("Byte value out of range 0..255.");
    return (int)v;
}

static char *dupstr(const char *s) {
    size_t n = strlen(s);
    char *p = (char *)malloc(n + 1);
    if (!p) die("Out of memory.");
    memcpy(p, s, n + 1);
    return p;
}

static char *basename_only(const char *path) {
    const char *p = path + strlen(path);
    while (p > path) {
        if (p[-1] == '/' || p[-1] == '\\') break;
        p--;
    }
    return dupstr(p);
}

static char *default_out_imd(const char *imd_path, const char *suffix) {
    const char *dot = strrchr(imd_path, '.');
    size_t base_len = dot ? (size_t)(dot - imd_path) : strlen(imd_path);
    size_t out_len = base_len + strlen(suffix) + 1;
    char *out = (char *)malloc(out_len);
    if (!out) die("Out of memory.");
    memcpy(out, imd_path, base_len);
    out[base_len] = '\0';
    strcat(out, suffix);
    return out;
}

static void atomic_write(const char *path, const unsigned char *data, size_t len) {
    char tmp[512];
    int r;
    FILE *f;
    size_t wrote;
    unsigned int rnd = (unsigned int)time(NULL);
    const char *base = path;
    const char *slash = strrchr(path, '/');
    const char *bslash = strrchr(path, '\\');
    if (bslash && (!slash || bslash > slash)) slash = bslash;
    if (slash) base = slash + 1;

    sprintf(tmp, ".%s.tmp.%u", base, rnd);
    f = fopen(tmp, "wb");
    if (!f) {
        perror("temp write");
        exit(1);
    }
    wrote = (len > 0) ? fwrite(data, 1, len, f) : 0;
    if (len > 0 && wrote != len) {
        perror("fwrite");
        fclose(f);
        remove(tmp);
        exit(1);
    }
    fclose(f);
    r = rename(tmp, path);
    if (r != 0) {
        /* Windows rename() fails if target exists; retry after removing. */
        if (remove(path) == 0) r = rename(tmp, path);
        if (r != 0) {
            perror("rename");
            remove(tmp);
            exit(1);
        }
    }
}

/* ---------------- IMD structures ---------------- */

typedef struct {
    int mode;
    int cyl;
    int head_raw;
    int nsec;
    int ssize_code;
    unsigned char *sec_ids; /* nsec bytes */
    unsigned char *cyl_map; /* optional */
    unsigned char *head_map; /* optional */
    unsigned char **sec_data; /* nsec pointers, NULL if unavailable */
} IMDTrack;

typedef struct {
    unsigned char *header;
    size_t header_len;
    IMDTrack *tracks;
    int track_count;
} IMDImage;

static int ssize_from_code(int code) {
    if (code == 0xFF) {
        die("Per-sector sizes (0xFF) not supported in this tool.");
    }
    return 128 << code;
}

static void free_imd(IMDImage *img) {
    int i, j;
    if (!img) return;
    for (i = 0; i < img->track_count; i++) {
        IMDTrack *t = &img->tracks[i];
        if (t->sec_ids) free(t->sec_ids);
        if (t->cyl_map) free(t->cyl_map);
        if (t->head_map) free(t->head_map);
        if (t->sec_data) {
            for (j = 0; j < t->nsec; j++) {
                if (t->sec_data[j]) free(t->sec_data[j]);
            }
            free(t->sec_data);
        }
    }
    if (img->tracks) free(img->tracks);
    if (img->header) free(img->header);
}

static IMDImage parse_imd(const char *path) {
    size_t len = 0, cut = 0;
    unsigned char *blob = read_file_bytes(path, &len);
    IMDImage img;
    size_t p;
    int track_cap = 32;

    img.header = NULL;
    img.header_len = 0;
    img.tracks = (IMDTrack *)malloc(sizeof(IMDTrack) * track_cap);
    img.track_count = 0;
    if (!img.tracks) die("Out of memory.");

    if (!find_byte(blob, len, 0x1A, &cut)) {
        free(blob);
        die("No 0x1A (EOF marker) found in IMD header.");
    }
    img.header_len = cut;
    img.header = (unsigned char *)malloc(cut);
    if (!img.header) die("Out of memory.");
    if (cut > 0) memcpy(img.header, blob, cut);
    p = cut + 1;

    while (p < len) {
        IMDTrack t;
        int i;
        int has_cyl_map, has_head_map;
        int ssize;

        if (p >= len) break;
        t.mode = blob[p++];
        if (p >= len) break;
        t.cyl = blob[p];
        t.head_raw = blob[p + 1];
        t.nsec = blob[p + 2];
        t.ssize_code = blob[p + 3];
        p += 4;

        has_cyl_map = (t.head_raw & 0x80) ? 1 : 0;
        has_head_map = (t.head_raw & 0x40) ? 1 : 0;

        t.sec_ids = (unsigned char *)malloc((size_t)t.nsec);
        t.cyl_map = NULL;
        t.head_map = NULL;
        t.sec_data = (unsigned char **)malloc(sizeof(unsigned char *) * (size_t)t.nsec);
        if (!t.sec_ids || !t.sec_data) die("Out of memory.");
        memcpy(t.sec_ids, blob + p, (size_t)t.nsec);
        p += t.nsec;

        if (has_cyl_map) {
            t.cyl_map = (unsigned char *)malloc((size_t)t.nsec);
            if (!t.cyl_map) die("Out of memory.");
            memcpy(t.cyl_map, blob + p, (size_t)t.nsec);
            p += t.nsec;
        }
        if (has_head_map) {
            t.head_map = (unsigned char *)malloc((size_t)t.nsec);
            if (!t.head_map) die("Out of memory.");
            memcpy(t.head_map, blob + p, (size_t)t.nsec);
            p += t.nsec;
        }

        ssize = ssize_from_code(t.ssize_code);
        for (i = 0; i < t.nsec; i++) t.sec_data[i] = NULL;
        for (i = 0; i < t.nsec; i++) {
            unsigned char rt = blob[p++];
            if (rt == 0x00) {
                t.sec_data[i] = NULL;
            } else if (rt == 0x01 || rt == 0x03 || rt == 0x05 || rt == 0x07) {
                t.sec_data[i] = (unsigned char *)malloc((size_t)ssize);
                if (!t.sec_data[i]) die("Out of memory.");
                memcpy(t.sec_data[i], blob + p, (size_t)ssize);
                p += (size_t)ssize;
            } else if (rt == 0x02 || rt == 0x04 || rt == 0x06 || rt == 0x08) {
                unsigned char fill = blob[p++];
                t.sec_data[i] = (unsigned char *)malloc((size_t)ssize);
                if (!t.sec_data[i]) die("Out of memory.");
                memset(t.sec_data[i], fill, (size_t)ssize);
            } else {
                free(blob);
                die("Unknown IMD sector record type.");
            }
        }

        if (img.track_count >= track_cap) {
            track_cap *= 2;
            img.tracks = (IMDTrack *)realloc(img.tracks, sizeof(IMDTrack) * track_cap);
            if (!img.tracks) die("Out of memory.");
        }
        img.tracks[img.track_count++] = t;
    }

    free(blob);
    return img;
}

/* ---------------- Sector skew handling ---------------- */

static int DEFAULT_SKEW_9[9] = {1, 4, 7, 2, 5, 8, 3, 6, 9};

static int *identity_skew_for_tracks(IMDImage *img, int *out_len) {
    int *skew;
    int i;
    if (img->track_count == 0) {
        *out_len = 0;
        return NULL;
    }
    skew = (int *)malloc(sizeof(int) * (size_t)img->tracks[0].nsec);
    if (!skew) die("Out of memory.");
    for (i = 0; i < img->tracks[0].nsec; i++) skew[i] = (int)img->tracks[0].sec_ids[i];
    *out_len = img->tracks[0].nsec;
    return skew;
}

static int *parse_skew_spec(const char *spec, IMDImage *img, int *out_len, int *is_auto) {
    char *tmp;
    char *tok;
    int *skew = NULL;
    int count = 0;
    int i;
    int nsec;
    int *phys_ids;

    *is_auto = 0;

    if (!spec || !*spec || strieq(spec, "auto")) {
        *is_auto = 1;
        *out_len = 0;
        return NULL;
    }
    if (strieq(spec, "none") || strieq(spec, "identity") || strieq(spec, "phys") || strieq(spec, "physical")) {
        return identity_skew_for_tracks(img, out_len);
    }

    if (img->track_count == 0) {
        *out_len = 0;
        return NULL;
    }

    nsec = img->tracks[0].nsec;
    phys_ids = (int *)malloc(sizeof(int) * (size_t)nsec);
    if (!phys_ids) die("Out of memory.");
    for (i = 0; i < nsec; i++) phys_ids[i] = (int)img->tracks[0].sec_ids[i];

    if (strieq(spec, "default") || strieq(spec, "vfii")) {
        int sequential = 1;
        for (i = 0; i < nsec; i++) {
            if (phys_ids[i] != (i + 1)) {
                sequential = 0;
                break;
            }
        }
        if (sequential && nsec == 9) {
            skew = (int *)malloc(sizeof(int) * 9);
            if (!skew) die("Out of memory.");
            for (i = 0; i < 9; i++) skew[i] = DEFAULT_SKEW_9[i];
            *out_len = 9;
            free(phys_ids);
            return skew;
        }
        free(phys_ids);
        return identity_skew_for_tracks(img, out_len);
    }

    tmp = dupstr(spec);
    skew = (int *)malloc(sizeof(int) * (size_t)nsec);
    if (!skew) die("Out of memory.");
    tok = strtok(tmp, ", ");
    while (tok) {
        skew[count++] = atoi(tok);
        tok = strtok(NULL, ", ");
    }
    free(tmp);

    if (count != nsec) {
        free(skew);
        free(phys_ids);
        die("--skew expects the track's number of sectors.");
    }

    /* Validate permutation */
    for (i = 0; i < nsec; i++) {
        int j, found = 0;
        for (j = 0; j < nsec; j++) {
            if (skew[i] == phys_ids[j]) { found = 1; break; }
        }
        if (!found) {
            free(skew);
            free(phys_ids);
            die("--skew must be a permutation of physical sector IDs.");
        }
    }
    for (i = 0; i < nsec; i++) {
        int j;
        for (j = i + 1; j < nsec; j++) {
            if (skew[i] == skew[j]) {
                free(skew);
                free(phys_ids);
                die("--skew must not contain duplicates.");
            }
        }
    }

    *out_len = nsec;
    free(phys_ids);
    return skew;
}

/* Candidate skews for auto-detect */
static int **candidate_skews_for_nsec(int nsec, int *out_count) {
    int **cands = NULL;
    int cap = 16;
    int count = 0;
    int i;

    cands = (int **)malloc(sizeof(int *) * cap);
    if (!cands) die("Out of memory.");

    /* helper to add unique */
    {
        int *seq;
        int j;
        if (nsec == 9) {
            seq = (int *)malloc(sizeof(int) * 9);
            if (!seq) die("Out of memory.");
            for (j = 0; j < 9; j++) seq[j] = DEFAULT_SKEW_9[j];
            cands[count++] = seq;
        }
    }

    {
        int *base = (int *)malloc(sizeof(int) * (size_t)nsec);
        if (!base) die("Out of memory.");
        for (i = 0; i < nsec; i++) base[i] = i + 1;
        cands[count++] = base;
    }

    /* step interleave */
    for (i = 2; i < nsec; i++) {
        int *seq = (int *)malloc(sizeof(int) * (size_t)nsec);
        int pos = 1;
        int *used = (int *)calloc((size_t)(nsec + 1), sizeof(int));
        int k;
        int idx = 0;
        if (!seq || !used) die("Out of memory.");
        for (k = 0; k < nsec; k++) {
            while (used[pos]) {
                pos = (pos % nsec) + 1;
            }
            seq[idx++] = pos;
            used[pos] = 1;
            pos = ((pos - 1 + i) % nsec) + 1;
        }
        free(used);
        if (count >= cap) {
            cap *= 2;
            cands = (int **)realloc(cands, sizeof(int *) * cap);
            if (!cands) die("Out of memory.");
        }
        cands[count++] = seq;
    }

    /* factor-group interleave */
    for (i = 2; i < nsec; i++) {
        int f = i;
        int k, start, idx = 0;
        int *seq;
        if ((nsec % f) != 0) continue;
        seq = (int *)malloc(sizeof(int) * (size_t)nsec);
        if (!seq) die("Out of memory.");
        for (start = 1; start <= f; start++) {
            for (k = 0; k < nsec / f; k++) {
                seq[idx++] = start + k * f;
            }
        }
        if (count >= cap) {
            cap *= 2;
            cands = (int **)realloc(cands, sizeof(int *) * cap);
            if (!cands) die("Out of memory.");
        }
        cands[count++] = seq;
    }

    *out_count = count;
    return cands;
}

/* ---------------- RAW build/rebuild ---------------- */

static IMDTrack *find_track(IMDImage *img, int cyl, int head) {
    int i;
    for (i = 0; i < img->track_count; i++) {
        IMDTrack *t = &img->tracks[i];
        if (t->cyl == cyl && (t->head_raw & 0x3F) == head) return t;
    }
    return NULL;
}

static unsigned char *build_raw(IMDImage *img, int *sec_skew, int skew_len, size_t *out_len) {
    int max_cyl = 0;
    int max_head = 0;
    int i;
    int ssize;
    int nsec;
    size_t total_len;
    unsigned char *raw;
    unsigned char *p;

    if (img->track_count == 0) {
        *out_len = 0;
        return NULL;
    }

    for (i = 0; i < img->track_count; i++) {
        if (img->tracks[i].cyl > max_cyl) max_cyl = img->tracks[i].cyl;
        if ((img->tracks[i].head_raw & 0x3F) > max_head) max_head = (img->tracks[i].head_raw & 0x3F);
    }
    max_cyl += 1;
    max_head += 1;

    ssize = ssize_from_code(img->tracks[0].ssize_code);
    nsec = img->tracks[0].nsec;
    total_len = (size_t)max_cyl * (size_t)max_head * (size_t)nsec * (size_t)ssize;
    raw = (unsigned char *)malloc(total_len);
    if (!raw) die("Out of memory.");
    memset(raw, 0, total_len);

    p = raw;
    {
        unsigned char *zeros = (unsigned char *)malloc((size_t)ssize);
        int c, h;
        if (!zeros) die("Out of memory.");
        memset(zeros, 0, (size_t)ssize);
        for (c = 0; c < max_cyl; c++) {
            for (h = 0; h < max_head; h++) {
                IMDTrack *t = find_track(img, c, h);
                if (t) {
                    int sid_to_idx[256];
                    int j;
                    for (j = 0; j < 256; j++) sid_to_idx[j] = -1;
                    for (j = 0; j < t->nsec; j++) sid_to_idx[(int)t->sec_ids[j]] = j;
                    for (j = 0; j < skew_len; j++) {
                        int phys_id = sec_skew[j];
                        int idx = (phys_id >= 0 && phys_id < 256) ? sid_to_idx[phys_id] : -1;
                        if (idx >= 0 && t->sec_data[idx]) {
                            memcpy(p, t->sec_data[idx], (size_t)ssize);
                        } else {
                            memcpy(p, zeros, (size_t)ssize);
                        }
                        p += ssize;
                    }
                } else {
                    int j;
                    for (j = 0; j < skew_len; j++) {
                        memcpy(p, zeros, (size_t)ssize);
                        p += ssize;
                    }
                }
            }
        }
        free(zeros);
    }

    *out_len = total_len;
    return raw;
}

typedef struct {
    unsigned char *data;
    size_t len;
    size_t cap;
} ByteBuf;

static void buf_init(ByteBuf *b) {
    b->data = NULL;
    b->len = 0;
    b->cap = 0;
}

static void buf_reserve(ByteBuf *b, size_t add) {
    size_t need = b->len + add;
    if (need > b->cap) {
        size_t ncap = b->cap ? b->cap * 2 : 1024;
        while (ncap < need) ncap *= 2;
        b->data = (unsigned char *)realloc(b->data, ncap);
        if (!b->data) die("Out of memory.");
        b->cap = ncap;
    }
}

static void buf_append(ByteBuf *b, const unsigned char *data, size_t len) {
    buf_reserve(b, len);
    memcpy(b->data + b->len, data, len);
    b->len += len;
}

static void buf_append_byte(ByteBuf *b, unsigned char v) {
    buf_reserve(b, 1);
    b->data[b->len++] = v;
}

static unsigned char *rebuild_imd(IMDImage *img, const unsigned char *raw, size_t raw_len,
                                  int *sec_skew, int skew_len, size_t *out_len) {
    int max_cyl = 0, max_head = 0;
    int i;
    int ssize, nsec;
    size_t p = 0;
    ByteBuf out;
    int inv_map[256];

    if (img->track_count == 0) {
        *out_len = 0;
        return NULL;
    }

    for (i = 0; i < img->track_count; i++) {
        if (img->tracks[i].cyl > max_cyl) max_cyl = img->tracks[i].cyl;
        if ((img->tracks[i].head_raw & 0x3F) > max_head) max_head = (img->tracks[i].head_raw & 0x3F);
    }
    max_cyl += 1;
    max_head += 1;

    ssize = ssize_from_code(img->tracks[0].ssize_code);
    nsec = img->tracks[0].nsec;

    for (i = 0; i < 256; i++) inv_map[i] = -1;
    for (i = 0; i < skew_len; i++) {
        if (sec_skew[i] >= 0 && sec_skew[i] < 256) inv_map[sec_skew[i]] = i;
    }

    buf_init(&out);
    if (img->header_len > 0) buf_append(&out, img->header, img->header_len);
    buf_append_byte(&out, 0x1A);

    {
        int c, h;
        for (c = 0; c < max_cyl; c++) {
            for (h = 0; h < max_head; h++) {
                IMDTrack *t = find_track(img, c, h);
                int j;
                if (!t) {
                    p += (size_t)(nsec * ssize);
                    continue;
                }

                if (p + (size_t)(nsec * ssize) > raw_len) die("RAW length underrun.");

                /* Track header */
                buf_append_byte(&out, (unsigned char)t->mode);
                buf_append_byte(&out, (unsigned char)t->cyl);
                buf_append_byte(&out, (unsigned char)t->head_raw);
                buf_append_byte(&out, (unsigned char)t->nsec);
                buf_append_byte(&out, (unsigned char)t->ssize_code);
                buf_append(&out, t->sec_ids, (size_t)t->nsec);
                if (t->cyl_map) buf_append(&out, t->cyl_map, (size_t)t->nsec);
                if (t->head_map) buf_append(&out, t->head_map, (size_t)t->nsec);

                for (j = 0; j < t->nsec; j++) {
                    int phys_id = (int)t->sec_ids[j];
                    int li = (phys_id >= 0 && phys_id < 256) ? inv_map[phys_id] : -1;
                    const unsigned char *chunk;
                    buf_append_byte(&out, 0x01);
                    if (li < 0 || li >= nsec) {
                        unsigned char *zeros = (unsigned char *)calloc((size_t)ssize, 1);
                        if (!zeros) die("Out of memory.");
                        buf_append(&out, zeros, (size_t)ssize);
                        free(zeros);
                    } else {
                        chunk = raw + p + (size_t)li * (size_t)ssize;
                        buf_append(&out, chunk, (size_t)ssize);
                    }
                }

                p += (size_t)(nsec * ssize);
            }
        }
    }

    *out_len = out.len;
    return out.data;
}

/* ---------------- Directory detection helpers ---------------- */

static int is_dir_sector(const unsigned char *buf512) {
    int i;
    for (i = 0; i < 16; i++) {
        unsigned char u = buf512[i * 32];
        if (!(u == 0xE5 || u <= 0x0F)) return 0;
    }
    return 1;
}

typedef struct {
    int cyls;
    int heads;
    int phys_secs_per_track;
    int phys_sec_size;
    int bytes_per_track;
    int *sec_skew;
    int sec_skew_len;
    int dir_cyl;
    int dir_head;
    int *dir_phys_secs;
    int dir_phys_secs_len;
    int block_size;
    int exm;
    int block0_offset;
} DiskParams;

static int looks_like_name83(const unsigned char *e) {
    int i;
    int all_space = 1;
    for (i = 1; i < 12; i++) {
        if (e[i] != 0x20) { all_space = 0; break; }
    }
    if (all_space) return 0;
    for (i = 1; i < 12; i++) {
        unsigned char ch = e[i];
        if (ch == 0x20) continue;
        if (!((ch >= 0x30 && ch <= 0x39) || (ch >= 0x41 && ch <= 0x5A))) return 0;
    }
    return 1;
}

static void score_directory(const unsigned char *raw, int cyls, int heads, int bytes_per_track,
                            int phys_secs, int phys_sec_size,
                            int *out_active, int *out_dirsecs, int *out_cyl, int *out_head,
                            int **out_dir_list, int *out_dir_list_len) {
    int best_active = 0;
    int best_dirsec_count = 0;
    int best_c = 0, best_h = 0;
    int *best_list = NULL;
    int best_list_len = 0;
    int c, h;

    for (c = 0; c < cyls; c++) {
        for (h = 0; h < heads; h++) {
            int track_start = (c * heads + h) * bytes_per_track;
            int *sec_hits = (int *)malloc(sizeof(int) * (size_t)phys_secs);
            int sec_hits_count = 0;
            int active_entries = 0;
            int sid;
            if (!sec_hits) die("Out of memory.");
            for (sid = 1; sid <= phys_secs; sid++) {
                int off = track_start + (sid - 1) * phys_sec_size;
                const unsigned char *sec = raw + off;
                if (phys_sec_size == 512 && is_dir_sector(sec)) {
                    int i;
                    sec_hits[sec_hits_count++] = sid;
                    for (i = 0; i < 16; i++) {
                        const unsigned char *e = sec + i * 32;
                        if (e[0] != 0xE5 && e[0] <= 0x0F && looks_like_name83(e)) {
                            active_entries++;
                        }
                    }
                }
            }
            if (sec_hits_count >= 2 && active_entries > 0) {
                int better = 0;
                if (best_list == NULL) better = 1;
                else if (active_entries > best_active) better = 1;
                else if (active_entries == best_active && sec_hits_count > best_dirsec_count) better = 1;
                if (better) {
                    if (best_list) free(best_list);
                    best_list = sec_hits;
                    best_list_len = sec_hits_count;
                    best_active = active_entries;
                    best_dirsec_count = sec_hits_count;
                    best_c = c;
                    best_h = h;
                    continue;
                }
            }
            free(sec_hits);
        }
    }

    if (!best_list) {
        *out_active = 0; *out_dirsecs = 0; *out_cyl = 0; *out_head = 0;
        *out_dir_list = NULL; *out_dir_list_len = 0;
        return;
    }

    *out_active = best_active;
    *out_dirsecs = best_dirsec_count;
    *out_cyl = best_c;
    *out_head = best_h;
    *out_dir_list = best_list;
    *out_dir_list_len = best_list_len;
}

static DiskParams infer_params(const unsigned char *raw, size_t raw_len, IMDImage *img,
                               int *sec_skew, int skew_len) {
    DiskParams dp;
    int max_cyl = 0, max_head = 0;
    int phys_secs, phys_sec_size, bytes_per_track;
    int active, dirsecs, dir_cyl, dir_head;
    int *dir_list = NULL;
    int dir_list_len = 0;
    int i;

    if (img->track_count == 0) die("No tracks in IMD.");
    for (i = 0; i < img->track_count; i++) {
        if (img->tracks[i].cyl > max_cyl) max_cyl = img->tracks[i].cyl;
        if ((img->tracks[i].head_raw & 0x3F) > max_head) max_head = (img->tracks[i].head_raw & 0x3F);
    }
    dp.cyls = max_cyl + 1;
    dp.heads = max_head + 1;
    phys_secs = img->tracks[0].nsec;
    phys_sec_size = ssize_from_code(img->tracks[0].ssize_code);
    bytes_per_track = phys_secs * phys_sec_size;

    score_directory(raw, dp.cyls, dp.heads, bytes_per_track, phys_secs, phys_sec_size,
                    &active, &dirsecs, &dir_cyl, &dir_head, &dir_list, &dir_list_len);
    if (active < 2 || dir_list_len < 2) {
        if (dir_list) free(dir_list);
        die("Could not locate CP/M directory sectors in this IMD.");
    }

    dp.phys_secs_per_track = phys_secs;
    dp.phys_sec_size = phys_sec_size;
    dp.bytes_per_track = bytes_per_track;
    dp.sec_skew = (int *)malloc(sizeof(int) * (size_t)skew_len);
    if (!dp.sec_skew) die("Out of memory.");
    memcpy(dp.sec_skew, sec_skew, sizeof(int) * (size_t)skew_len);
    dp.sec_skew_len = skew_len;
    dp.dir_cyl = dir_cyl;
    dp.dir_head = dir_head;
    dp.dir_phys_secs = dir_list;
    dp.dir_phys_secs_len = dir_list_len;
    dp.block_size = 2048;
    dp.exm = 1;
    dp.block0_offset = (dir_cyl * dp.heads + dir_head) * bytes_per_track;

    (void)raw_len; /* unused in this calculation */
    return dp;
}

static int directory_entry_offsets(const DiskParams *dp, int *out_count, int **out_offsets) {
    int track_start = (dp->dir_cyl * dp->heads + dp->dir_head) * dp->bytes_per_track;
    int total = dp->dir_phys_secs_len * 16;
    int *offs = (int *)malloc(sizeof(int) * (size_t)total);
    int i, idx = 0;
    if (!offs) die("Out of memory.");
    for (i = 0; i < dp->dir_phys_secs_len; i++) {
        int sid = dp->dir_phys_secs[i];
        int sec_off = track_start + (sid - 1) * 512;
        int j;
        for (j = 0; j < 16; j++) {
            offs[idx++] = sec_off + j * DIR_ENTRY_SIZE;
        }
    }
    *out_count = total;
    *out_offsets = offs;
    return total;
}

typedef struct {
    int offset;
    unsigned char bytes[32];
} DirEntry;

static DirEntry *parse_directory(const unsigned char *raw, const DiskParams *dp, int *out_count) {
    int n, i;
    int *offs = NULL;
    DirEntry *ents;
    directory_entry_offsets(dp, &n, &offs);
    ents = (DirEntry *)malloc(sizeof(DirEntry) * (size_t)n);
    if (!ents) die("Out of memory.");
    for (i = 0; i < n; i++) {
        ents[i].offset = offs[i];
        memcpy(ents[i].bytes, raw + offs[i], 32);
    }
    free(offs);
    *out_count = n;
    return ents;
}

static void normalize_83(const char *name, unsigned char out83[11]) {
    char upper[64];
    int i;
    const char *dot;
    char fn[9], ex[4];
    for (i = 0; name[i] && i < (int)sizeof(upper) - 1; i++) {
        upper[i] = (char)toupper((unsigned char)name[i]);
    }
    upper[i] = '\0';
    dot = strchr(upper, '.');
    if (dot) {
        size_t fnlen = (size_t)(dot - upper);
        if (fnlen > 8) fnlen = 8;
        strncpy(fn, upper, fnlen);
        fn[fnlen] = '\0';
        strncpy(ex, dot + 1, 3);
        ex[3] = '\0';
    } else {
        strncpy(fn, upper, 8);
        fn[8] = '\0';
        ex[0] = '\0';
    }
    for (i = 0; i < 8; i++) {
        out83[i] = (i < (int)strlen(fn)) ? (unsigned char)fn[i] : 0x20;
    }
    for (i = 0; i < 3; i++) {
        out83[8 + i] = (i < (int)strlen(ex)) ? (unsigned char)ex[i] : 0x20;
    }
}

static void decode_name(const unsigned char *e, char *out, size_t out_len) {
    char fn[9];
    char ex[4];
    int i;
    for (i = 0; i < 8; i++) fn[i] = (char)e[1 + i];
    fn[8] = '\0';
    for (i = 0; i < 3; i++) ex[i] = (char)e[9 + i];
    ex[3] = '\0';
    while (strlen(fn) > 0 && fn[strlen(fn) - 1] == ' ') fn[strlen(fn) - 1] = '\0';
    while (strlen(ex) > 0 && ex[strlen(ex) - 1] == ' ') ex[strlen(ex) - 1] = '\0';
    if (out_len == 0) return;
    if (strlen(ex) > 0) {
        sprintf(out, "%s.%s", fn, ex);
    } else {
        sprintf(out, "%s", fn);
    }
}

static int dirent_extent_num(const unsigned char *e) {
    int ex = e[12] & 0x1F;
    int s2 = e[14] & 0xFF;
    return (s2 << 5) | ex;
}

typedef struct {
    int extent_num;
    int rc;
    int blocks[16];
    int block_count;
} ExtentInfo;

static int iter_dirent_subextents(const unsigned char *e, const DiskParams *dp,
                                 ExtentInfo *out, int max_out) {
    int ptrs_per_sub = 16 / (dp->exm + 1);
    int extent_num = dirent_extent_num(e);
    int last_sub = (e[12] & 0x1F) & dp->exm;
    int start_extent = extent_num - last_sub;
    int out_count = 0;
    int sub;

    for (sub = 0; sub <= last_sub; sub++) {
        int start = 16 + sub * ptrs_per_sub;
        int i;
        int rc;
        int blocks[16];
        int bc = 0;
        for (i = 0; i < ptrs_per_sub; i++) {
            unsigned char b = e[start + i];
            if (b != 0) blocks[bc++] = (int)b;
        }
        if (bc == 0) continue;
        rc = (sub < last_sub) ? 128 : (int)e[15];
        if (out_count < max_out) {
            out[out_count].extent_num = start_extent + sub;
            out[out_count].rc = rc;
            out[out_count].block_count = bc;
            memcpy(out[out_count].blocks, blocks, sizeof(int) * (size_t)bc);
            out_count++;
        }
    }

    /* sort by extent_num (simple bubble due to small size) */
    {
        int i, j;
        for (i = 0; i < out_count; i++) {
            for (j = i + 1; j < out_count; j++) {
                if (out[j].extent_num < out[i].extent_num) {
                    ExtentInfo tmp = out[i];
                    out[i] = out[j];
                    out[j] = tmp;
                }
            }
        }
    }
    return out_count;
}

static ExtentInfo *collect_file_extents(const DirEntry *ents, int ent_count,
                                        const unsigned char name83[11],
                                        const DiskParams *dp, int *out_count) {
    ExtentInfo *exts = NULL;
    int ext_count = 0;
    int ext_cap = 16;
    int i;

    exts = (ExtentInfo *)malloc(sizeof(ExtentInfo) * (size_t)ext_cap);
    if (!exts) die("Out of memory.");

    for (i = 0; i < ent_count; i++) {
        const unsigned char *e = ents[i].bytes;
        int j;
        unsigned char masked[11];
        if (e[0] == 0xE5) continue;
        for (j = 0; j < 11; j++) masked[j] = (unsigned char)(e[1 + j] & 0x7F);
        if (memcmp(masked, name83, 11) == 0) {
            ExtentInfo subs[8];
            int subc = iter_dirent_subextents(e, dp, subs, 8);
            int s;
            for (s = 0; s < subc; s++) {
                int k, found = 0;
                for (k = 0; k < ext_count; k++) {
                    if (exts[k].extent_num == subs[s].extent_num) {
                        /* choose with more blocks, then rc */
                        if (subs[s].block_count > exts[k].block_count ||
                            (subs[s].block_count == exts[k].block_count && subs[s].rc > exts[k].rc)) {
                            exts[k] = subs[s];
                        }
                        found = 1;
                        break;
                    }
                }
                if (!found) {
                    if (ext_count >= ext_cap) {
                        ext_cap *= 2;
                        exts = (ExtentInfo *)realloc(exts, sizeof(ExtentInfo) * (size_t)ext_cap);
                        if (!exts) die("Out of memory.");
                    }
                    exts[ext_count++] = subs[s];
                }
            }
        }
    }

    *out_count = ext_count;
    return exts;
}

static int count_files(const unsigned char *raw, const DiskParams *dp) {
    int ent_count = 0;
    DirEntry *ents = parse_directory(raw, dp, &ent_count);
    int i;
    unsigned char (*names)[11] = NULL;
    int name_count = 0;
    int name_cap = 32;

    names = (unsigned char (*)[11])malloc(sizeof(unsigned char[11]) * (size_t)name_cap);
    if (!names) die("Out of memory.");

    for (i = 0; i < ent_count; i++) {
        unsigned char name83[11];
        int j, exists = 0;
        if (ents[i].bytes[0] == 0xE5) continue;
        memcpy(name83, ents[i].bytes + 1, 11);
        {
            int all_space = 1;
            for (j = 0; j < 11; j++) {
                if (name83[j] != 0x20) { all_space = 0; break; }
            }
            if (all_space) continue;
        }
        for (j = 0; j < name_count; j++) {
            if (memcmp(names[j], name83, 11) == 0) { exists = 1; break; }
        }
        if (!exists) {
            if (name_count >= name_cap) {
                name_cap *= 2;
                names = (unsigned char (*)[11])realloc(names, sizeof(unsigned char[11]) * (size_t)name_cap);
                if (!names) die("Out of memory.");
            }
            memcpy(names[name_count++], name83, 11);
        }
    }

    free(names);
    free(ents);
    return name_count;
}

static void list_files(const unsigned char *raw, const DiskParams *dp) {
    int ent_count = 0;
    DirEntry *ents = parse_directory(raw, dp, &ent_count);
    int i;

    /* Gather unique names */
    unsigned char (*names)[11] = NULL;
    int name_count = 0;
    int name_cap = 32;

    names = (unsigned char (*)[11])malloc(sizeof(unsigned char[11]) * (size_t)name_cap);
    if (!names) die("Out of memory.");

    for (i = 0; i < ent_count; i++) {
        unsigned char name83[11];
        int j, exists = 0;
        if (ents[i].bytes[0] == 0xE5) continue;
        memcpy(name83, ents[i].bytes + 1, 11);
        {
            int all_space = 1;
            for (j = 0; j < 11; j++) {
                if (name83[j] != 0x20) { all_space = 0; break; }
            }
            if (all_space) continue;
        }
        for (j = 0; j < name_count; j++) {
            if (memcmp(names[j], name83, 11) == 0) { exists = 1; break; }
        }
        if (!exists) {
            if (name_count >= name_cap) {
                name_cap *= 2;
                names = (unsigned char (*)[11])realloc(names, sizeof(unsigned char[11]) * (size_t)name_cap);
                if (!names) die("Out of memory.");
            }
            memcpy(names[name_count++], name83, 11);
        }
    }

    /* sort names for stable output */
    {
        int a, b;
        for (a = 0; a < name_count; a++) {
            for (b = a + 1; b < name_count; b++) {
                if (memcmp(names[b], names[a], 11) < 0) {
                    unsigned char tmp[11];
                    memcpy(tmp, names[a], 11);
                    memcpy(names[a], names[b], 11);
                    memcpy(names[b], tmp, 11);
                }
            }
        }
    }

    for (i = 0; i < name_count; i++) {
        int ext_count;
        ExtentInfo *exts = collect_file_extents(ents, ent_count, names[i], dp, &ext_count);
        int j;
        long size = 0;
        int used_blocks[256];
        int used_count = 0;
        char namebuf[20];

        for (j = 0; j < 256; j++) used_blocks[j] = 0;

        for (j = 0; j < ext_count; j++) {
            size += (long)exts[j].rc * RECORD_SIZE;
            {
                int k;
                for (k = 0; k < exts[j].block_count; k++) {
                    int b = exts[j].blocks[k];
                    if (b >= 0 && b < 256) used_blocks[b] = 1;
                }
            }
        }
        for (j = 0; j < 256; j++) if (used_blocks[j]) used_count++;

        /* construct name string */
        {
            unsigned char e[32];
            memset(e, 0x20, sizeof(e));
            memcpy(e + 1, names[i], 11);
            decode_name(e, namebuf, sizeof(namebuf));
        }

        printf("%-12s %8ld bytes  (%d blocks)\n", namebuf, size, used_count);
        if (exts) free(exts);
    }

    free(names);
    free(ents);
}

static unsigned char *read_file_from_image(const unsigned char *raw, const DiskParams *dp,
                                           const char *cpm_name, size_t *out_len) {
    int ent_count = 0;
    DirEntry *ents = parse_directory(raw, dp, &ent_count);
    unsigned char name83[11];
    int ext_count;
    ExtentInfo *exts;
    int i;
    unsigned char *out = NULL;
    size_t out_cap = 0;
    size_t out_len_local = 0;

    normalize_83(cpm_name, name83);
    exts = collect_file_extents(ents, ent_count, name83, dp, &ext_count);
    if (ext_count == 0) {
        free(ents);
        free(exts);
        die("File not found in directory.");
    }

    /* sort extents by extent_num */
    for (i = 0; i < ext_count; i++) {
        int j;
        for (j = i + 1; j < ext_count; j++) {
            if (exts[j].extent_num < exts[i].extent_num) {
                ExtentInfo tmp = exts[i];
                exts[i] = exts[j];
                exts[j] = tmp;
            }
        }
    }

    for (i = 0; i < ext_count; i++) {
        int rc = exts[i].rc;
        int k;
        size_t extent_bytes_len = (size_t)rc * RECORD_SIZE;
        size_t extent_bytes_written = 0;

        for (k = 0; k < exts[i].block_count; k++) {
            int b = exts[i].blocks[k];
            size_t off = (size_t)dp->block0_offset + (size_t)b * (size_t)dp->block_size;
            size_t to_copy = (size_t)dp->block_size;
            if (off + to_copy > 0x7FFFFFFFUL) {
                free(ents);
                free(exts);
                die("Offset overflow.");
            }
            if (out_len_local + to_copy > out_cap) {
                out_cap = (out_cap == 0) ? 4096 : out_cap * 2;
                while (out_len_local + to_copy > out_cap) out_cap *= 2;
                out = (unsigned char *)realloc(out, out_cap);
                if (!out) die("Out of memory.");
            }
            memcpy(out + out_len_local, raw + off, to_copy);
            out_len_local += to_copy;
            extent_bytes_written += to_copy;
        }

        if (extent_bytes_written > extent_bytes_len) {
            out_len_local -= (extent_bytes_written - extent_bytes_len);
        }
    }

    free(ents);
    free(exts);
    *out_len = out_len_local;
    return out;
}

static int rm_file(unsigned char *raw_mut, const DiskParams *dp, const char *cpm_name) {
    int ent_count = 0;
    DirEntry *ents = parse_directory(raw_mut, dp, &ent_count);
    unsigned char name83[11];
    int i, removed = 0;

    normalize_83(cpm_name, name83);
    for (i = 0; i < ent_count; i++) {
        unsigned char masked[11];
        int j;
        if (ents[i].bytes[0] == 0xE5) continue;
        for (j = 0; j < 11; j++) masked[j] = (unsigned char)(ents[i].bytes[1 + j] & 0x7F);
        if (memcmp(masked, name83, 11) == 0) {
            raw_mut[ents[i].offset] = 0xE5;
            removed++;
        }
    }

    free(ents);
    if (removed == 0) die("File not found.");
    return removed;
}

static int *build_allocation_bitmap(const unsigned char *raw, size_t raw_len, const DiskParams *dp, int *out_total) {
    int total_blocks;
    int i;
    int ent_count = 0;
    DirEntry *ents;
    int *used;

    if (!raw) die("RAW is NULL.");

    if ((long)raw_len - (long)dp->block0_offset < 0) total_blocks = 0;
    else total_blocks = (int)(((long)raw_len - (long)dp->block0_offset) / dp->block_size);

    used = (int *)calloc((size_t)total_blocks, sizeof(int));
    if (!used) die("Out of memory.");
    if (total_blocks > 0) used[0] = 1; /* block 0 reserved */

    ents = parse_directory(raw, dp, &ent_count);
    for (i = 0; i < ent_count; i++) {
        const unsigned char *e = ents[i].bytes;
        int j;
        if (e[0] == 0xE5) continue;
        {
            int all_space = 1;
            for (j = 1; j < 12; j++) {
                if (e[j] != 0x20) { all_space = 0; break; }
            }
            if (all_space) continue;
        }
        for (j = 16; j < 32; j++) {
            unsigned char b = e[j];
            if (b == 0) continue;
            if ((int)b < total_blocks) {
                used[(int)b] = 1;
            } else {
                free(used);
                free(ents);
                die("Directory references block beyond disk size.");
            }
        }
    }

    free(ents);
    *out_total = total_blocks;
    return used;
}

static void put_file(unsigned char *raw_mut, size_t raw_len, const DiskParams *dp,
                     const char *local_path, const char *as_name, int overwrite) {
    size_t data_len = 0;
    unsigned char *data = read_file_bytes(local_path, &data_len);
    char *cpm_name = NULL;
    unsigned char name83[11];
    int ent_count = 0;
    DirEntry *ents = parse_directory(raw_mut, dp, &ent_count);
    int i;

    if (as_name && *as_name) {
        cpm_name = dupstr(as_name);
    } else {
        cpm_name = basename_only(local_path);
    }
    normalize_83(cpm_name, name83);

    /* delete existing entries if overwrite */
    {
        int found = 0;
        for (i = 0; i < ent_count; i++) {
            unsigned char masked[11];
            int j;
            if (ents[i].bytes[0] == 0xE5) continue;
            for (j = 0; j < 11; j++) masked[j] = (unsigned char)(ents[i].bytes[1 + j] & 0x7F);
            if (memcmp(masked, name83, 11) == 0) {
                found = 1;
                if (overwrite) raw_mut[ents[i].offset] = 0xE5;
            }
        }
        if (found && !overwrite) {
            free(ents);
            free(data);
            free(cpm_name);
            die("File already exists. Use --overwrite to replace.");
        }
        if (found && overwrite) {
            free(ents);
            ents = parse_directory(raw_mut, dp, &ent_count);
        }
    }

    /* allocation bitmap */
    {
        int total_blocks = 0;
        int *used = build_allocation_bitmap(raw_mut, raw_len, dp, &total_blocks);
        int records_total = (data_len > 0) ? (int)((data_len + RECORD_SIZE - 1) / RECORD_SIZE) : 0;
        int subextents_total = (records_total > 0) ? (int)((records_total + 127) / 128) : 1;
        int group_size = dp->exm + 1;
        int ptrs_per_sub = 16 / group_size;
        int groups_needed = (subextents_total + group_size - 1) / group_size;
        int needed_blocks = (data_len > 0) ? (int)((data_len + dp->block_size - 1) / dp->block_size) : 0;
        int *free_blocks = (int *)malloc(sizeof(int) * (size_t)total_blocks);
        int free_block_count = 0;
        int *free_dir_offsets = (int *)malloc(sizeof(int) * (size_t)ent_count);
        int free_dir_count = 0;
        int data_p = 0;
        int g;

        if (!free_blocks || !free_dir_offsets) die("Out of memory.");
        for (i = 1; i < total_blocks; i++) {
            if (!used[i]) free_blocks[free_block_count++] = i;
        }
        for (i = 0; i < ent_count; i++) {
            if (ents[i].bytes[0] == 0xE5) free_dir_offsets[free_dir_count++] = ents[i].offset;
        }

        if (free_block_count < needed_blocks) {
            free(used); free(free_blocks); free(free_dir_offsets); free(ents); free(data); free(cpm_name);
            die("Not enough free space.");
        }
        if (free_dir_count < groups_needed) {
            free(used); free(free_blocks); free(free_dir_offsets); free(ents); free(data); free(cpm_name);
            die("Not enough free directory entries.");
        }

        for (g = 0; g < groups_needed; g++) {
            int remaining = subextents_total - g * group_size;
            int sub_in_group = (remaining < group_size) ? remaining : group_size;
            int last_sub = sub_in_group - 1;
            int last_extent_num = g * group_size + last_sub;
            unsigned char ent[32];
            int sub;

            memset(ent, 0, sizeof(ent));
            ent[0] = 0x00;
            memcpy(ent + 1, name83, 11);
            ent[12] = (unsigned char)(last_extent_num & 0x1F);
            ent[13] = 0x00;
            ent[14] = (unsigned char)((last_extent_num >> 5) & 0xFF);
            if (records_total) {
                int rc = records_total - last_extent_num * 128;
                if (rc < 0) rc = 0;
                if (rc > 128) rc = 128;
                ent[15] = (unsigned char)rc;
            } else {
                ent[15] = 0x00;
            }

            for (sub = 0; sub < sub_in_group; sub++) {
                int extent_index = 0;
                int remaining_records = 0;
                int rc_sub = 0;
                int need_bytes = 0;
                int need_blocks = 0;
                int bi = 0;
                int start = 0;

                extent_index = g * group_size + sub;
                remaining_records = records_total - extent_index * 128;
                if (records_total > 0) {
                    if (remaining_records < 0) rc_sub = 0;
                    else rc_sub = (remaining_records < 128) ? remaining_records : 128;
                } else {
                    rc_sub = 0;
                }
                need_bytes = rc_sub * RECORD_SIZE;
                need_blocks = (need_bytes > 0) ? (need_bytes + dp->block_size - 1) / dp->block_size : 0;
                start = 16 + sub * ptrs_per_sub;

                for (bi = 0; bi < need_blocks; bi++) {
                    int b = free_blocks[0];
                    int off = dp->block0_offset + b * dp->block_size;
                    int chunk_len = (int)(data_len - data_p);
                    if (chunk_len > dp->block_size) chunk_len = dp->block_size;
                    if (chunk_len > 0) {
                        memcpy(raw_mut + off, data + data_p, (size_t)chunk_len);
                        data_p += chunk_len;
                    }
                    if (chunk_len < dp->block_size) {
                        memset(raw_mut + off + chunk_len, 0x1A, (size_t)(dp->block_size - chunk_len));
                    }
                    /* shift free_blocks */
                    memmove(free_blocks, free_blocks + 1, sizeof(int) * (size_t)(free_block_count - 1));
                    free_block_count--;
                    used[b] = 1;
                    ent[start + bi] = (unsigned char)b;
                }
            }

            /* write directory entry */
            {
                int slot = free_dir_offsets[0];
                memmove(free_dir_offsets, free_dir_offsets + 1, sizeof(int) * (size_t)(free_dir_count - 1));
                free_dir_count--;
                memcpy(raw_mut + slot, ent, 32);
            }
        }

        free(used);
        free(free_blocks);
        free(free_dir_offsets);
    }

    free(ents);
    free(data);
    free(cpm_name);
}

/* ---------------- Sector skew auto-detect ---------------- */

static int resolve_sector_skew(IMDImage *img, const char *skew_spec, int verbose, int **out_skew, int *out_len) {
    int is_auto = 0;
    int *parsed = parse_skew_spec(skew_spec, img, out_len, &is_auto);
    if (!is_auto) {
        *out_skew = parsed;
        return 1;
    }

    /* Auto-detect */
    if (img->track_count == 0) {
        *out_len = 0;
        *out_skew = NULL;
        return 1;
    }

    {
        IMDTrack *sample = &img->tracks[0];
        int nsec = sample->nsec;
        int i;
        int sequential = 1;
        int *phys_ids = (int *)malloc(sizeof(int) * (size_t)nsec);
        if (!phys_ids) die("Out of memory.");
        for (i = 0; i < nsec; i++) phys_ids[i] = (int)sample->sec_ids[i];
        for (i = 0; i < nsec; i++) {
            if (phys_ids[i] != i + 1) { sequential = 0; break; }
        }
        if (!sequential) {
            free(phys_ids);
            *out_skew = identity_skew_for_tracks(img, out_len);
            return 1;
        }
        free(phys_ids);

        /* try candidates */
        {
            int cand_count = 0;
            int **cands = candidate_skews_for_nsec(nsec, &cand_count);
            int best_active = -1;
            int best_dirsecs = -1;
            int best_prefix = -1;
            int best_gaps = 0;
            int best_range = 0;
            int *best = NULL;
            int best_len = 0;
            int c;
            int max_cyl = 0, max_head = 0;
            int bytes_per_track = nsec * ssize_from_code(img->tracks[0].ssize_code);

            for (c = 0; c < img->track_count; c++) {
                if (img->tracks[c].cyl > max_cyl) max_cyl = img->tracks[c].cyl;
                if ((img->tracks[c].head_raw & 0x3F) > max_head) max_head = (img->tracks[c].head_raw & 0x3F);
            }
            max_cyl += 1;
            max_head += 1;

            for (c = 0; c < cand_count; c++) {
                size_t raw_len = 0;
                unsigned char *raw = build_raw(img, cands[c], nsec, &raw_len);
                int active, dirsecs, dir_c, dir_h, dir_list_len;
                int *dir_list = NULL;
                int prefix = 0, gaps = 0, range = 0;
                int score_active, score_dir, score_prefix, score_gaps, score_range;
                score_directory(raw,
                                max_cyl,
                                max_head,
                                bytes_per_track,
                                nsec,
                                ssize_from_code(img->tracks[0].ssize_code),
                                &active, &dirsecs, &dir_c, &dir_h, &dir_list, &dir_list_len);

                if (dir_list_len > 0) {
                    int *sorted = (int *)malloc(sizeof(int) * (size_t)dir_list_len);
                    int i;
                    memcpy(sorted, dir_list, sizeof(int) * (size_t)dir_list_len);
                    /* sort */
                    for (i = 0; i < dir_list_len; i++) {
                        int j;
                        for (j = i + 1; j < dir_list_len; j++) {
                            if (sorted[j] < sorted[i]) {
                                int tmp = sorted[i];
                                sorted[i] = sorted[j];
                                sorted[j] = tmp;
                            }
                        }
                    }
                    range = sorted[dir_list_len - 1] - sorted[0];
                    gaps = 0;
                    for (i = 0; i < dir_list_len - 1; i++) {
                        if (sorted[i + 1] - sorted[i] > 1) gaps++;
                    }
                    prefix = 0;
                    {
                        int k = 1;
                        int found;
                        while (1) {
                            found = 0;
                            for (i = 0; i < dir_list_len; i++) if (sorted[i] == k) { found = 1; break; }
                            if (!found) break;
                            prefix++;
                            k++;
                        }
                    }
                    free(sorted);
                } else {
                    range = 1000000;
                    gaps = 1000000;
                    prefix = 0;
                }

                if (verbose) {
                    int i;
                    printf("Auto-detect skew candidate [");
                    for (i = 0; i < nsec; i++) {
                        printf("%d%s", cands[c][i], (i == nsec - 1) ? "" : ",");
                    }
                    printf("]: active_entries=%d dirsecs=%d prefix=%d gaps=%d range=%d\n",
                           active, dirsecs, prefix, gaps, range);
                }

                score_active = active;
                score_dir = dirsecs;
                score_prefix = prefix;
                score_gaps = -gaps;
                score_range = -range;

                if (best == NULL ||
                    score_active > best_active ||
                    (score_active == best_active && score_dir > best_dirsecs) ||
                    (score_active == best_active && score_dir == best_dirsecs && score_prefix > best_prefix) ||
                    (score_active == best_active && score_dir == best_dirsecs && score_prefix == best_prefix && score_gaps > best_gaps) ||
                    (score_active == best_active && score_dir == best_dirsecs && score_prefix == best_prefix && score_gaps == best_gaps && score_range > best_range)) {
                    best = cands[c];
                    best_len = nsec;
                    best_active = score_active;
                    best_dirsecs = score_dir;
                    best_prefix = score_prefix;
                    best_gaps = score_gaps;
                    best_range = score_range;
                }

                if (dir_list) free(dir_list);
                if (raw) free(raw);
            }

            if (!best || best_active < 2) {
                /* fallback to default */
                if (nsec == 9) {
                    *out_skew = (int *)malloc(sizeof(int) * 9);
                    if (!*out_skew) die("Out of memory.");
                    for (i = 0; i < 9; i++) (*out_skew)[i] = DEFAULT_SKEW_9[i];
                    *out_len = 9;
                } else {
                    *out_skew = identity_skew_for_tracks(img, out_len);
                }
            } else {
                *out_skew = (int *)malloc(sizeof(int) * (size_t)best_len);
                if (!*out_skew) die("Out of memory.");
                for (i = 0; i < best_len; i++) (*out_skew)[i] = best[i];
                *out_len = best_len;
            }

            /* free candidates */
            for (c = 0; c < cand_count; c++) {
                if (cands[c]) free(cands[c]);
            }
            free(cands);
        }
    }

    return 1;
}

/* ---------------- SPDTAB patching ---------------- */

static unsigned char VFII_OPRTAB_ADRTAB_SIG[] = {
    0xF0, 0x70, 0xE0, 0x60,
    0xD0, 0x50, 0xC0, 0x40,
    0xB0, 0x30, 0xA0, 0x20,
    0x90, 0x10, 0x80, 0x00,
    0x0E, 0x0D, 0x0B, 0x07
};

#define VFII_SPDTAB_BACKOFFSET 8

typedef struct {
    int spdtab_off;
    int oprtab_off;
    unsigned char spdtab[4];
    int cyl;
    int head;
    int sec;
    int sec_off;
} SpdTabHit;

static void off_to_chs(int off, int bytes_per_track, int heads, int sec_size,
                       int *cyl, int *head, int *sec, int *sec_off) {
    int track_num = off / bytes_per_track;
    int in_track = off % bytes_per_track;
    *cyl = track_num / heads;
    *head = track_num % heads;
    *sec = in_track / sec_size + 1;
    *sec_off = in_track % sec_size;
}

static SpdTabHit *find_spdtab_hits(const unsigned char *raw, size_t raw_len, IMDImage *img, int *out_count) {
    int i;
    int sec_size;
    int heads;
    int bytes_per_track;
    SpdTabHit *hits = NULL;
    int hit_count = 0;
    int hit_cap = 8;

    if (img->track_count == 0) {
        *out_count = 0;
        return NULL;
    }

    sec_size = ssize_from_code(img->tracks[0].ssize_code);
    heads = (img->tracks[0].head_raw & 0x3F) + 1;
    bytes_per_track = img->tracks[0].nsec * sec_size;

    hits = (SpdTabHit *)malloc(sizeof(SpdTabHit) * (size_t)hit_cap);
    if (!hits) die("Out of memory.");

    for (i = 0; i + (int)sizeof(VFII_OPRTAB_ADRTAB_SIG) <= (int)raw_len; i++) {
        if (memcmp(raw + i, VFII_OPRTAB_ADRTAB_SIG, sizeof(VFII_OPRTAB_ADRTAB_SIG)) == 0) {
            int spdtab_off = i - VFII_SPDTAB_BACKOFFSET;
            if (spdtab_off < 0) continue;
            if (spdtab_off + 4 > (int)raw_len) continue;
            {
                unsigned char spd[4];
                int valid = 1;
                int k;
                for (k = 0; k < 4; k++) spd[k] = raw[spdtab_off + k];
                for (k = 0; k < 4; k++) {
                    if ((spd[k] & 0x7C) != 0) { valid = 0; break; }
                }
                if (!valid) continue;

                if (hit_count >= hit_cap) {
                    hit_cap *= 2;
                    hits = (SpdTabHit *)realloc(hits, sizeof(SpdTabHit) * (size_t)hit_cap);
                    if (!hits) die("Out of memory.");
                }
                hits[hit_count].spdtab_off = spdtab_off;
                hits[hit_count].oprtab_off = i;
                memcpy(hits[hit_count].spdtab, spd, 4);
                off_to_chs(spdtab_off, bytes_per_track, heads, sec_size,
                           &hits[hit_count].cyl, &hits[hit_count].head,
                           &hits[hit_count].sec, &hits[hit_count].sec_off);
                hit_count++;
            }
        }
    }

    *out_count = hit_count;
    return hits;
}

static void decode_spd_byte(unsigned char b, char *out, size_t out_len) {
    const char *klass = (b & 0x80) ? "5\"" : "8\"";
    int step = b & 0x03;
    if (out_len == 0) return;
    sprintf(out, "0x%02X (%s, step=%d)", b, klass, step);
}

static unsigned char apply_spd_patch(unsigned char val, int klass, int step) {
    if (klass == 8) val &= 0x7F;
    else if (klass == 5) val |= 0x80;
    else die("klass must be 8 or 5");

    if (step >= 0) {
        if (step < 0 || step > 3) die("--step must be 0..3");
        val = (unsigned char)((val & ~0x03) | (step & 0x03));
    }
    return val;
}

/* ---------------- Console patching ---------------- */

/*
 * NOTE: The original Python file refers to SBC548N fixed offsets but does not
 * define their values. Keep placeholders here; use --scan for now or update
 * these offsets to match your target image.
 */
static int SBC548N_STATUS_OFFSETS[2] = { 0x1EB5, 0x1EC7 };
static int SBC548N_DATA_OFFSETS[2] = { 0x1EC2, 0x1ED6 };

typedef struct {
    int cyl;
    int head;
    int sec;
    int stype;
    int size;
    int data_off;
} ImdSectorPayload;

static ImdSectorPayload *iter_imd_sector_payloads(const unsigned char *blob, size_t len, int *out_count) {
    size_t cut = 0;
    size_t p;
    int cap = 32;
    int count = 0;
    ImdSectorPayload *out = (ImdSectorPayload *)malloc(sizeof(ImdSectorPayload) * (size_t)cap);
    if (!out) die("Out of memory.");

    if (!find_byte(blob, len, 0x1A, &cut)) die("No 0x1A (EOF marker) found in IMD header.");
    p = cut + 1;

    while (p < len) {
        unsigned char mode = blob[p++];
        unsigned char cyl, head_raw, nsec, ssize_code;
        unsigned char *sec_ids;
        unsigned char *cyl_map;
        unsigned char *head_map;
        int has_cyl_map, has_head_map;
        int ssize;
        int i;

        if (p >= len) break;
        cyl = blob[p];
        head_raw = blob[p + 1];
        nsec = blob[p + 2];
        ssize_code = blob[p + 3];
        p += 4;

        has_cyl_map = (head_raw & 0x80) ? 1 : 0;
        has_head_map = (head_raw & 0x40) ? 1 : 0;

        sec_ids = (unsigned char *)malloc(nsec);
        if (!sec_ids) die("Out of memory.");
        memcpy(sec_ids, blob + p, nsec);
        p += nsec;

        if (has_cyl_map) {
            cyl_map = (unsigned char *)malloc(nsec);
            if (!cyl_map) die("Out of memory.");
            memcpy(cyl_map, blob + p, nsec);
            p += nsec;
        } else {
            cyl_map = (unsigned char *)malloc(nsec);
            if (!cyl_map) die("Out of memory.");
            memset(cyl_map, cyl, nsec);
        }

        if (has_head_map) {
            head_map = (unsigned char *)malloc(nsec);
            if (!head_map) die("Out of memory.");
            memcpy(head_map, blob + p, nsec);
            p += nsec;
        } else {
            head_map = (unsigned char *)malloc(nsec);
            if (!head_map) die("Out of memory.");
            memset(head_map, head_raw & 0x3F, nsec);
        }

        ssize = ssize_from_code(ssize_code);
        for (i = 0; i < nsec; i++) {
            unsigned char stype = blob[p++];
            if (stype == 0x00) {
                continue;
            } else if (stype == 0x01 || stype == 0x03 || stype == 0x05 || stype == 0x07) {
                if (count >= cap) {
                    cap *= 2;
                    out = (ImdSectorPayload *)realloc(out, sizeof(ImdSectorPayload) * (size_t)cap);
                    if (!out) die("Out of memory.");
                }
                out[count].cyl = cyl_map[i];
                out[count].head = head_map[i];
                out[count].sec = sec_ids[i];
                out[count].stype = stype;
                out[count].size = ssize;
                out[count].data_off = (int)p;
                count++;
                p += (size_t)ssize;
            } else if (stype == 0x02 || stype == 0x04 || stype == 0x06 || stype == 0x08) {
                p += 1;
            } else {
                die("Unknown IMD sector record type.");
            }
        }

        free(sec_ids);
        free(cyl_map);
        free(head_map);
        (void)mode;
    }

    *out_count = count;
    return out;
}

static int mode_of_values(const unsigned char *vals, int count) {
    int freq[256];
    int i;
    int best = 0;
    int best_count = -1;
    for (i = 0; i < 256; i++) freq[i] = 0;
    for (i = 0; i < count; i++) freq[vals[i]]++;
    for (i = 0; i < 256; i++) {
        if (freq[i] > best_count) {
            best_count = freq[i];
            best = i;
        }
    }
    return best;
}

static int patch_fixed_sbc548n(unsigned char *blob_mut, size_t blob_len,
                               int new_status, int new_data,
                               int from_status, int from_status_set,
                               int from_data, int from_data_set,
                               int verbose, int *out_patched_count) {
    int checks[4][2];
    int i;
    int cur_status_a, cur_status_b, cur_data_a, cur_data_b;
    int cur_status, cur_data;
    int patched = 0;

    checks[0][0] = SBC548N_STATUS_OFFSETS[0]; checks[0][1] = 0xDB;
    checks[1][0] = SBC548N_DATA_OFFSETS[0];   checks[1][1] = 0xDB;
    checks[2][0] = SBC548N_STATUS_OFFSETS[1]; checks[2][1] = 0xDB;
    checks[3][0] = SBC548N_DATA_OFFSETS[1];   checks[3][1] = 0xD3;

    for (i = 0; i < 4; i++) {
        int off = checks[i][0];
        int op = checks[i][1];
        if (off <= 0 || (size_t)off >= blob_len) {
            die("Fixed offset out of range (use --scan or set SBC548N offsets).");
        }
        if (blob_mut[off - 1] != op) {
            die("Fixed-offset opcode check failed (use --scan).");
        }
    }

    cur_status_a = blob_mut[SBC548N_STATUS_OFFSETS[0]];
    cur_status_b = blob_mut[SBC548N_STATUS_OFFSETS[1]];
    cur_data_a = blob_mut[SBC548N_DATA_OFFSETS[0]];
    cur_data_b = blob_mut[SBC548N_DATA_OFFSETS[1]];

    cur_status = from_status_set ? from_status : cur_status_a;
    cur_data = from_data_set ? from_data : cur_data_a;

    if (cur_status_a != cur_status_b) die("Fixed status operand bytes do not match (use --scan).");
    if (cur_data_a != cur_data_b) die("Fixed data operand bytes do not match (use --scan).");
    if (from_status_set && cur_status_a != from_status) die("--from-status mismatch at fixed offset.");
    if (from_data_set && cur_data_a != from_data) die("--from-data mismatch at fixed offset.");

    if (verbose) {
        printf("Fixed-offset mode: current status=0x%02X, data=0x%02X\n", cur_status, cur_data);
        printf("  status offsets: 0x%04X, 0x%04X\n", SBC548N_STATUS_OFFSETS[0], SBC548N_STATUS_OFFSETS[1]);
        printf("  data offsets:   0x%04X, 0x%04X\n", SBC548N_DATA_OFFSETS[0], SBC548N_DATA_OFFSETS[1]);
    }

    for (i = 0; i < 2; i++) {
        int off = SBC548N_STATUS_OFFSETS[i];
        blob_mut[off] = (unsigned char)new_status;
        patched++;
    }
    for (i = 0; i < 2; i++) {
        int off = SBC548N_DATA_OFFSETS[i];
        blob_mut[off] = (unsigned char)new_data;
        patched++;
    }

    *out_patched_count = patched;
    return 1;
}

static int scan_and_patch(unsigned char *blob_mut, size_t blob_len,
                          int new_status, int new_data,
                          int from_status, int from_status_set,
                          int from_data, int from_data_set,
                          int verbose, int *out_patched_count) {
    int payload_count = 0;
    ImdSectorPayload *payloads = iter_imd_sector_payloads(blob_mut, blob_len, &payload_count);
    int *status_ops = (int *)malloc(sizeof(int) * (size_t)payload_count * 16);
    int *in_ops = (int *)malloc(sizeof(int) * (size_t)payload_count * 16);
    int *out_ops = (int *)malloc(sizeof(int) * (size_t)payload_count * 16);
    int status_count = 0, in_count = 0, out_count = 0;
    int i;

    if (!status_ops || !in_ops || !out_ops) die("Out of memory.");

    for (i = 0; i < payload_count; i++) {
        const ImdSectorPayload *sec = &payloads[i];
        const unsigned char *buf = blob_mut + sec->data_off;
        int size = sec->size;
        int idx;

        /* status patterns: DB xx E6 01/02 */
        for (idx = 0; idx + 4 <= size; idx++) {
            if (buf[idx] == 0xDB && buf[idx + 2] == 0xE6 && (buf[idx + 3] == 0x01 || buf[idx + 3] == 0x02)) {
                status_ops[status_count++] = sec->data_off + idx + 1;
            }
        }

        /* out patterns: D3 xx */
        for (idx = 0; idx + 2 <= size; idx++) {
            if (buf[idx] == 0xD3) {
                out_ops[out_count++] = sec->data_off + idx + 1;
            }
        }

        /* in patterns: DB xx with RET nearby */
        for (idx = 0; idx + 2 <= size; idx++) {
            if (buf[idx] == 0xDB) {
                int w;
                int found = 0;
                for (w = idx + 2; w < size && w < idx + 12; w++) {
                    if (buf[w] == 0xC9) { found = 1; break; }
                }
                if (found) in_ops[in_count++] = sec->data_off + idx + 1;
            }
        }
    }

    if (status_count == 0) die("Scan mode: no status patterns found.");

    {
        unsigned char *vals = (unsigned char *)malloc((size_t)status_count);
        int cur_status;
        int cur_data;
        int patched = 0;
        int j;
        int data_candidate_count = 0;
        unsigned char *data_candidates = (unsigned char *)malloc((size_t)(in_count + out_count + 1));
        if (!vals || !data_candidates) die("Out of memory.");

        for (j = 0; j < status_count; j++) vals[j] = blob_mut[status_ops[j]];
        cur_status = from_status_set ? from_status : mode_of_values(vals, status_count);

        for (j = 0; j < out_count; j++) {
            unsigned char v = blob_mut[out_ops[j]];
            if (v != cur_status) data_candidates[data_candidate_count++] = v;
        }
        for (j = 0; j < in_count; j++) {
            unsigned char v = blob_mut[in_ops[j]];
            if (v != cur_status) data_candidates[data_candidate_count++] = v;
        }
        if (data_candidate_count == 0) die("Scan mode: could not infer data port.");
        cur_data = from_data_set ? from_data : mode_of_values(data_candidates, data_candidate_count);

        if (verbose) {
            printf("Scan mode: inferred status=0x%02X, data=0x%02X\n", cur_status, cur_data);
            printf("  status hits: %d  in hits: %d  out hits: %d\n", status_count, in_count, out_count);
        }

        for (j = 0; j < status_count; j++) {
            int o = status_ops[j];
            if (blob_mut[o] == cur_status) { blob_mut[o] = (unsigned char)new_status; patched++; }
        }
        for (j = 0; j < in_count; j++) {
            int o = in_ops[j];
            if (blob_mut[o] == cur_data) { blob_mut[o] = (unsigned char)new_data; patched++; }
        }
        for (j = 0; j < out_count; j++) {
            int o = out_ops[j];
            if (blob_mut[o] == cur_data) { blob_mut[o] = (unsigned char)new_data; patched++; }
        }

        free(vals);
        free(data_candidates);
        *out_patched_count = patched;
    }

    free(payloads);
    free(status_ops);
    free(in_ops);
    free(out_ops);
    return 1;
}

/* ---------------- Commands ---------------- */

static void cmd_info(const char *imd_path, const char *skew_spec, int skew_verbose, int long_list) {
    IMDImage img = parse_imd(imd_path);
    int *skew = NULL;
    int skew_len = 0;
    size_t raw_len = 0;
    unsigned char *raw;
    DiskParams dp;
    int cyls, heads, secs, ssize;
    size_t expected_bytes;
    char *header_text;
    int mode_counts[256];
    int mi;

    resolve_sector_skew(&img, skew_spec, skew_verbose, &skew, &skew_len);
    raw = build_raw(&img, skew, skew_len, &raw_len);
    dp = infer_params(raw, raw_len, &img, skew, skew_len);

    header_text = (char *)malloc(img.header_len + 1);
    if (!header_text) die("Out of memory.");
    memcpy(header_text, img.header, img.header_len);
    header_text[img.header_len] = '\0';

    cyls = dp.cyls;
    heads = dp.heads;
    secs = dp.phys_secs_per_track;
    ssize = dp.phys_sec_size;
    expected_bytes = (size_t)cyls * (size_t)heads * (size_t)secs * (size_t)ssize;

    for (mi = 0; mi < 256; mi++) mode_counts[mi] = 0;
    for (mi = 0; mi < img.track_count; mi++) {
        int m = img.tracks[mi].mode;
        if (m >= 0 && m < 256) mode_counts[m]++;
    }

    printf("IMD: %s\n", imd_path);
    {
        char *first_line = strtok(header_text, "\r\n");
        printf("Header: %s\n", first_line ? first_line : "(no header text)");
    }

    {
        const char *spec = (skew_spec && *skew_spec) ? skew_spec : "auto";
        const char *skew_note;
        int i;
        if (strieq(spec, "auto") || spec[0] == '\0') skew_note = "auto";
        else if (strieq(spec, "none") || strieq(spec, "identity") || strieq(spec, "phys") || strieq(spec, "physical")) skew_note = "identity";
        else if (strieq(spec, "default") || strieq(spec, "vfii")) skew_note = "vfii/default";
        else skew_note = "explicit";
        printf("Selected skew (logical->physical): ");
        for (i = 0; i < skew_len; i++) {
            printf("%d%s", skew[i], (i == skew_len - 1) ? "" : ", ");
        }
        printf(" (%s)\n", skew_note);
    }

    printf("\nGeometry (physical, from IMD):\n");
    printf("  Cylinders : %d\n", cyls);
    printf("  Heads     : %d\n", heads);
    printf("  Sectors/tr: %d\n", secs);
    printf("  Sec size  : %d bytes\n", ssize);
    printf("  Track modes: ");
    {
        int first = 1;
        for (mi = 0; mi < 256; mi++) {
            if (mode_counts[mi] > 0) {
                if (!first) printf(", ");
                printf("%d:%s x%d", mi, mode_desc(mi), mode_counts[mi]);
                first = 0;
            }
        }
        if (first) printf("none");
        printf("\n");
    }
    printf("  Raw size  : %lu bytes (expected %lu bytes)\n",
           (unsigned long)raw_len, (unsigned long)expected_bytes);
    if (raw_len != expected_bytes) {
        printf("  NOTE: Raw size differs from simple cyl*head*sec*size; IMD may have missing tracks.\n");
    }

    printf("\nCP/M layout (inferred for this image):\n");
    printf("  Directory track: C=%d H=%d\n", dp.dir_cyl, dp.dir_head);
    {
        int i;
        printf("  Directory sectors (logical): ");
        for (i = 0; i < dp.dir_phys_secs_len; i++) {
            printf("%d%s", dp.dir_phys_secs[i], (i == dp.dir_phys_secs_len - 1) ? "" : ", ");
        }
        printf("\n");
        printf("  Directory sectors (physical IDs): ");
        for (i = 0; i < dp.dir_phys_secs_len; i++) {
            int sid = dp.dir_phys_secs[i];
            int phys = (sid > 0 && sid <= dp.sec_skew_len) ? dp.sec_skew[sid - 1] : sid;
            printf("%d%s", phys, (i == dp.dir_phys_secs_len - 1) ? "" : ", ");
        }
        printf("\n");
    }
    printf("  Allocation block size: %d bytes\n", dp.block_size);
    printf("  EXM: %d  (block pointers per extent: %d)\n", dp.exm, 16 / (dp.exm + 1));
    printf("  Block0 offset into RAW: %d bytes\n", dp.block0_offset);

    printf("\nDisk usage:\n");
    {
        int total_blocks = 0;
        int *used = build_allocation_bitmap(raw, raw_len, &dp, &total_blocks);
        int free_blocks = 0;
        int i;
        int file_count = count_files(raw, &dp);
        for (i = 0; i < total_blocks; i++) if (!used[i]) free_blocks++;
        printf("  Total blocks: %d\n", total_blocks);
        printf("  Used blocks : %d\n", total_blocks - free_blocks);
        printf("  Free blocks : %d\n", free_blocks);
        printf("  Free bytes  : %ld\n", (long)free_blocks * dp.block_size);
        printf("  File count  : %d\n", file_count);
        if (long_list) {
            printf("\nFiles:\n");
            list_files(raw, &dp);
        }
        free(used);
    }

    free(header_text);
    free(raw);
    free(skew);
    free(dp.sec_skew);
    if (dp.dir_phys_secs) free(dp.dir_phys_secs);
    free_imd(&img);
}

static void cmd_ls(const char *imd_path, const char *skew_spec, int skew_verbose) {
    IMDImage img = parse_imd(imd_path);
    int *skew = NULL;
    int skew_len = 0;
    size_t raw_len = 0;
    unsigned char *raw;
    DiskParams dp;

    resolve_sector_skew(&img, skew_spec, skew_verbose, &skew, &skew_len);
    raw = build_raw(&img, skew, skew_len, &raw_len);
    dp = infer_params(raw, raw_len, &img, skew, skew_len);

    list_files(raw, &dp);

    free(raw);
    free(skew);
    free(dp.sec_skew);
    if (dp.dir_phys_secs) free(dp.dir_phys_secs);
    free_imd(&img);
}

static void cmd_get(const char *imd_path, const char *cpm_name, const char *out_path,
                    const char *skew_spec, int skew_verbose) {
    IMDImage img = parse_imd(imd_path);
    int *skew = NULL;
    int skew_len = 0;
    size_t raw_len = 0;
    unsigned char *raw;
    DiskParams dp;
    size_t data_len = 0;
    unsigned char *data;

    resolve_sector_skew(&img, skew_spec, skew_verbose, &skew, &skew_len);
    raw = build_raw(&img, skew, skew_len, &raw_len);
    dp = infer_params(raw, raw_len, &img, skew, skew_len);

    data = read_file_from_image(raw, &dp, cpm_name, &data_len);
    write_file_bytes(out_path ? out_path : cpm_name, data, data_len);
    printf("Wrote %lu bytes to %s\n", (unsigned long)data_len, out_path ? out_path : cpm_name);

    free(data);
    free(raw);
    free(skew);
    free(dp.sec_skew);
    if (dp.dir_phys_secs) free(dp.dir_phys_secs);
    free_imd(&img);
}

static void cmd_rm(const char *imd_path, const char *cpm_name, const char *out_path,
                   const char *skew_spec, int skew_verbose) {
    IMDImage img = parse_imd(imd_path);
    int *skew = NULL;
    int skew_len = 0;
    size_t raw_len = 0;
    unsigned char *raw;
    DiskParams dp;
    size_t out_len = 0;
    unsigned char *out_imd;
    int n;
    char *out_path_local = NULL;

    resolve_sector_skew(&img, skew_spec, skew_verbose, &skew, &skew_len);
    raw = build_raw(&img, skew, skew_len, &raw_len);
    dp = infer_params(raw, raw_len, &img, skew, skew_len);

    n = rm_file(raw, &dp, cpm_name);
    out_imd = rebuild_imd(&img, raw, raw_len, skew, skew_len, &out_len);

    if (out_path) out_path_local = dupstr(out_path);
    else out_path_local = default_out_imd(imd_path, "_patched.IMD");
    write_file_bytes(out_path_local, out_imd, out_len);

    printf("Removed %d directory entries for %s. Wrote %s\n", n, cpm_name, out_path_local);

    free(out_path_local);
    free(out_imd);
    free(raw);
    free(skew);
    free(dp.sec_skew);
    if (dp.dir_phys_secs) free(dp.dir_phys_secs);
    free_imd(&img);
}

static void cmd_put(const char *imd_path, const char *local_path, const char *as_name,
                    int overwrite, const char *out_path, int inplace, int verify,
                    const char *skew_spec, int skew_verbose) {
    IMDImage img = parse_imd(imd_path);
    int *skew = NULL;
    int skew_len = 0;
    size_t raw_len = 0;
    unsigned char *raw;
    DiskParams dp;
    size_t out_len = 0;
    unsigned char *out_imd;
    char *out_path_local = NULL;
    char *cpm_name = NULL;

    resolve_sector_skew(&img, skew_spec, skew_verbose, &skew, &skew_len);
    raw = build_raw(&img, skew, skew_len, &raw_len);
    dp = infer_params(raw, raw_len, &img, skew, skew_len);

    put_file(raw, raw_len, &dp, local_path, as_name, overwrite);
    out_imd = rebuild_imd(&img, raw, raw_len, skew, skew_len, &out_len);

    if (as_name && *as_name) cpm_name = dupstr(as_name);
    else cpm_name = basename_only(local_path);

    if (inplace && out_path) {
        die("Error: --inplace and --out are mutually exclusive.");
    }

    if (inplace) {
        atomic_write(imd_path, out_imd, out_len);
        out_path_local = dupstr(imd_path);
        printf("Inserted %s as %s. Updated in-place: %s\n", local_path, cpm_name, out_path_local);
    } else {
        if (out_path) out_path_local = dupstr(out_path);
        else out_path_local = default_out_imd(imd_path, "_patched.IMD");
        write_file_bytes(out_path_local, out_imd, out_len);
        printf("Inserted %s as %s. Wrote %s\n", local_path, cpm_name, out_path_local);
    }

    if (verify) {
        IMDImage img2 = parse_imd(out_path_local);
        int *skew2 = NULL;
        int skew_len2 = 0;
        size_t raw_len2 = 0;
        unsigned char *raw2;
        DiskParams dp2;
        size_t stored_len = 0;
        unsigned char *stored;
        size_t inp_len = 0;
        unsigned char *inp = read_file_bytes(local_path, &inp_len);
        int ok_prefix, ok_pad;

        resolve_sector_skew(&img2, skew_spec, skew_verbose, &skew2, &skew_len2);
        raw2 = build_raw(&img2, skew2, skew_len2, &raw_len2);
        dp2 = infer_params(raw2, raw_len2, &img2, skew2, skew_len2);

        stored = read_file_from_image(raw2, &dp2, cpm_name, &stored_len);
        ok_prefix = (stored_len >= inp_len && memcmp(stored, inp, inp_len) == 0);
        ok_pad = 1;
        if (stored_len > inp_len) {
            size_t i;
            for (i = inp_len; i < stored_len; i++) {
                if (!(stored[i] == 0x00 || stored[i] == 0x1A)) { ok_pad = 0; break; }
            }
        }

        if (ok_prefix && ok_pad) {
            printf("Verify: OK (stored bytes match input; remaining record padding is 0x00/0x1A).\n");
        } else {
            printf("Verify: FAILED\n");
            printf("  stored_len=%lu input_len=%lu\n", (unsigned long)stored_len, (unsigned long)inp_len);
        }

        free(inp);
        free(stored);
        free(raw2);
        free(skew2);
        free(dp2.sec_skew);
        if (dp2.dir_phys_secs) free(dp2.dir_phys_secs);
        free_imd(&img2);
    }

    free(cpm_name);
    free(out_path_local);
    free(out_imd);
    free(raw);
    free(skew);
    free(dp.sec_skew);
    if (dp.dir_phys_secs) free(dp.dir_phys_secs);
    free_imd(&img);
}

static void cmd_spdtab(const char *imd_path, const char *drive, int klass, int step,
                       const char *target, const char *out_path, int inplace,
                       const char *skew_spec, int skew_verbose) {
    IMDImage img = parse_imd(imd_path);
    int *skew = NULL;
    int skew_len = 0;
    size_t raw_len = 0;
    unsigned char *raw;
    int hit_count = 0;
    SpdTabHit *hits;
    int dir_cyl = -1;

    resolve_sector_skew(&img, skew_spec, skew_verbose, &skew, &skew_len);
    raw = build_raw(&img, skew, skew_len, &raw_len);

    hits = find_spdtab_hits(raw, raw_len, &img, &hit_count);
    if (hit_count == 0) die("No SPDTAB/OPRTAB signature found in this IMD.");

    /* try infer directory track */
    {
        DiskParams dp;
        dp = infer_params(raw, raw_len, &img, skew, skew_len);
        dir_cyl = dp.dir_cyl;
        free(dp.sec_skew);
        if (dp.dir_phys_secs) free(dp.dir_phys_secs);
    }

    printf("Found %d SPDTAB table(s) (signature match).\n", hit_count);
    {
        int i;
        for (i = 0; i < hit_count; i++) {
            char a[32], b[32], c[32], d[32];
            const char *tag = "";
            if (dir_cyl >= 0) tag = (hits[i].cyl < dir_cyl) ? " [system]" : " [data]";
            decode_spd_byte(hits[i].spdtab[0], a, sizeof(a));
            decode_spd_byte(hits[i].spdtab[1], b, sizeof(b));
            decode_spd_byte(hits[i].spdtab[2], c, sizeof(c));
            decode_spd_byte(hits[i].spdtab[3], d, sizeof(d));
            printf("#%d: raw_off=0x%05X  C=%d H=%d  sec=%d +%d%s\n", i + 1,
                   hits[i].spdtab_off, hits[i].cyl, hits[i].head, hits[i].sec, hits[i].sec_off, tag);
            printf("    A=%s  B=%s  C=%s  D=%s\n", a, b, c, d);
        }
    }

    if (!drive || !*drive) {
        free(hits);
        free(raw);
        free(skew);
        free_imd(&img);
        return;
    }

    if (klass != 5 && klass != 8) die("Error: --class is required when using --drive (use 8 or 5).");

    {
        int drive_idx = (drive[0] == 'A' || drive[0] == 'a') ? 0 :
                        (drive[0] == 'B' || drive[0] == 'b') ? 1 :
                        (drive[0] == 'C' || drive[0] == 'c') ? 2 : 3;
        int i;
        int *selected = (int *)malloc(sizeof(int) * (size_t)hit_count);
        int sel_count = 0;
        unsigned char *raw_mut;
        unsigned char *out_imd;
        size_t out_len = 0;
        char *out_path_local;

        if (!selected) die("Out of memory.");

        if (strieq(target, "first")) {
            selected[sel_count++] = 0;
        } else if (strieq(target, "all")) {
            for (i = 0; i < hit_count; i++) selected[sel_count++] = i;
        } else if (strieq(target, "system")) {
            for (i = 0; i < hit_count; i++) if (dir_cyl < 0 || hits[i].cyl < dir_cyl) selected[sel_count++] = i;
        } else if (strieq(target, "files")) {
            for (i = 0; i < hit_count; i++) if (dir_cyl < 0 || hits[i].cyl >= dir_cyl) selected[sel_count++] = i;
        } else {
            free(selected);
            die("Unknown --target value.");
        }

        if (sel_count == 0) {
            free(selected);
            die("No SPDTAB hits matched --target.");
        }

        raw_mut = (unsigned char *)malloc(raw_len);
        if (!raw_mut) die("Out of memory.");
        memcpy(raw_mut, raw, raw_len);

        printf("\n");
        for (i = 0; i < sel_count; i++) {
            SpdTabHit *h = &hits[selected[i]];
            int off = h->spdtab_off + drive_idx;
            unsigned char old = raw_mut[off];
            unsigned char newv = apply_spd_patch(old, klass, step);
            raw_mut[off] = newv;
            {
                char old_s[32], new_s[32];
                const char *tag = "";
                if (dir_cyl >= 0) tag = (h->cyl < dir_cyl) ? " [system]" : " [data]";
                decode_spd_byte(old, old_s, sizeof(old_s));
                decode_spd_byte(newv, new_s, sizeof(new_s));
                printf("Patched at raw_off=0x%05X  C=%d H=%d  %c: %s -> %s%s\n",
                       off, h->cyl, h->head, (char)('A' + drive_idx), old_s, new_s, tag);
            }
        }

        out_imd = rebuild_imd(&img, raw_mut, raw_len, skew, skew_len, &out_len);
        if (inplace && out_path) die("Error: --inplace and --out are mutually exclusive.");

        if (inplace) {
            atomic_write(imd_path, out_imd, out_len);
            out_path_local = dupstr(imd_path);
            printf("\nUpdated in-place: %s\n", out_path_local);
        } else {
            if (out_path) out_path_local = dupstr(out_path);
            else out_path_local = default_out_imd(imd_path, "_spdtab_patched.IMD");
            write_file_bytes(out_path_local, out_imd, out_len);
            printf("\nWrote %s\n", out_path_local);
        }

        free(out_path_local);
        free(out_imd);
        free(raw_mut);
        free(selected);
    }

    free(hits);
    free(raw);
    free(skew);
    free_imd(&img);
}

static void cmd_console(const char *imd_path, const char *status_s, const char *data_s,
                        const char *from_status_s, const char *from_data_s,
                        int scan, const char *out_path, int inplace, int verbose) {
    size_t blob_len = 0;
    unsigned char *blob = read_file_bytes(imd_path, &blob_len);
    int new_status = parse_u8_auto(status_s);
    int new_data = parse_u8_auto(data_s);
    int from_status = 0, from_data = 0;
    int from_status_set = 0, from_data_set = 0;
    int patched = 0;

    if (from_status_s) { from_status = parse_u8_auto(from_status_s); from_status_set = 1; }
    if (from_data_s) { from_data = parse_u8_auto(from_data_s); from_data_set = 1; }

    if (scan) {
        scan_and_patch(blob, blob_len, new_status, new_data, from_status, from_status_set,
                       from_data, from_data_set, verbose, &patched);
    } else {
        patch_fixed_sbc548n(blob, blob_len, new_status, new_data, from_status, from_status_set,
                            from_data, from_data_set, verbose, &patched);
    }

    if (patched == 0) die("No bytes were patched (already set, or patterns not found).");

    if (inplace && out_path) die("Error: --inplace and --out are mutually exclusive.");

    if (inplace) {
        atomic_write(imd_path, blob, blob_len);
        printf("Console patch: status -> 0x%02X, data -> 0x%02X\n", new_status, new_data);
        printf("Patched %d byte(s). Updated in-place: %s\n", patched, imd_path);
    } else {
        char *out_path_local;
        if (out_path) out_path_local = dupstr(out_path);
        else {
            char suffix[32];
            sprintf(suffix, "_serial_%02X_%02X.IMD", new_data, new_status);
            out_path_local = default_out_imd(imd_path, suffix);
        }
        write_file_bytes(out_path_local, blob, blob_len);
        printf("Console patch: status -> 0x%02X, data -> 0x%02X\n", new_status, new_data);
        printf("Patched %d byte(s). Wrote %s\n", patched, out_path_local);
        free(out_path_local);
    }

    free(blob);
}

/* ---------------- CLI parsing ---------------- */

static void usage(void) {
    printf("imd_cpmtools_vfii (C)\n");
    printf("Commands:\n");
    printf("  info <image.imd> [--long] [--skew ...] [--skew-verbose]\n");
    printf("  ls <image.imd> [--skew ...] [--skew-verbose]\n");
    printf("  get <image.imd> <CPMFILE> [--out <path>] [--skew ...]\n");
    printf("  put <image.imd> <local_path> [--as NAME.EXT] [--overwrite] [--out <image.imd>] [--inplace] [--verify] [--skew ...]\n");
    printf("  rm <image.imd> <CPMFILE> [--out <image.imd>] [--skew ...]\n");
    printf("  spdtab <image.imd> [--drive A|B|C|D] [--class 8|5] [--step 0..3] [--target all|system|files|first] [--out <image.imd>] [--inplace] [--skew ...]\n");
    printf("  console <image.imd> --data <byte> --status <byte> [--from-data <byte>] [--from-status <byte>] [--scan] [--out <image.imd>] [--inplace] [--verbose]\n");
}

int main(int argc, char **argv) {
    const char *cmd;
    const char *skew_spec = "auto";
    int skew_verbose = 0;

    if (argc < 2) {
        usage();
        return 1;
    }

    cmd = argv[1];

    /* Common options are parsed per-command in simple loops */
    if (strieq(cmd, "info")) {
        const char *imd = NULL;
        int long_list = 0;
        int i;
        for (i = 2; i < argc; i++) {
            if (strieq(argv[i], "--skew") && i + 1 < argc) { skew_spec = argv[++i]; }
            else if (strieq(argv[i], "--skew-verbose")) { skew_verbose = 1; }
            else if (strieq(argv[i], "--long")) { long_list = 1; }
            else if (!imd) { imd = argv[i]; }
            else { die("Unknown argument."); }
        }
        if (!imd) die("info: missing <image.imd>");
        cmd_info(imd, skew_spec, skew_verbose, long_list);
    } else if (strieq(cmd, "ls")) {
        const char *imd = NULL;
        int i;
        for (i = 2; i < argc; i++) {
            if (strieq(argv[i], "--skew") && i + 1 < argc) { skew_spec = argv[++i]; }
            else if (strieq(argv[i], "--skew-verbose")) { skew_verbose = 1; }
            else if (!imd) { imd = argv[i]; }
            else { die("Unknown argument."); }
        }
        if (!imd) die("ls: missing <image.imd>");
        cmd_ls(imd, skew_spec, skew_verbose);
    } else if (strieq(cmd, "get")) {
        const char *imd = NULL;
        const char *cpm = NULL;
        const char *out = NULL;
        int i;
        for (i = 2; i < argc; i++) {
            if (strieq(argv[i], "--skew") && i + 1 < argc) { skew_spec = argv[++i]; }
            else if (strieq(argv[i], "--skew-verbose")) { skew_verbose = 1; }
            else if (strieq(argv[i], "--out") && i + 1 < argc) { out = argv[++i]; }
            else if (!imd) imd = argv[i];
            else if (!cpm) cpm = argv[i];
            else die("Unknown argument.");
        }
        if (!imd || !cpm) die("get: missing arguments.");
        cmd_get(imd, cpm, out, skew_spec, skew_verbose);
    } else if (strieq(cmd, "put")) {
        const char *imd = NULL;
        const char *local = NULL;
        const char *as_name = NULL;
        const char *out = NULL;
        int overwrite = 0, inplace = 0, verify = 0;
        int i;
        for (i = 2; i < argc; i++) {
            if (strieq(argv[i], "--skew") && i + 1 < argc) { skew_spec = argv[++i]; }
            else if (strieq(argv[i], "--skew-verbose")) { skew_verbose = 1; }
            else if (strieq(argv[i], "--as") && i + 1 < argc) { as_name = argv[++i]; }
            else if (strieq(argv[i], "--overwrite")) { overwrite = 1; }
            else if (strieq(argv[i], "--out") && i + 1 < argc) { out = argv[++i]; }
            else if (strieq(argv[i], "--inplace")) { inplace = 1; }
            else if (strieq(argv[i], "--verify")) { verify = 1; }
            else if (!imd) imd = argv[i];
            else if (!local) local = argv[i];
            else die("Unknown argument.");
        }
        if (!imd || !local) die("put: missing arguments.");
        cmd_put(imd, local, as_name, overwrite, out, inplace, verify, skew_spec, skew_verbose);
    } else if (strieq(cmd, "rm")) {
        const char *imd = NULL;
        const char *cpm = NULL;
        const char *out = NULL;
        int i;
        for (i = 2; i < argc; i++) {
            if (strieq(argv[i], "--skew") && i + 1 < argc) { skew_spec = argv[++i]; }
            else if (strieq(argv[i], "--skew-verbose")) { skew_verbose = 1; }
            else if (strieq(argv[i], "--out") && i + 1 < argc) { out = argv[++i]; }
            else if (!imd) imd = argv[i];
            else if (!cpm) cpm = argv[i];
            else die("Unknown argument.");
        }
        if (!imd || !cpm) die("rm: missing arguments.");
        cmd_rm(imd, cpm, out, skew_spec, skew_verbose);
    } else if (strieq(cmd, "spdtab")) {
        const char *imd = NULL;
        const char *drive = NULL;
        int klass = 0;
        int step = -1;
        const char *target = "all";
        const char *out = NULL;
        int inplace = 0;
        int i;
        for (i = 2; i < argc; i++) {
            if (strieq(argv[i], "--skew") && i + 1 < argc) { skew_spec = argv[++i]; }
            else if (strieq(argv[i], "--skew-verbose")) { skew_verbose = 1; }
            else if (strieq(argv[i], "--drive") && i + 1 < argc) { drive = argv[++i]; }
            else if (strieq(argv[i], "--class") && i + 1 < argc) { klass = atoi(argv[++i]); }
            else if (strieq(argv[i], "--step") && i + 1 < argc) { step = atoi(argv[++i]); }
            else if (strieq(argv[i], "--target") && i + 1 < argc) { target = argv[++i]; }
            else if (strieq(argv[i], "--out") && i + 1 < argc) { out = argv[++i]; }
            else if (strieq(argv[i], "--inplace")) { inplace = 1; }
            else if (!imd) imd = argv[i];
            else die("Unknown argument.");
        }
        if (!imd) die("spdtab: missing <image.imd>");
        cmd_spdtab(imd, drive, klass, step, target, out, inplace, skew_spec, skew_verbose);
    } else if (strieq(cmd, "console")) {
        const char *imd = NULL;
        const char *status = NULL;
        const char *data = NULL;
        const char *from_status = NULL;
        const char *from_data = NULL;
        const char *out = NULL;
        int scan = 0;
        int inplace = 0;
        int verbose = 0;
        int i;
        for (i = 2; i < argc; i++) {
            if (strieq(argv[i], "--status") && i + 1 < argc) { status = argv[++i]; }
            else if (strieq(argv[i], "--data") && i + 1 < argc) { data = argv[++i]; }
            else if (strieq(argv[i], "--from-status") && i + 1 < argc) { from_status = argv[++i]; }
            else if (strieq(argv[i], "--from-data") && i + 1 < argc) { from_data = argv[++i]; }
            else if (strieq(argv[i], "--scan")) { scan = 1; }
            else if (strieq(argv[i], "--out") && i + 1 < argc) { out = argv[++i]; }
            else if (strieq(argv[i], "--inplace")) { inplace = 1; }
            else if (strieq(argv[i], "--verbose")) { verbose = 1; }
            else if (!imd) imd = argv[i];
            else die("Unknown argument.");
        }
        if (!imd || !status || !data) die("console: missing required arguments.");
        cmd_console(imd, status, data, from_status, from_data, scan, out, inplace, verbose);
    } else {
        usage();
        return 1;
    }

    return 0;
}
