;; ----------------------------------------------------------------------
;; crt0_banked.s — Sprinter ESTEX C runtime startup with bank loading.
;;
;; Same entry contract as crt0.s, plus the EXE was packaged with
;; `loader > 0` so the file handle survives in (IX-3).
;;
;; Sequence:
;;   1.  Boot stack in W1 (CODE area).  We can't yet touch W2 because
;;       small images (<16 KB) get only the W1 page from DSS — W2 reads
;;       as 0xFF and ignores writes.  This mirrors crt0_small.s.
;;   2.  Detect via IN A,(#0xC2) whether DSS already mapped W2.  If not,
;;       ESTEX $3D GETMEM + $3A SETWIN2 maps a fresh page (BIOS $C4 + OUT
;;       would require SP in W2, which we don't have yet).
;;   3.  SP = 0xBFFE                     (now W2 is real RAM)
;;   4.  Stash file handle + ESTEX block id from prefix
;;   5.  GETMEM(_n_banks) → block id, then for each bank i in 1..n:
;;         BIOS_EMM_GETPAGE → phys page → _bank_pages[i]
;;         OUT (#0xE2), phys                       ; map into W3
;;         ESTEX READ 16384 bytes → 0xC000          ; load bank from file
;;   6.  CLOSE file
;;   7.  Standard gsinit + call _main + ESTEX EXIT
;;
;; _n_banks MUST be defined as `const uint8_t n_banks = N;` — we read it
;; before gsinit, so a plain initialized `uint8_t` would still hold 0
;; (the initializer hasn't been copied yet) and bank loading would be
;; silently skipped, leaving window 3 mapped to a garbage page.
;; For zero banks, link crt0.s instead.
;; ----------------------------------------------------------------------

    .module crt0_banked
    .globl  _main
    .globl  _n_banks
    .globl  _bank_pages

    .globl  s__INITIALIZER
    .globl  l__INITIALIZER
    .globl  s__INITIALIZED
    .globl  s__DATA
    .globl  l__DATA
    .globl  s__BSS
    .globl  l__BSS

;; argv parsing tunables — mirror crt0.s.
ARGV_MAX_ARGS    = 16
ARGV_BUF_BYTES   = 128
BOOT_STACK_BYTES = 32              ; enough for a few ESTEX RST 10h calls

;; ----- Banking-window parameters ----------------------------------------
;; Choose between W3 (default = HUGE mode) and W1 (BIG mode, prepended
;; BANK_W1 = 1 at assembly time by sprinter-cc).  These constants steer
;; the bank-loading IO port and the address where each bank lives in
;; the program's virtual memory once mapped in.
.ifdef BANK_W1
BANK_PORT      = 0xA2              ; W1 page register
BANK_LOAD_ADDR = 0x4000            ; banks at 0x4000-0x7FFF when mapped
.else
BANK_PORT      = 0xE2              ; W3 page register
BANK_LOAD_ADDR = 0xC000            ; banks at 0xC000-0xFFFF when mapped
.endif

;; ----- Area declaration 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::
    ;; ----- Step 1: switch to W1 boot stack (W2 may be unmapped). -------
    ld    sp, #boot_stack_top
    push  ix                          ; stash IX in W1 (W2 not yet safe)

    ;; ----- Step 2: detect / allocate W2. ------------------------------
    ;; Port 0xC2 = W2 page register.  0xFF means DSS hasn't mapped W2
    ;; (program ≤ 16 KB).  Any other value means W2 is real RAM and we
    ;; must NOT replace it (DSS may have stored bytes there).
    in    a, (#0xC2)
    cp    #0xFF
    jr    nz, w2_already_mapped
    ;; ESTEX $3D GETMEM — alloc 1 page; A = block id, CF = err
    ld    b, #1
    ld    c, #0x3D
    rst   #0x10
    jp    c, abort_fault
    ;; ESTEX $3A SETWIN2 — A = block id, B = page idx → map into W2
    ld    b, #0
    ld    c, #0x3A
    rst   #0x10
    jp    c, abort_fault
.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
    pop   ix                          ; restore IX
    ld    sp, #0xBFFE                 ; canonical stack top (W2)
.ifdef DEBUG_RT
    push  af                          ; preserve flag across gsinit/bank load
.endif

    ;; Stash startup prefix info from IX (asxxxx syntax: disp(ix)).
    ld    a, -3 (ix)
    ld    (_estex_file_handle), a
    ld    a, -2 (ix)
    ld    (_estex_block_id), a
    ld    (_estex_startup_ix), ix

.ifdef W3_RESIDENT
    ;; --w3 в HUGE: резидентный код окна W3 (0xC000) — часть HOME-образа,
    ;; замаплен DSS.  Загрузка банков ниже подменяет W3 своими страницами;
    ;; запомним физ-страницу резидента, чтобы вернуть её дефолтом после
    ;; цикла — тогда прямые вызовы в резидентный W3-код снова корректны.
    ;; (Трамплин bank.s сам сохраняет/восстанавливает W3 на каждый __banked
    ;; вызов, поэтому дефолтная страница обязана быть резидентной.)
    in    a, (#0xE2)
    ld    (_w3_resident_page), a
.endif

    ;; ---- Allocate _n_banks pages via ESTEX GETMEM ----
    ld    a, (_n_banks)
    or    a, a
    jr    Z, skip_bank_load          ; nothing to allocate

    ld    b, a
    ld    c, #0x3D                   ; ESTEX GETMEM
    rst   #0x10
    jp    c, abort_fault             ; CF=1 → out of memory
    ld    (_bank_block_id), a        ; remember block id

    ;; ---- For each bank i in 0..n-1 ----
    ld    b, #0                      ; B = page index inside block
load_bank_loop:
    ld    a, (_n_banks)
    cp    b
    jr    Z, load_bank_done          ; B == n_banks → finished

    ;; --- Get physical page for slot B from BIOS EMM ---
    push  bc                         ; save loop counter
    ld    a, (_bank_block_id)
    ld    c, #0xC4                   ; BIOS EMM_GETPAGE
    rst   #0x08
    pop   bc
    jp    c, abort_fault

    ;; A = physical page. Store in _bank_pages[B+1].
    push  af                         ; save A (the phys page)
    push  bc
    ld    c, b
    inc   c                          ; C = B+1 (1-based index)
    ld    b, #0
    ld    hl, #_bank_pages
    add   hl, bc
    pop   bc
    pop   af
    ld    (hl), a                    ; _bank_pages[B+1] = phys

    ;; Map this page into the banking window (W1 for BIG, W3 for HUGE).
    out   (#BANK_PORT), a

    ;; ESTEX READ: A=handle, HL=buf, DE=count, C=0x13
    push  bc                         ; save loop counter across RST
    ld    a, (_estex_file_handle)
    ld    hl, #BANK_LOAD_ADDR
    ld    de, #0x4000                ; 16384 bytes (one full bank)
    ld    c, #0x13                   ; ESTEX READ
    rst   #0x10
    pop   bc
    jp    c, abort_fault

    inc   b
    jr    load_bank_loop

load_bank_done:
    ;; Close the executable file — banks are in RAM now.
    ld    a, (_estex_file_handle)
    ld    c, #0x12                   ; ESTEX CLOSE
    rst   #0x10
    ;; Ignore CF from close.

.ifdef W3_RESIDENT
    ;; Вернуть резидентную страницу W3 дефолтом (цикл выше оставил в W3
    ;; последний банк).  После этого прямые call'ы в резидентный код 0xC0xx
    ;; корректны, а трамплины будут сохранять/возвращать именно её.
    ld    a, (_w3_resident_page)
    out   (#0xE2), a
.endif

skip_bank_load:
    ;; ---- Standard SDCC init path ----
    call  gsinit
.ifdef DEBUG_RT
    pop   af
    ld    (_w2_self_allocated), a
.endif

    ;; Parse cmdline into argv[] (BSS now zeroed by gsinit), then fix
    ;; argv[0] to the .EXE basename via APPINFO.  Both routines fail
    ;; gracefully (argv[0] stays empty if APPINFO returns CF=1).
    call  parse_argv
    call  get_progname

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

    ;; main returned: int return is in DE per SDCC 4.5 __sdcccall(1).
    ld    a, e
    jr    exit_with_a

_exit::
    ;; Userspace _exit(int code) — code in HL (single-arg ABI).
    ld    a, l

exit_with_a:
    ld    b, a
    ld    c, #0x41                   ; ESTEX EXIT
    rst   #0x10
1$: halt
    jr    1$

abort_fault:
    ;; Bail out with a sentinel exit code so failures are visible in the
    ;; emulator / shell.
    ld    b, #0xFE
    ld    c, #0x41
    rst   #0x10
1$: halt
    jr    1$

;; =========================================================================
;; parse_argv / get_progname — identical copy of crt0.s.  When we build
;; libsprinter.lib these two routines will be factored into argv.s and
;; linked once for all crt0 variants.
;; =========================================================================
    .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

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

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
;; =========================================================================
    .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
_estex_file_handle::
    .ds 1
_estex_block_id::
    .ds 1
_bank_block_id::
    .ds 1
.ifdef W3_RESIDENT
_w3_resident_page:
    .ds 1
.endif

    .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 — same semantics as in crt0.s / crt0_small.s.
_w2_self_allocated::
    .ds 1
.endif

;; =========================================================================
;; gsinit — same as crt0.s (zero _DATA + _BSS, copy INITIALIZER)
;; =========================================================================
    .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::
    ;; --- Zero _DATA + _BSS (C guarantees zero-valued statics) ---
    ld    hl, #s__DATA
    ld    bc, #l__DATA
    call  zero_area
    ld    hl, #s__BSS
    ld    bc, #l__BSS
    call  zero_area
    ;; --- Copy _INITIALIZER -> _INITIALIZED if non-empty ---
    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
