/*
 * stat.c — POSIX stat() and fstat() over ESTEX metadata.
 *
 *   fstat(fd, &st)  -> ESTEX GET_D_T ($17) for mtime + lseek/SEEK_END
 *                      for size.
 *   stat(path, &st) -> open(O_RDONLY) + fstat() + close.  This works
 *                      for any regular file; directory paths fail at
 *                      open() — F_FIRST-based stat had unreliable
 *                      semantics for exact filenames.
 *
 * Sprinter / DSS doesn't track POSIX owner/group/inode, so we synth a
 * minimal mode (S_IFREG | rw user perm).
 */

#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <time.h>
#include <errno.h>
#include <stdint.h>
#include <string.h>
#include <dir.h>

/* ESTEX GET_D_T: A=fd, C=$17 → D=day, E=month, IX=year, H=hour, L=min,
 * B=sec.  CF=1 + A=errcode on failure.
 *
 * Writes directly into *out using `ex (sp), hl` to swap the saved out
 * pointer with HL=hour:min after RST — no static scratch needed.
 * `out->dow` is left untouched (GET_D_T doesn't return it). */
static int get_dt_for_handle(int fd, datetime_t *out) __naked
{
    (void)fd; (void)out;
    __asm
        ;; __sdcccall(1): fd in HL (low byte), out in DE.
        push  ix                    ; save caller IX
        push  de                    ; stash out pointer
        ld    a, l                  ; A = fd
        ld    c, #0x17              ; ESTEX GET_D_T
        rst   #0x10
        jr    c, _gdt_err
        ;; D=day E=month IX=year H=hour L=min B=sec
        ex    (sp), hl              ; TOS<->HL: HL=out, TOS=hour:min
        ld    (hl), d               ; +0 day
        inc   hl
        ld    (hl), e               ; +1 month
        inc   hl
        push  ix                    ; year onto stack
        pop   de                    ; DE = year
        ld    (hl), e               ; +2 year low
        inc   hl
        ld    (hl), d               ; +3 year high
        inc   hl
        pop   de                    ; D=hour E=min (from earlier ex (sp))
        ld    (hl), d               ; +4 hour
        inc   hl
        ld    (hl), e               ; +5 min
        inc   hl
        ld    (hl), b               ; +6 sec  (+7 dow left untouched)
        pop   ix                    ; restore caller IX
        ld    de, #0
        ret
    _gdt_err:
        pop   hl                    ; discard stashed out pointer
        pop   ix                    ; restore caller IX
        call  __errno_set
        ld    de, #-1
        ret
    __endasm;
}

int fstat(int fd, struct stat *buf)
{
    /* Size via lseek trick. */
    long cur = lseek(fd, 0L, SEEK_CUR);
    if (cur < 0) return -1;
    long end = lseek(fd, 0L, SEEK_END);
    if (end < 0) return -1;
    (void)lseek(fd, cur, SEEK_SET);
    buf->st_size = (uint32_t)end;

    /* Date/time via ESTEX. */
    datetime_t ft;
    if (get_dt_for_handle(fd, &ft) < 0) return -1;
    {
        struct tm tm;
        tm.tm_sec   = ft.second;
        tm.tm_min   = ft.minute;
        tm.tm_hour  = ft.hour;
        tm.tm_mday  = ft.day;
        tm.tm_mon   = (unsigned char)(ft.month - 1);
        tm.tm_year  = (int)ft.year - 1900;
        tm.tm_isdst = 0;
        tm.tm_hundredth = 0;
        buf->st_mtime = mktime(&tm);
    }
    buf->st_mode = S_IFREG | S_IRUSR | S_IWUSR;
    return 0;
}

/* Convert ESTEX DOS-style date+time to time_t epoch (used by stat()
 * when going through F_FIRST for directory entries). */
static time_t dos_to_epoch(uint16_t date, uint16_t dtime)
{
    struct tm tm;
    tm.tm_sec   = (unsigned char)((dtime & 0x1F) << 1);
    tm.tm_min   = (unsigned char)((dtime >> 5) & 0x3F);
    tm.tm_hour  = (unsigned char)((dtime >> 11) & 0x1F);
    tm.tm_mday  = (unsigned char)(date & 0x1F);
    tm.tm_mon   = (unsigned char)(((date >> 5) & 0x0F) - 1);
    tm.tm_year  = (int)((date >> 9) & 0x7F) + 80;   /* DOS year base = 1980 */
    tm.tm_isdst = 0;
    tm.tm_hundredth = 0;
    return mktime(&tm);
}

/* Returns 1 if path is "." or "..", else 0.  Reads at most 3 bytes;
 * NULL-safe.  ~28 bytes / 34–117 T-states depending on input. */
static char is_dot_or_dotdot(const char *path) __naked
{
    (void)path;
    __asm
        ld      a, h
        or      a, l
        jr      Z, _idd_fail            ; NULL → 0
        ld      a, (hl)
        sub     a, #0x2E
        jr      NZ, _idd_fail           ; path[0] != '.'
        inc     hl
        ld      a, (hl)
        or      a, a
        jr      Z, _idd_ok              ; ".\0" → 1
        sub     a, #0x2E
        jr      NZ, _idd_fail           ; path[1] not '.' and not '\0'
        inc     hl
        ld      a, (hl)
        or      a, a
        jr      Z, _idd_ok              ; "..\0" → 1
    _idd_fail:
        xor     a, a
        ret
    _idd_ok:
        ld      a, #1
        ret
    __endasm;
}

int stat(const char *path, struct stat *buf)
{
    /* Regular file: open + fstat. */
    int fd = open(path, O_RDONLY);
    if (fd >= 0) {
        int r = fstat(fd, buf);
        close(fd);
        return r;
    }
    int saved = errno;

    /* Try ffirst directly — works for ordinary subdirectories. */
    ffblk_t ffb;
    if (ffirst(path, &ffb, FA_DIREC) == 0 && (ffb.found_attr & FA_DIREC)) {
        buf->st_size  = ffb.size;
        buf->st_mtime = dos_to_epoch(ffb.date, ffb.time);
        buf->st_mode  = S_IFDIR | S_IRWXU;
        return 0;
    }

    /* Verified 2026-05-29: ESTEX F_FIRST rejects bare "." and ".." with
     * EINAME (16), same as open().  But they DO appear in the "*.*"
     * directory listing with FA_DIREC.  Iterate to find them. */
    if (is_dot_or_dotdot(path)) {
        if (ffirst("*.*", &ffb, FA_DIREC) == 0) {
            do {
                if (strcmp(ffb.found_name, path) == 0) {
                    buf->st_size  = ffb.size;
                    buf->st_mtime = dos_to_epoch(ffb.date, ffb.time);
                    buf->st_mode  = S_IFDIR | S_IRWXU;
                    return 0;
                }
            } while (fnext(&ffb) == 0);
        }
        /* Last-resort synthetic entry — FS variant didn't expose them. */
        buf->st_mode  = S_IFDIR | S_IRWXU;
        buf->st_size  = 0;
        buf->st_mtime = 0;
        return 0;
    }

    errno = saved;
    return -1;
}
