;; ----------------------------------------------------------------------
;; crt0_small.s — Sprinter ESTEX C runtime startup for SMALL memory mode.
;;
;; Layout (small mode):
;;   CODE   at 0x4100 (W1)
;;   DATA   chained after code by the linker (W1, possibly spills into W2)
;;   STACK  at 0xBFFE (top of W2, valid after W2 page is mapped)
;;   HEAP   from end-of-BSS up to ___sdcc_heap_end (default 0xBB00)
;;
;; Size auto-detect:
;;   For programs < ~14 KB, DSS allocates exactly ONE 16 KB page (W1).  W2
;;   is "page #FF" (read=0xFF, writes ignored) and we must allocate &
;;   map a page ourselves.  For programs that already span > 16 KB (the
;;   linker emitted bytes above 0x7FFF), DSS allocates TWO pages on its
;;   own — W2 already RAM — and we MUST NOT call SETWIN2, or we'd swap
;;   the DSS-loaded code page out of W2.
;;
;;   We auto-detect by writing a test byte at 0xBFFF (well above stack
;;   pushes) and reading back.  Match → W2 is RAM, skip allocation.
;;   Mismatch → W2 is page #FF, allocate via ESTEX $3D + $3A.
;;
;; Chicken-and-egg:
;;   DSS sets SP from the EXE header (0xBFFE) — in unmapped W2 for small
;;   programs.  Pushes silently fail; first `call` returns to 0xFFFF and
;;   crashes.  Boot stack lives in HOME (W1, real RAM) until W2 is ready.
;;
;; Why ESTEX $3A SETWIN2 not BIOS $C4 + OUT (0xC2):
;;   BIOS calls require SP in W2 (0x8000..0xBFFF) — exactly what we don't
;;   have yet.  SETWIN2 is ESTEX, works with W1 stack.
;;
;; Entry contract — same as crt0.s.
;; ----------------------------------------------------------------------

    .module crt0_small
    .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 — mirror crt0.s.
ARGV_MAX_ARGS    = 16
ARGV_BUF_BYTES   = 128
BOOT_STACK_BYTES = 32              ; enough for 2-3 nested RST 10h/RST 8 calls

;; =========================================================================
;; AREA ORDERING.
;; =========================================================================
    .area _HOME
    .area _CODE
    .area _INITIALIZER
    .area _GSINIT
    .area _GSFINAL

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

;; =========================================================================
;; Entry point — must be FIRST in _CODE so it lands at --code-loc (0x4100).
;; W2 is unmapped at this instant — no `call` allowed until we point SP at
;; boot_stack_top (in W1, declared just below).
;; =========================================================================
    .area _CODE
_start::
    ;; Switch to the boot stack in W1.  The DSS-set SP=0xBFFE may point
    ;; into unmapped W2 — pushing there silently fails and the first call
    ;; would return to 0xFFFF.  Boot stack lives in HOME (W1, real RAM).
    ld    sp, #boot_stack_top

    ;; The DSS startup-prefix pointer is in IX.  Stash it on the boot
    ;; stack — _estex_startup_ix is in W2 which may not be RAM yet.
    push  ix

    ;; ----- Step 1: detect whether W2 is already mapped. -----------------
    ;; Port 0xC2 is the W2 page register (0x82/A2/C2/E2 = W0/W1/W2/W3).
    ;; Reading it returns the page number currently selected for W2; page
    ;; 0xFF is the "unmapped" sentinel (read=0xFF / write-ignored).  A
    ;; non-0xFF value means DSS already allocated and mapped a real RAM
    ;; page for W2 — this happens for programs whose HOME image extends
    ;; past 0x7FFF (DSS allocates as many pages as the image size demands).
    in    a, (#0xC2)
    cp    #0xFF
    jr    nz, w2_already_mapped

    ;; ----- Step 2a: W2 unmapped → allocate one page and map it. --------
    ;; ESTEX $3D GETMEM: B = npages → A = block id, CF = err.
    ld    b, #1
    ld    c, #0x3D
    rst   #0x10
    jr    c, w2_alloc_fail
    ;; A = block id of the freshly-allocated 1-page block.

    ;; ESTEX $3A SETWIN2: A = block id, B = page idx → maps to W2.  This
    ;; is an ESTEX (RST 10h) call, not BIOS (RST 8) — BIOS would require
    ;; SP in W2, which is exactly what we don't have yet.
    ld    b, #0                  ; page 0 of the 1-page block
    ld    c, #0x3A
    rst   #0x10
    jr    c, w2_alloc_fail
.ifdef DEBUG_RT
    ld    a, #1                  ; flag: we self-allocated W2
    jr    w2_join
.endif

w2_already_mapped:
.ifdef DEBUG_RT
    xor   a                      ; flag: DSS already gave us W2
w2_join:
.endif
    ;; ----- Step 3: switch to the canonical SP at the top of W2. --------
    ;; Pop IX back from the boot stack first.
    pop   ix
    ld    sp, #0xBFFE
.ifdef DEBUG_RT
    ;; Save the self-alloc flag across gsinit (which clobbers BC/DE/HL and
    ;; zeroes _DATA/_BSS).  SP is in W2 now, push is safe.
    push  af
.endif

    ;; ----- Step 5: save IX prefix in its canonical slot (_CODE, W1). ----
    ld    (_estex_startup_ix), ix

    ;; ----- Step 6: standard gsinit + argv flow (identical to crt0.s). --
    call  gsinit
.ifdef DEBUG_RT
    pop   af
    ld    (_w2_self_allocated), a
.endif
    call  parse_argv
    call  get_progname

    ld    hl, (_argc)
    ld    de, (_argv)
    call  _main

    ;; SDCC int return in DE.  Low byte = exit code.
    ld    a, e
    ld    b, a
    ld    c, #0x41              ; ESTEX EXIT
    rst   #0x10
1$: halt
    jr    1$

;; -------------------------------------------------------------------------
;; W2 allocation failed — there's not much we can do without DATA/BSS.
;; Try to abort via ESTEX EXIT with a non-zero code (no return).
;; -------------------------------------------------------------------------
w2_alloc_fail:
    ld    b, #0xFE              ; arbitrary failure code
    ld    c, #0x41              ; ESTEX EXIT
    rst   #0x10
2$: halt
    jr    2$

;; =========================================================================
;; Boot stack — reserved in HOME (W1, real RAM).  Used only during the
;; first few instructions of _start, before W2 is mapped.  After we switch
;; SP to 0xBFFE the boot stack memory is just dead bytes in HOME.
;; =========================================================================
boot_stack:
    .ds   BOOT_STACK_BYTES
boot_stack_top:

;; =========================================================================
;; parse_argv / get_progname / runtime data / gsinit — same as crt0.s.
;; Kept inline rather than factored out to keep crt0 build self-contained.
;; =========================================================================
    .area _CODE
parse_argv::
    ld    hl, (_estex_startup_ix)
    ld    a, (hl)
    cp    #ARGV_BUF_BYTES
    jr    c, len_ok
    ld    a, #ARGV_BUF_BYTES-1
len_ok:
    ld    c, a
    ld    b, #0
    inc   hl
    ld    de, #argv_buf
    ld    a, b
    or    a, c
    jr    Z, after_copy
    ldir
after_copy:
    xor   a
    ld    (de), a

    ld    hl, #argv_buf
strip_ws:
    ld    a, (hl)
    or    a
    jr    Z, no_args
    cp    #0x20
    jr    Z, strip_more
    cp    #0x09
    jr    nz, have_first_token
strip_more:
    inc   hl
    jr    strip_ws

no_args:
    ld    hl, #empty_str
    ld    (argv_array), hl
    ld    hl, #0
    ld    (argv_array+2), hl
    ld    hl, #1
    ld    (_argc), hl
    ld    hl, #argv_array
    ld    (_argv), hl
    ret

have_first_token:
    ld    de, #empty_str
    ld    (argv_array), de
    ld    b, #1

token_loop:
    ld    a, b
    cp    #ARGV_MAX_ARGS
    jr    nc, tokens_done

    push  hl
    ld    a, b
    add   a, a
    ld    e, a
    ld    d, #0
    ld    hl, #argv_array
    add   hl, de
    pop   de
    ld    (hl), e
    inc   hl
    ld    (hl), d
    ex    de, hl

    inc   b

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:
    xor   a
    ld    (hl), a
    inc   hl

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:
    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
    ld    a, b
    ld    (_argc), a
    xor   a
    ld    (_argc+1), a
    ld    hl, #argv_array
    ld    (_argv), hl
    ret

empty_str:
    .db 0

    .area _CODE
get_progname::
    push  ix
    ld    hl, #progname_buf
    ld    b, #2
    ld    c, #0x47
    rst   #0x10
    pop   ix
    jr    c, gpn_skip

    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:
    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
gpn_dec:
    dec   hl
    ld    a, (hl)
    cp    #0x5C
    jr    Z, gpn_after_sep
    cp    #0x3A
    jr    Z, gpn_after_sep
    jr    gpn_scan_back

gpn_after_sep:
    inc   hl
gpn_set:
    ld    (argv_array), hl
gpn_skip:
    ret

;; =========================================================================
;; Runtime data — same layout as crt0.s.  Lives in DATA/BSS which the
;; linker places in W2 (--data-loc 0x8000).
;; =========================================================================
    .area _CODE
;; _estex_startup_ix is written BEFORE gsinit runs (gsinit zeroes _DATA),
;; so it lives in _CODE — RAM on the Sprinter, always mapped.
_estex_startup_ix::
    .ds 2

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

    .area _BSS
argv_buf:
    .ds ARGV_BUF_BYTES
argv_array:
    .ds (ARGV_MAX_ARGS + 1) * 2
progname_buf:
    .ds 128

.ifdef DEBUG_RT
;; Runtime diagnostic: 0 = DSS already mapped W2 for us (program > 16 KB,
;; or some other case); 1 = crt0 had to allocate and map W2 itself via
;; ESTEX $3D + $3A.  Only present when sprinter-cc is invoked with --debug.
_w2_self_allocated::
    .ds 1
.endif

;; =========================================================================
;; gsinit — zero _DATA + _BSS, then copy _INITIALIZER -> _INITIALIZED.
;; Identical to crt0.s; runs AFTER W2 is mapped so the writes land.
;; _DATA is zeroed because C guarantees zero-valued statics — initializing
;; `static uint8_t x;` here lets code drop explicit `= 0` initializers
;; that would otherwise burn _INITIALIZER bytes in the EXE image.
;; =========================================================================
    .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
