;; ----------------------------------------------------------------------
;; crt0.s — Sprinter ESTEX C runtime startup, with argv parsing.
;;
;; What's special vs crt0_minimal.s:
;;   After gsinit and before calling main, we read the ESTEX command-line
;;   from the startup prefix (IX+0 = length, IX+1.. = ASCIIZ bytes) and
;;   tokenize it in place into a static argv[] table.  argc → HL, argv → DE
;;   when we then `call _main`, which matches SDCC's __sdcccall(1) two-arg
;;   ABI.  Programs declared as `int main(void)` simply ignore the input
;;   registers — no harm done — so this is the default crt0.
;;
;; Entry contract (per ESTEX EXEC):
;;   IX -> startup prefix:
;;       IX-3 = open file handle (only if EXE header.loader > 0)
;;       IX-2 = memory block id (released automatically on EXIT)
;;       IX-1 = current process level
;;       IX+0 = command-line length (1 byte)
;;       IX+1 = command-line bytes (ASCIIZ, up to 127 chars)
;;
;; Memory map: see compiler_approach memory file.
;; ----------------------------------------------------------------------

    .module crt0
    .globl  _main

    ;; Linker-emitted symbols (resolved at link time).
    .globl  s__INITIALIZER
    .globl  l__INITIALIZER
    .globl  s__INITIALIZED
    .globl  s__DATA
    .globl  l__DATA
    .globl  s__BSS
    .globl  l__BSS

;; Tunables — match the constants in argv parsing below.
ARGV_MAX_ARGS    = 16              ; including argv[0]
ARGV_BUF_BYTES   = 128             ; cmdline buffer size (ESTEX max + 1)

;; ___sdcc_heap_end (upper bound of the malloc free-list) lives in a
;; separate per-program file (runtime/heap_top.s) so it can be regenerated
;; with a custom value without re-assembling crt0.  The default is set in
;; that file (0xBB00); sprinter-cc may emit a different value when
;; `--stack-size N` is given.

;; =========================================================================
;; AREA ORDERING — emitted up-front so the linker walks them in this order.
;; =========================================================================
    .area _HOME
    .area _CODE
    .area _INITIALIZER
    .area _GSINIT
    .area _GSFINAL

    .area _DATA
    .area _INITIALIZED
    .area _BSEG
    .area _BSS
    .area _HEAP

;; =========================================================================
;; Entry point — first instruction in _CODE, hence at --code-loc (0x4100).
;; =========================================================================
    .area _CODE
_start::
    ld    sp, #0xBFFE
    ld    (_estex_startup_ix), ix    ; save IX prefix pointer

    call  gsinit

.ifdef DEBUG_RT
    ;; tiny mode never self-allocates a W2 page (CODE+DATA both live in W2,
    ;; which DSS itself maps for us).  Publish 0 to the diagnostic flag —
    ;; must run AFTER gsinit since BSS is zeroed there and we want a
    ;; deterministic value visible to user code from main() onward.
    xor   a
    ld    (_w2_self_allocated), a
.endif

    ;; Parse the ESTEX command-line into argv[]; populates _argc, _argv.
    call  parse_argv

    ;; Replace argv[0] (empty string placeholder) with the basename of the
    ;; running .EXE.  Safe to skip if ESTEX APPINFO fails.
    call  get_progname

    ;; Load argc/argv per SDCC __sdcccall(1): arg1 → HL, arg2 → DE.
    ld    hl, (_argc)
    ld    de, (_argv)
    call  _main

    ;; SDCC's int return → DE.  Low byte is the exit code.
    ld    a, e
    ld    b, a
    ld    c, #0x41                   ; ESTEX EXIT
    rst   #0x10
    ;; Should not return; halt loop just in case.
1$: halt
    jr    1$

;; =========================================================================
;; parse_argv — tokenize ESTEX cmdline into argv[].
;;
;;   The prefix:  (IX+0) = length; (IX+1...) = ASCIIZ bytes.
;;   We copy up to ARGV_BUF_BYTES-1 chars into our own buffer (so we can
;;   overwrite separators with NUL), strip leading whitespace (DSS quirk),
;;   then walk through tokens.  argv[0] is set to an empty string because
;;   ESTEX doesn't pass the program name in the prefix.
;; =========================================================================
    .area _CODE
parse_argv::
    ;; Copy cmdline body into argv_buf, NUL-terminate.
    ld    hl, (_estex_startup_ix)
    ld    a, (hl)                    ; A = cmdline length
    cp    #ARGV_BUF_BYTES
    jr    c, len_ok
    ld    a, #ARGV_BUF_BYTES-1
len_ok:
    ld    c, a
    ld    b, #0                      ; BC = number of bytes to copy
    inc   hl                         ; HL = body start (= IX+1)
    ld    de, #argv_buf
    ld    a, b
    or    a, c
    jr    Z, after_copy
    ldir                             ; DE = one past last copied byte
after_copy:
    xor   a
    ld    (de), a                    ; NUL-terminate

    ;; Skip leading whitespace.
    ld    hl, #argv_buf
strip_ws:
    ld    a, (hl)
    or    a
    jr    Z, no_args
    cp    #0x20                      ; ' '
    jr    Z, strip_more
    cp    #0x09                      ; tab
    jr    nz, have_first_token
strip_more:
    inc   hl
    jr    strip_ws

no_args:
    ;; Only argv[0] = "" — no real arguments.
    ld    hl, #empty_str
    ld    (argv_array), hl
    ld    hl, #0
    ld    (argv_array+2), hl         ; argv[1] = NULL
    ld    hl, #1
    ld    (_argc), hl
    ld    hl, #argv_array
    ld    (_argv), hl
    ret

have_first_token:
    ;; HL points at start of first real argument.
    ;; argv[0] = empty string (placeholder for program name).
    ld    de, #empty_str
    ld    (argv_array), de

    ;; B = current argc (start at 1).  Use B because HL/DE are busy.
    ld    b, #1

token_loop:
    ;; Bound check: argc < ARGV_MAX_ARGS
    ld    a, b
    cp    #ARGV_MAX_ARGS
    jr    nc, tokens_done

    ;; argv[argc] = HL.  Compute slot address = argv_array + argc*2.
    push  hl
    ld    a, b
    add   a, a                       ; A = argc * 2
    ld    e, a
    ld    d, #0
    ld    hl, #argv_array
    add   hl, de                     ; HL = &argv[argc]
    pop   de                         ; DE = current token pointer
    ld    (hl), e
    inc   hl
    ld    (hl), d
    ex    de, hl                     ; HL = token pointer again

    inc   b                          ; argc++

    ;; Advance past token (non-whitespace).
walk_token:
    ld    a, (hl)
    or    a
    jr    Z, tokens_done
    cp    #0x20
    jr    Z, end_of_token
    cp    #0x09
    jr    Z, end_of_token
    inc   hl
    jr    walk_token

end_of_token:
    ;; Replace separator with NUL.
    xor   a
    ld    (hl), a
    inc   hl

    ;; Skip extra whitespace.
skip_ws:
    ld    a, (hl)
    or    a
    jr    Z, tokens_done
    cp    #0x20
    jr    Z, skip_more
    cp    #0x09
    jr    Z, skip_more
    jr    token_loop
skip_more:
    inc   hl
    jr    skip_ws

tokens_done:
    ;; argv[argc] = NULL terminator.
    ld    a, b
    add   a, a
    ld    e, a
    ld    d, #0
    ld    hl, #argv_array
    add   hl, de
    ld    (hl), #0
    inc   hl
    ld    (hl), #0
    ;; Publish argc (16-bit, zero-extended from B).
    ld    a, b
    ld    (_argc), a
    xor   a
    ld    (_argc+1), a
    ;; Publish argv = &argv_array[0]
    ld    hl, #argv_array
    ld    (_argv), hl
    ret

;; The empty string used as argv[0] placeholder — lives in code.
empty_str:
    .db 0

;; =========================================================================
;; get_progname — fetch the running program's full path via ESTEX APPINFO
;; (subfn 2, $47) and set argv[0] to the basename portion.
;;
;;   ESTEX APPINFO: A=err, C=$47, B=subfn, HL=buf → A=err, CF=1 on error.
;;   subfn 2 = full app path, e.g. "A:\PROGRAMS\ARGV_TES.EXE"
;;
;; On error or when no separator is found, we leave argv[0] as the empty
;; string placeholder that parse_argv set.
;; =========================================================================
    .area _CODE
get_progname::
    push  ix                        ; ESTEX clobbers IX
    ld    hl, #progname_buf
    ld    b, #2                     ; subfn 2 = app_path
    ld    c, #0x47
    rst   #0x10
    pop   ix
    jr    c, gpn_skip               ; APPINFO failed → keep empty argv[0]

    ;; First make sure the buffer is null-terminated (it should already be).
    ;; Then scan forward to find the end-of-string, then scan backwards to
    ;; the last directory separator ('\' or ':').
    ld    hl, #progname_buf
gpn_find_end:
    ld    a, (hl)
    or    a, a
    jr    Z, gpn_at_end
    inc   hl
    jr    gpn_find_end
gpn_at_end:
    ;; HL points at the NUL.  Walk backwards looking for '\' or ':'.  Stop
    ;; if we reach the start of the buffer — then the whole path is the
    ;; basename.
    ld    de, #progname_buf
gpn_scan_back:
    ld    a, h
    cp    a, d
    jr    nz, gpn_dec
    ld    a, l
    cp    a, e
    jr    Z, gpn_set                ; reached buffer start
gpn_dec:
    dec   hl
    ld    a, (hl)
    cp    #0x5C                     ; backslash
    jr    Z, gpn_after_sep
    cp    #0x3A                     ; colon (drive separator)
    jr    Z, gpn_after_sep
    jr    gpn_scan_back

gpn_after_sep:
    inc   hl                        ; skip past the separator
gpn_set:
    ld    (argv_array), hl          ; argv[0] = basename
gpn_skip:
    ret

;; =========================================================================
;; Runtime data (HOME's _DATA / _BSS)
;; =========================================================================
    .area _CODE
;; Written BEFORE gsinit runs (gsinit zeroes _DATA), so these live in
;; _CODE — RAM on the Sprinter, always mapped.
_estex_startup_ix::
    .ds 2

    .area _DATA
_argc::
    .ds 2
_argv::
    .ds 2

.ifdef DEBUG_RT
;; Runtime diagnostic: 0 = no extra W2 page was self-allocated by crt0
;; (DSS gave us what we needed); 1 = crt0 had to allocate W2 itself.
;; Only present when sprinter-cc is invoked with --debug.
_w2_self_allocated::
    .ds 1
.endif

    .area _BSS
argv_buf:
    .ds ARGV_BUF_BYTES
argv_array:
    .ds (ARGV_MAX_ARGS + 1) * 2    ; +1 for trailing NULL pointer
progname_buf:
    .ds 128                          ; ESTEX APPINFO app_path target

;; =========================================================================
;; gsinit — zero _DATA + _BSS, then copy _INITIALIZER -> _INITIALIZED.
;; _DATA is zeroed because C guarantees zero-valued statics — this lets
;; code drop explicit `= 0` initializers that burn _INITIALIZER bytes.
;; =========================================================================
    .area _GSINIT
    .area _CODE
;; zero_area — fill BC bytes at HL with 0 (BC may be zero).
zero_area:
    ld    a, b
    or    a, c
    ret   Z
    ld    (hl), #0
    dec   bc
    ld    a, b
    or    a, c
    ret   Z
    ld    d, h
    ld    e, l
    inc   de
    ldir
    ret

    .area _GSINIT
gsinit::
    ld    hl, #s__DATA
    ld    bc, #l__DATA
    call  zero_area
    ld    hl, #s__BSS
    ld    bc, #l__BSS
    call  zero_area
    ld    bc, #l__INITIALIZER
    ld    a, b
    or    a, c
    jr    Z, gsinit_done
    ld    de, #s__INITIALIZED
    ld    hl, #s__INITIALIZER
    ldir
gsinit_done:

    .area _GSFINAL
    ret
