; =======================================================
; Ocean-240.2
; CP/M BDOS at 0xC800:D5FF
;
; Disassembled by Romych 2025-09-09
; ======================================================

    INCLUDE "io.inc"
    INCLUDE "equates.inc"
    INCLUDE "ram.inc"

    IFNDEF  BUILD_ROM
        OUTPUT bdos.bin
    ENDIF

    MODULE  BDOS

    ORG     0xC800

bdos_start:
    LD    SP, HL
    LD    D, 0x00
    NOP
    NOP
    LD    L, E

; --------------------------------------------------
; BDOS Entry point
; --------------------------------------------------
bdos_enter:
    JP    bdos_entrance

bdos_pere_addr:
    DW    bdos_persub                               ; permanent error
bdos_sele_addr:
    DW    bdos_selsub                               ; select error
bdos_rode_addr:
    DW    bdos_rodsub                               ; write to ro disk error
bdos_rofe_addr:
    DW    bdos_rofsub                               ; write to ro file error

; -------------------------------------------------------
; BDOS Handler
; Inp: C - func no
;      DE or E - parameter
; Out: A or HL - result
; -------------------------------------------------------
bdos_entrance:
    ; store parameter DE
    EX   DE, HL
    LD   (CPM_VARS.bdos_info), HL
    EX   DE, HL
    ; Store E
    LD   A, E
    LD   (CPM_VARS.bdos_linfo), A
    ; value to return, default = 0
    LD   HL, 0x00
    LD   (CPM_VARS.bdos_aret), HL

    ; Save user's stack pointer, set to local stack
    ADD  HL, SP
    LD   (CPM_VARS.bdos_usersp), HL

    LD   SP, CPM_VARS.bdos_stack                    ; local stack setup
    XOR  A
    LD   (CPM_VARS.bdos_fcbdsk), A                  ; fcbdsk,resel=false
    LD   (CPM_VARS.bdos_resel), A                   ; bdos_resel = FALSE
    LD   HL, bdos_goback
    PUSH HL                                         ; push goback address to return after jump
    LD   A, C
    CP   BDOS_NFUNCS
    RET  NC                                         ; return in func no out of range
    LD   C, E                                       ; store param E to C
    ; calculate offset in functab
    LD   HL, functab                                ; DE=func_no, HL->functab
    LD   E, A
    LD   D, 0
    ADD  HL, DE
    ADD  HL, DE
    LD   E, (HL)
    INC  HL
    LD   D, (HL)                                    ; DE=functab(func)
    LD   HL, (CPM_VARS.bdos_info)                   ; restore input parameter
    EX   DE, HL
    JP   (HL)                                       ; dispatch function

; -------------------------------------------------------
; BDOS function handlers address table
; -------------------------------------------------------
functab:
    DW    BIOS.wboot_f                               ; fn0
    DW    bdos_con_inp                               ; fn1
    DW    bdos_outcon                                ; fn2
    DW    bdos_aux_inp                               ; fn3
    DW    BIOS.punch_f                               ; fn4
    DW    BIOS.list_f                                ; fn5
    DW    bdos_dir_con_io                            ; fn6
    DW    bdos_get_io_byte                           ; fn7
    DW    bdos_set_io_byte                           ; fn8
    DW    bdos_print_str                             ; fn9
    DW    bdos_read                                  ; fn10 0x0a
    DW    bdos_get_con_st                            ; fn11 0x0b
    DW    bdos_get_version                           ; fn12 0x0c
    DW    bdos_reset_disks                           ; fn13 0x0d
    DW    bdos_select_disk                           ; fn14 0x0e
    DW    bdos_open_file                             ; fn15 0x0f
    DW    bdos_close_file                            ; fn16 0x10
    DW    bdos_search_first                          ; fn17 0x11
    DW    bdos_search_next                           ; fn18 0x12
    DW    bdos_rm_file                               ; fn19 0x13
    DW    bdos_read_file                             ; fn20 0x14
    DW    bdos_write_file                            ; fn21 0x15
    DW    bdos_make_file                             ; fn22 0x16
    DW    bdos_ren_file                              ; fn23 0x17
    DW    bdos_get_login_vec                         ; fn24 0x18
    DW    bdos_get_cur_drive                         ; fn25 0x19
    DW    bdos_set_dma_addr                          ; fn26 0x1a
    DW    bdos_get_alloc_addr                        ; fn27 0x1b
    DW    bdos_set_ro                                ; fn28 0x1c
    DW    bdos_get_wr_protect                        ; fn29 0x1d
    DW    bdos_set_attr                              ; fn30 0x1e
    DW    bdos_get_dpb                               ; fn31 0x1f
    DW    bdos_set_user                              ; fn32 0x20
    DW    bdos_rand_read                             ; fn33 0x21
    DW    bdos_rand_write                            ; fn34 0x22
    DW    bdos_compute_fs                            ; fn35 0x23
    DW    bdos_set_random                            ; fn36 0x24
    DW    bdos_reset_drives                          ; fn37 0x25
    DW    bdos_not_impl                              ; fn38 0x26 (Access Drive)
    DW    bdos_not_impl                              ; fn39 0x27 (Free Drive)
    DW    bdos_rand_write_z                          ; fn40 0x28

; -------------------------------------------------------
; Report permanent error
; -------------------------------------------------------
bdos_persub:
    LD    HL, permsg                                  ; = 'B'
    CALL  bdos_print_err
    CP    CTRL_C
    JP    Z, warm_boot
    RET

; -------------------------------------------------------
; Report select error
; -------------------------------------------------------
bdos_selsub:
    LD    HL, selmsg                                  ;= 'S'
    JP    bdos_wait_err

; -------------------------------------------------------
; Report write to read/only disk
; -------------------------------------------------------
bdos_rodsub:
    LD    HL, rodmsg                                  ;= 'R'
    JP    bdos_wait_err

; -------------------------------------------------------
; Report read/only file
; -------------------------------------------------------
bdos_rofsub:
    LD    HL, rofmsg                                  ;= 'F'

; -------------------------------------------------------
; Wait for response before boot
; -------------------------------------------------------
bdos_wait_err:
    CALL  bdos_print_err
    JP    warm_boot

; -------------------------------------------------------
; Error messages
; -------------------------------------------------------
dskmsg:
    DB    'Bdos Err On '
dskerr:
    DB    " : $"
permsg:
    DB    "Bad Sector$"
selmsg:
    DB    "Select$"
rofmsg:
    DB    "File "
rodmsg:
    DB    "R/O$"

; -------------------------------------------------------
; Print error to console, message address in HL
; -------------------------------------------------------
bdos_print_err:
    PUSH  HL                                        ; save second message pointer
    CALL  bdos_crlf
    ; set drive letter to message
    LD    A, (CPM_VARS.bdos_curdsk)
    ADD   A, 'A'
    LD    (dskerr), A

    LD    BC, dskmsg                                ; print first error message
    CALL  bdos_print
    POP   BC
    CALL  bdos_print                                ; print second message

; -------------------------------------------------------
; Console handlers
; Read console character to A
; -------------------------------------------------------
bdos_conin:
    LD    HL, CPM_VARS.bdos_kbchar
    LD    A, (HL)
    LD    (HL), 0x00
    OR    A
    RET   NZ
    ;no previous keyboard character ready
    JP    BIOS.conin_f

; -------------------------------------------------------
; Read character from console with echo
; -------------------------------------------------------
bdos_conech:
    CALL  bdos_conin
    CALL  bdos_chk_ctrl_char
    RET   C
    PUSH  AF
    LD    C, A
    CALL  bdos_outcon
    POP   AF
    RET

; -------------------------------------------------------
; Check for control char
; Onp: A - character
; Out: ZF if cr, lf, tab, or backspace
;      CF if code < 0x20
; -------------------------------------------------------
bdos_chk_ctrl_char:
    CP    ASCII_CR
    RET   Z
    CP    ASCII_LF
    RET   Z
    CP    ASCII_TAB
    RET   Z
    CP    ASCII_BS
    RET   Z
    CP    ' '
    RET

; -------------------------------------------------------
; check console  during output for
; Ctrl-S and Ctrl-C
; -------------------------------------------------------
bdos_conbrk:
    LD    A, (CPM_VARS.bdos_kbchar)
    OR    A
    JP    NZ, .if_key                                ; skip if key pressed
    CALL  BIOS.const_f
    AND   0x1
    RET   Z                                          ; return if no char ready
    CALL  BIOS.conin_f
    CP    CTRL_S
    JP    NZ, .no_crtl_s
    ; stop on crtl-s
    CALL  BIOS.conin_f
    CP    CTRL_C
    ; reboot on crtl-c
    JP    Z, warm_boot
    XOR   A
    RET
.no_crtl_s:
    LD    (CPM_VARS.bdos_kbchar), A                  ; character in A, save it
.if_key:
    LD    A, 0x1                                     ; return with true set in accumulator
    RET

; --------------------------------------------------
; Out char C to screen (and printer)
; --------------------------------------------------
bdos_conout:
    LD    A, (CPM_VARS.bdos_no_outflag)                 ; if set, skip output, only move cursor
    OR    A
    JP    NZ, .compout
    ; check console break
    PUSH  BC
    CALL  bdos_conbrk
    POP   BC
    ; output c
    PUSH  BC
    CALL  BIOS.conout_f
    POP   BC

    ; send to printer (list) device
    PUSH  BC
    LD    A, (CPM_VARS.bdos_prnflag)
    OR    A
    CALL  NZ, BIOS.list_f
    POP   BC

; -------------------------------------------------------
; Update cursor position
; -------------------------------------------------------
.compout:
    LD    A, C
    ; recall the character
    ; and compute column position
    LD    HL, CPM_VARS.bdos_column
    CP    ASCII_DEL                                 ; [DEL] key
    RET   Z                                         ; 0x7F dont move cursor
    INC   (HL)                                      ; inc column
    CP    ASCII_SP                                  ; return for normal character > 0x20
    RET   NC

    ; restore column position
    DEC   (HL)
    LD    A, (HL)
    OR    A
    RET   Z                                         ; return if column=0

    LD    A, C
    CP    ASCII_BS
    JP    NZ, .not_backsp
    ; backspace character
    DEC   (HL)                                      ; BKSP  -> column-1
    RET

.not_backsp:
    CP    ASCII_LF
    RET   NZ
    LD    (HL), 0                                   ; LF -> column=0
    RET

; -------------------------------------------------------
; Send C character with possible preceding '^' char
; -------------------------------------------------------
bdos_ctlout:
    LD    A, C
    CALL  bdos_chk_ctrl_char                        ; cy if not graphic (or special case)
    JP    NC, bdos_outcon                           ; normal output if non control char (tab, cr, lf)

    PUSH  AF
    LD    C, CTRL                                   ; '^'
    CALL  bdos_conout
    POP   AF
    OR    0x40                                      ; convert to letter equivalent (^M, ^J and so on)
    LD    C, A                                      ;

bdos_outcon:
    LD    A, C
    CP    ASCII_TAB
    JP    NZ, bdos_conout

    ; Print spaces until tab stop position
.to_tabpos:
    LD    C, ' '
    CALL  bdos_conout
    LD    A, (CPM_VARS.bdos_column)
    AND   00000111b                                  ; column mod 8 = 0 ?
    JP    NZ, .to_tabpos
    RET

; -------------------------------------------------------
; Output backspace - erase previous character
; -------------------------------------------------------
bdos_out_bksp:
    CALL  bdos_prn_bksp
    LD    C, ' '
    CALL  BIOS.conout_f

; -------------------------------------------------------
; Send backspace to console
; -------------------------------------------------------
bdos_prn_bksp:
    LD    C, ASCII_BS
    JP    BIOS.conout_f

; -------------------------------------------------------
; print #, cr, lf for ctlx, ctlu, ctlr functions
; then move to strtcol (starting column)
; -------------------------------------------------------
bdos_newline:
    LD    C, '#'
    CALL  bdos_conout
    CALL  bdos_crlf
    ; move the cursor to the starting position
.print_sp:
    LD    A, (CPM_VARS.bdos_column)                 ; a=column
    LD    HL, CPM_VARS.bdos_strtcol
    CP    (HL)
    RET   NC                                        ; ret id startcol>column
    LD    C, ' '                                    ; print space
    CALL  bdos_conout
    JP    .print_sp

; -------------------------------------------------------
; Out carriage return line feed sequence
; -------------------------------------------------------
bdos_crlf:
    LD    C, ASCII_CR
    CALL  bdos_conout
    LD    C, ASCII_LF
    JP    bdos_conout

; -------------------------------------------------------
; Print $-ended string
; Inp: BC -> str$
; -------------------------------------------------------
bdos_print:
    LD    A, (BC)
    CP    '$'
    RET   Z
    INC   BC
    PUSH  BC
    LD    C, A
    CALL  bdos_outcon
    POP   BC
    JP    bdos_print

; -------------------------------------------------------
; Buffered console input
; Reads characters from the keyboard into a memory buffer
; until RETURN is pressed.
; Inp: C=0Ah
; DE=address or zero
; -------------------------------------------------------
bdos_read:
    LD    A, (CPM_VARS.bdos_column)
    LD    (CPM_VARS.bdos_strtcol), A
    LD    HL, (CPM_VARS.bdos_info)
    LD    C, (HL)
    INC   HL
    PUSH  HL
    LD    B, 0
    ; B = current buffer length,
    ; C = maximum buffer length,
    ; HL= next to fill - 1
.readnx:
    PUSH  BC
    PUSH  HL
.readn1:
    CALL  bdos_conin
    AND   0x7f                                      ; strip 7th bit
    POP   HL
    POP   BC
    ; end of line?
    CP    ASCII_CR
    JP    Z, .read_end
    CP    ASCII_LF
    JP    Z, .read_end

    ; backspace ?
    CP    ASCII_BS
    JP    NZ, .no_bksp
    LD    A, B
    OR    A
    JP    Z, .readnx                                ; ignore BKSP for column=0
    DEC   B
    LD    A, (CPM_VARS.bdos_column)
    LD    (CPM_VARS.bdos_no_outflag), A
    JP    .linelen

    ; not a backspace
.no_bksp:
    CP    ASCII_DEL                                 ; [DEL] Key
    JP    NZ, .no_del
    LD    A, B
    OR    A
    JP    Z, .readnx                                ; ignore DEL for column=0
    LD    A, (HL)                                   ; cur char
    DEC   B                                         ; dec column
    DEC   HL                                        ; point to previous char
    JP    .echo                                     ; out previous char

.no_del:
    CP    CTRL_E                                    ; ^E - physical end of line
    JP    NZ, .no_ctrle
    PUSH  BC
    PUSH  HL
    CALL  bdos_crlf                                 ; out CR+LF
    XOR   A
    LD    (CPM_VARS.bdos_strtcol), A                ; reset start column to 0
    JP    .readn1

.no_ctrle:
    CP    CTRL_P                                    ; ^P
    JP    NZ, .no_ctrlp
    PUSH  HL
    LD    HL, CPM_VARS.bdos_prnflag
    LD    A, 0x1
    SUB   (HL)                                      ; flip printer output flag
    LD    (HL), A
    POP   HL
    JP    .readnx

.no_ctrlp:
    CP    CTRL_X                                    ; cancel current cmd line
    JP    NZ, .no_ctrlx
    POP   HL
.backx:
    LD    A, (CPM_VARS.bdos_strtcol)
    LD    HL, CPM_VARS.bdos_column
    CP    (HL)
    JP    NC, bdos_read
    DEC   (HL)
    CALL  bdos_out_bksp
    JP    .backx

.no_ctrlx:
    CP    CTRL_U                                    ; cancel current cmd line
    JP    NZ, .no_ctrlu

    CALL  bdos_newline
    POP   HL
    JP    bdos_read

.no_ctrlu:
    CP    CTRL_R                                    ; repeats current cmd line
    JP    NZ, .stor_echo

    ; start new line and retype
.linelen:
    PUSH  BC
    CALL  bdos_newline
    POP   BC
    POP   HL
    PUSH  HL
    PUSH  BC
.next_char:
    LD    A, B
    OR    A
    JP    Z, .endof_cmd                             ; end of cmd line?
    INC   HL
    LD    C, (HL)
    DEC   B
    PUSH  BC
    PUSH  HL
    CALL  bdos_ctlout
    POP   HL
    POP   BC
    JP    .next_char
.endof_cmd:
    PUSH  HL
    LD    A, (CPM_VARS.bdos_no_outflag)
    OR    A
    JP    Z, .readn1                                ; if no display, read next characters
    ; update cursor pos
    LD    HL, CPM_VARS.bdos_column
    SUB   (HL)
    LD    (CPM_VARS.bdos_no_outflag), A
    ; move back compcol-column spaces

.backsp:
    CALL  bdos_out_bksp
    LD    HL, CPM_VARS.bdos_no_outflag
    DEC   (HL)
    JP    NZ, .backsp
    JP    .readn1

; Put and Echo normal character
.stor_echo:
    INC   HL
    LD    (HL), A
    INC   B
.echo:
    PUSH  BC
    PUSH  HL
    LD    C, A
    CALL  bdos_ctlout
    POP   HL
    POP   BC

    ; warm boot on ctrl+c only at start of line
    LD   A, (HL)
    CP   CTRL_C
    LD   A, B
    JP   NZ, .no_ctrlc
    CP   1
    JP   Z, warm_boot

.no_ctrlc:
    CP   C                                          ; buffer is full?
    JP   C, .readnx                                 ; get next char in not full

    ; End of read operation, store length
.read_end:
    POP  HL
    LD   (HL), B
    LD   C, ASCII_CR
    JP   bdos_conout                                ; Our CR and return

; -------------------------------------------------------
; Console input with echo (C_READ)
; Out: A=L=character
; -------------------------------------------------------
bdos_con_inp:
    CALL bdos_conech
    JP   bdos_ret_a

; -------------------------------------------------------
; Console input chartacter
; Out: A=L=ASCII character
; -------------------------------------------------------
bdos_aux_inp:
    CALL BIOS.reader_f
    JP   bdos_ret_a

; -------------------------------------------------------
; Direct console I/O
; Inp: E=code.
; E=code. Returned values (in A) vary.
; 0xff  Return a character without echoing if one is
; waiting; zero if none
; 0xfe  Return console input status. Zero if no
; character is waiting
; Out: A
; -------------------------------------------------------
bdos_dir_con_io:
    LD   A, C
    INC  A
    JP   Z, .dirinp
    INC  A
    JP   Z, BIOS.const_f
    JP   BIOS.conout_f
.dirinp:
    CALL BIOS.const_f
    OR   A
    JP   Z, bdos_ret_mon
    CALL BIOS.conin_f
    JP   bdos_ret_a

; -------------------------------------------------------
; Get I/O byte
; Out: A = I/O byte.
; -------------------------------------------------------
bdos_get_io_byte:
    LD   A, (iobyte)
    JP   bdos_ret_a

; -------------------------------------------------------
; Set I/O byte
; Inp: C=I/O byte.
; TODO: Check, will be E ?
; -------------------------------------------------------
bdos_set_io_byte:
    LD   HL, iobyte
    LD   (HL), C
    RET

; -------------------------------------------------------
; Out $-ended string
; Inp: DE -> string$.
; -------------------------------------------------------
bdos_print_str:
    EX   DE, HL
    LD   C, L
    LD   B, H
    JP   bdos_print

; -------------------------------------------------------
; Get console status
; Inp: A=L=status
; -------------------------------------------------------
bdos_get_con_st:
    CALL bdos_conbrk
bdos_ret_a:
    LD   (CPM_VARS.bdos_aret), A
bdos_not_impl:
    RET

; -------------------------------------------------------
; Set IO Error status = 1
; -------------------------------------------------------
set_ioerr_1:
    LD   A, 1
    JP   bdos_ret_a

; -------------------------------------------------------
; Report select error
; -------------------------------------------------------
bdos_sel_error:
    LD   HL, bdos_sele_addr

; indirect Jump to [HL]
bdos_jump_hl:
    LD   E, (HL)
    INC  HL
    LD   D, (HL)                                    ; ->bdos_sele_addr+1
    EX   DE, HL
    JP   (HL)                                       ; ->bdos_selsub

; -------------------------------------------------------
; Move C bytes from [DE] to [HL]
; -------------------------------------------------------
bdos_move_dehl:
    INC  C
.move_next:
    DEC  C
    RET  Z
    LD   A, (DE)
    LD   (HL), A
    INC  DE
    INC  HL
    JP   .move_next

; -------------------------------------------------------
; Select the disk drive given by curdsk, and fill
; the base addresses curtrka - alloca, then fill
; the values of the disk parameter block
; Out: ZF - for big disk
; -------------------------------------------------------
bdos_sel_dsk:
    LD   A, (CPM_VARS.bdos_curdsk)
    LD   C, A
    CALL BIOS.seldsk_f                              ; HL filled by call
    ; HL = 0000 if error, otherwise -> DPH
    LD   A, H
    OR   L
    RET  Z                                          ; return if not valid drive

    LD   E, (HL)
    INC  HL
    LD   D, (HL)
    ; DE -> disk header

    ; save pointers
    INC  HL
    LD   (CPM_VARS.bdos_cdrmax), HL
    INC  HL
    INC  HL
    LD   (CPM_VARS.bdos_curtrk), HL
    INC  HL
    INC  HL
    LD   (CPM_VARS.bdos_currec), HL
    INC  HL
    INC  HL
    ; save translation vector (table) addr
    EX   DE, HL
    LD   (CPM_VARS.bdos_tranv), HL                  ; tran vector
    LD   HL, CPM_VARS.bdos_buffa                    ; DE=source for move, HL=dest
    LD   C, 8
    CALL bdos_move_dehl

    ; Fill the disk parameter block
    LD   HL, (CPM_VARS.bdos_dpbaddr)
    EX   DE, HL
    LD   HL, CPM_VARS.bdos_sectpt
    LD   C, 15
    CALL bdos_move_dehl

    ; Set single/double map mode
    LD   HL, (CPM_VARS.bdos_maxall)                 ; largest allocation number
    LD   A, H                                       ; a=h>0 for large disks (>255 blocks)
    LD   HL, CPM_VARS.bdos_small_disk
    LD   (HL), TRUE                                 ; small
    OR   A
    JP   Z, .is_small
    LD   (HL), FALSE                                ; big disk > 255 sec
.is_small:
    LD   A, 0xff
    OR   A
    RET

; -------------------------------------------------------
; Move to home position,
; then reset pointers for track and sector
; -------------------------------------------------------
bdos_home:
    CALL BIOS.home_f                                ; home head
    XOR  A
    ;  set track pointer to 0x0000
    LD   HL, (CPM_VARS.bdos_curtrk)
    LD   (HL), A
    INC  HL
    LD   (HL), A

    ; set current record pointer to 0x0000
    LD   HL, (CPM_VARS.bdos_currec)
    LD   (HL), A
    INC   HL
    LD   (HL), A

    RET

; -------------------------------------------------------
; Read buffer and check condition
; -------------------------------------------------------
bdos_rdbuff:
    CALL  BIOS.read_f
    JP    diocomp                                    ; check for i/o errors

; -------------------------------------------------------
; Write buffer and check condition
; Inp: C - write type
; 0 - normal write operation
; 1 - directory write operation
; 2 - start of new block
; -------------------------------------------------------
bdos_wrbuff:
    CALL  BIOS.write_f

; -------------------------------------------------------
; Check for disk errors
; -------------------------------------------------------
diocomp:
    OR    A
    RET   Z
    LD    HL, bdos_pere_addr                         ; = C899h
    JP    bdos_jump_hl

; -------------------------------------------------------
; Seek the record containing the current dir entry
; -------------------------------------------------------
bdos_seekdir:
    LD    HL, (CPM_VARS.bdos_dcnt)
    LD    C, DSK_SHF
    CALL  bdos_hlrotr
    LD    (CPM_VARS.bdos_arecord), HL
    LD    (CPM_VARS.bdos_drec), HL

; -------------------------------------------------------
; Seek the track given by arecord (actual record)
; local equates for registers
; -------------------------------------------------------
bdos_seek:
    LD   HL, CPM_VARS.bdos_arecord
    LD   C, (HL)
    INC  HL
    LD   B, (HL)
    ; BC = sector number

    LD   HL, (CPM_VARS.bdos_currec)
    LD   E, (HL)
    INC  HL
    LD   D, (HL)
    ; DE = current sector number

    LD   HL, (CPM_VARS.bdos_curtrk)
    LD   A, (HL)
    INC  HL
    LD   H, (HL)
    LD   L, A
    ; HL = current track

.sec_less_cur:
    ; check specified sector before current
    LD   A, C
    SUB  E
    LD   A, B
    SBC  A, D
    JP   NC, .sec_ge_cur
    ; yes, decrement current sectors for one track
    PUSH HL
    LD   HL, (CPM_VARS.bdos_sectpt)                 ; sectors per track
    LD   A, E
    SUB  L
    LD   E, A
    LD   A, D
    SBC  A, H
    LD   D, A
    ; current track-1
    POP  HL
    DEC  HL
    JP   .sec_less_cur
    ; specified sector >= current
.sec_ge_cur:
    PUSH HL
    LD   HL, (CPM_VARS.bdos_sectpt)
    ADD  HL, DE                                     ; current sector+track
    JP   C, .track_found                            ; jump if after current
    ; sector before current
    LD   A, C
    SUB  L
    LD   A, B
    SBC  A, H
    JP   C, .track_found
    EX   DE, HL
    POP  HL
    ; increment track
    INC  HL
    JP   .sec_ge_cur
    ; determined track number with specified sector
.track_found:
    POP  HL
    PUSH BC
    PUSH DE
    PUSH HL
    ; Stack contains (lowest) BC=arecord, DE=currec, HL=curtrk
    EX   DE, HL
    LD   HL, (CPM_VARS.bdos_offset)                 ; adjust if have reserved tracks
    ADD  HL, DE
    LD   B, H
    LD   C, L
    CALL BIOS.settrk_f                              ; select track

    ; save current track
    POP  DE                                         ; curtrk
    LD   HL, (CPM_VARS.bdos_curtrk)
    LD   (HL), E
    INC  HL
    LD   (HL), D

    ; save current record
    POP  DE
    LD   HL, (CPM_VARS.bdos_currec)
    LD   (HL), E
    INC  HL
    LD   (HL), D

    ; calc relative offset between arecord and currec
    POP  BC
    LD   A, C
    SUB  E
    LD   C, A
    LD   A, B
    SBC  A, D
    LD   B, A

    LD   HL, (CPM_VARS.bdos_tranv)                  ; HL -> translation table
    EX   DE, HL
    ; translate via BIOS call
    CALL BIOS.sectran_f
    ; select translated sector and return
    LD   C, L
    LD   B, H
    JP   BIOS.setsec_f

; -------------------------------------------------------
; Compute block number from record number and extent
; number
; -------------------------------------------------------
bdos_dm_position:
    LD   HL, CPM_VARS.bdos_blkshf
    LD   C, (HL)
    LD   A, (CPM_VARS.bdos_vrecord)                ; record number
    ;  a = a / 2^blkshf

dmpos_l0:
    OR   A
    RRA
    DEC  C
    JP   NZ, dmpos_l0
    ; A = shr(vrecord, blkshf) = vrecord/2^(sect/block)
    LD   B, A
    LD   A, 8
    SUB  (HL)
    ; c = 8 - blkshf
    LD   C, A
    LD   A, (CPM_VARS.bdos_extval)
    ; a = extval * 2^(8-blkshf)
dmpos_l1:
    DEC  C
    JP   Z, dmpos_l2
    OR   A
    RLA
    JP   dmpos_l1
dmpos_l2:
    ADD  A, B
    RET                                             ; with dm_position in A

; -------------------------------------------------------
; Return disk map value from position given by BC
; -------------------------------------------------------
bdos_getdm:
    LD   HL, (CPM_VARS.bdos_info)
    LD   DE, DSK_MAP
    ADD  HL, DE
    ADD  HL, BC
    LD   A, (CPM_VARS.bdos_small_disk)
    OR   A
    JP   Z,getdmd
    LD   L, (HL)
    LD   H, 0x00
    RET

getdmd:
    ADD  HL, BC
    LD   E, (HL)
    INC  HL
    LD   D, (HL)
    EX   DE, HL
    RET

; -------------------------------------------------------
; Compute disk block number from current FCB
; -------------------------------------------------------
bdos_index:
    CALL bdos_dm_position
    LD   C, A
    LD   B, 0
    CALL bdos_getdm
    LD   (CPM_VARS.bdos_arecord), HL
    RET

; -------------------------------------------------------
; Called following index to see if block allocated
; Out: ZF if not allocated
; -------------------------------------------------------
bdos_allocated:
    LD   HL, (CPM_VARS.bdos_arecord)
    LD   A, L
    OR   H
    RET

; -------------------------------------------------------
; Compute actual record address, assuming index called
; -------------------------------------------------------
bdos_atran:
    LD   A, (CPM_VARS.bdos_blkshf)
    LD   HL, (CPM_VARS.bdos_arecord)
bdatarn_l0:
    ADD  HL, HL
    DEC  A
    JP   NZ, bdatarn_l0
    LD   (CPM_VARS.bdos_arecord1), HL
    LD   A, (CPM_VARS.bdos_blmsk)
    LD   C, A
    LD   A, (CPM_VARS.bdos_vrecord)
    AND  C
    OR   L
    LD   L, A
    LD   (CPM_VARS.bdos_arecord), HL
    RET

; -------------------------------------------------------
; Get current extent field address to A
; -------------------------------------------------------
bdos_getexta:
    LD   HL, (CPM_VARS.bdos_info)
    LD   DE, FCB_EXT
    ADD  HL, DE                                     ; HL -> .fcb(extnum)
    RET

; -------------------------------------------------------
; Compute reccnt and nxtrec addresses for get/setfcb
; Out: HL -> record count byte in fcb
;      DE -> next record number byte in fcb
; -------------------------------------------------------
bdos_fcb_rcnr:
    LD   HL, (CPM_VARS.bdos_info)
    LD   DE, FCB_RC
    ADD  HL, DE
    EX   DE, HL
    LD   HL, 17                                   ; (nxtrec-reccnt)
    ADD  HL, DE
    RET

; -------------------------------------------------------
; Set variables from currently addressed fcb
; -------------------------------------------------------
bdos_stor_fcb_var:
    CALL bdos_fcb_rcnr
    LD   A, (HL)
    LD   (CPM_VARS.bdos_vrecord), A
    EX   DE, HL
    LD   A, (HL)
    LD   (CPM_VARS.bdos_rcount), A
    CALL bdos_getexta
    LD   A, (CPM_VARS.bdos_extmsk)
    AND  (HL)
    LD   (CPM_VARS.bdos_extval), A
    RET

; -------------------------------------------------------
; Set the next record to access.
; Inp: HL -> fcb next record byte
; -------------------------------------------------------
bdos_set_nxt_rec:
    CALL bdos_fcb_rcnr
    LD   A, (CPM_VARS.bdos_seqio)                  ; sequential mode flag (=1)
    CP   2
    JP   NZ, bsfcb_l0
    XOR  A                                         ; if 2 - no adder needed
bsfcb_l0:
    LD   C, A
    LD   A, (CPM_VARS.bdos_vrecord)                ; last record number
    ADD  A, C
    LD   (HL), A
    EX   DE, HL
    LD   A, (CPM_VARS.bdos_rcount)                 ; next record byte
    LD   (HL), A
    RET

; -------------------------------------------------------
; Rotate HL right by C bits
; -------------------------------------------------------
bdos_hlrotr:
    INC  C
bhlr_nxt:
    DEC  C
    RET  Z
    LD   A, H
    OR   A
    RRA
    LD   H, A
    LD   A,L
    RRA
    LD   L, A
    JP   bhlr_nxt

; -------------------------------------------------------
; Compute checksum for current directory buffer
; Out: A - checksum
; -------------------------------------------------------
bdos_compute_cs:
    LD   C, DIR_BUFF_SIZE                           ; size of directory buffer
    LD   HL, (CPM_VARS.bdos_buffa)
    XOR  A
bcompcs_add_nxt:
    ADD  A, (HL)
    INC  HL
    DEC  C
    JP   NZ, bcompcs_add_nxt
    RET

; -------------------------------------------------------
; Rotate HL left by C bits
; -------------------------------------------------------
bdos_hlrotl:
    INC  C
.nxt:
    DEC  C
    RET  Z
    ADD  HL, HL
    JP   .nxt

; -------------------------------------------------------
; Set a "1" value in curdsk position
; Inp: BC - vector
; Out: HL - vector with bit set
; -------------------------------------------------------
bdos_set_dsk_bit:
    PUSH BC
    LD   A, (CPM_VARS.bdos_curdsk)
    LD   C, A
    LD   HL, 0x0001
    CALL bdos_hlrotl
    POP  BC
    LD   A, C
    OR   L
    LD   L, A
    LD   A, B
    OR   H
    LD   H, A
    RET

; -------------------------------------------------------
; Get write protect status
; -------------------------------------------------------
bdos_nowrite:
    LD   HL, (CPM_VARS.bdos_rodsk)
    LD   A, (CPM_VARS.bdos_curdsk)
    LD   C, A
    CALL bdos_hlrotr
    LD   A, L
    AND  0x1
    RET

; -------------------------------------------------------
; Temporarily set current drive to be read-only;
; attempts to write to it will fail
; -------------------------------------------------------
bdos_set_ro:
    LD   HL, CPM_VARS.bdos_rodsk
    LD   C, (HL)
    INC  HL
    LD   B, (HL)

    CALL bdos_set_dsk_bit
    LD   (CPM_VARS.bdos_rodsk), HL
    ; high water mark in directory goes to max
    LD   HL, (CPM_VARS.bdos_dirmax)
    INC  HL
    EX   DE, HL
    LD   HL, (CPM_VARS.bdos_cdrmax)
    LD   (HL), E
    INC  HL
    LD   (HL), D
    RET

; -------------------------------------------------------
; Check current directory element for read/only status
; -------------------------------------------------------
bdos_check_rodir:
    CALL bdos_getdptra

; -------------------------------------------------------
; Check current buff(dptr) or fcb(0) for r/o status
; -------------------------------------------------------
bdos_check_rofile:
    LD   DE, RO_FILE
    ADD  HL, DE
    LD   A, (HL)
    RLA
    RET  NC
    LD   HL, bdos_rofe_addr                         ; = C8B1h
    JP   bdos_jump_hl

; -------------------------------------------------------
; Check for write protected disk
; -------------------------------------------------------
bdos_check_write:
    CALL bdos_nowrite
    RET  Z
    LD   HL, bdos_rode_addr                         ; = C8ABh
    JP   bdos_jump_hl

; -------------------------------------------------------
; Compute the address of a directory element at
; positon dptr in the buffer
; -------------------------------------------------------
bdos_getdptra:
    LD   HL, (CPM_VARS.bdos_buffa)
    LD   A, (CPM_VARS.bdos_dptr)

; -------------------------------------------------------
; HL = HL + A
; -------------------------------------------------------
bdos_hl_add_a:
    ADD  A,L
    LD   L, A
    RET  NC
    INC  H
    RET

; -------------------------------------------------------
; Compute the address of the s2 from fcb
; Out: A - module number
;      HL -> module number
; -------------------------------------------------------
bdos_get_s2:
    LD    HL, (CPM_VARS.bdos_info)
    LD    DE, FCB_S2
    ADD   HL, DE
    LD    A, (HL)
    RET

; -------------------------------------------------------
; Clear the module number field for user open/make
; -------------------------------------------------------
bdos_clear_s2:
    CALL  bdos_get_s2
    LD    (HL), 0
    RET

; -------------------------------------------------------
; Set FWF (File Write Flag)
; -------------------------------------------------------
bdos_set_fwf:
    CALL  bdos_get_s2
    OR    FWF_MASK
    LD    (HL), A
    RET

; -------------------------------------------------------
; Check for free entries in directory
; Out: CF is set - directory have more file entries
;      CF is reset - directory is full
; -------------------------------------------------------
bdos_dir_is_free:
    LD    HL, (CPM_VARS.bdos_dcnt)
    EX    DE, HL                                        ; DE = directory counter
    LD    HL, (CPM_VARS.bdos_cdrmax)                    ; HL = cdrmax
    LD    A, E
    SUB   (HL)
    INC   HL
    LD    A, D
    SBC   A, (HL)
    ; Condition dcnt - cdrmax produces cy if cdrmax>dcnt
    RET

; -------------------------------------------------------
; If not (cdrmax > dcnt) then cdrmax = dcnt+1
; -------------------------------------------------------
bdos_setcdr:
    CALL  bdos_dir_is_free
    RET   C
    INC   DE
    LD    (HL), D
    DEC   HL
    LD    (HL), E
    RET

; -------------------------------------------------------
; HL = DE - HL
; -------------------------------------------------------
bdos_de_sub_hl:
    LD    A, E
    SUB   L
    LD    L, A
    LD    A, D
    SBC   A, H
    LD    H, A
    RET

; Set directory checksubm byte
bdos_newchecksum:
    LD    C, 0xff


; -------------------------------------------------------
bdos_checksum:
    LD    HL, (CPM_VARS.bdos_drec)
    EX    DE, HL
    LD    HL, (CPM_VARS.bdos_alloc1)
    CALL  bdos_de_sub_hl
    RET   NC
    PUSH  BC
    CALL  bdos_compute_cs                            ; Compute checksum to A for current dir buffer
    LD    HL, (CPM_VARS.bdos_cksum)
    EX    DE, HL
    LD    HL, (CPM_VARS.bdos_drec)
    ADD   HL, DE
    POP   BC
    INC   C
    JP    Z, initial_cs
    CP    (HL)
    RET   Z
    CALL  bdos_dir_is_free
    RET   NC
    CALL  bdos_set_ro
    RET
initial_cs:
    LD    (HL), A
    RET

; -------------------------------------------------------
; Write the current directory entry, set checksum
; -------------------------------------------------------
bdos_wrdir:
    CALL  bdos_newchecksum
    CALL  bdos_setdir
    LD    C, 0x1
    CALL  bdos_wrbuff
    JP    bdos_setdata

; -------------------------------------------------------
; Read a directory entry into the directory buffer
; -------------------------------------------------------
bdos_rd_dir:
    CALL  bdos_setdir
    CALL  bdos_rdbuff

; -------------------------------------------------------
; Set data dma address
; -------------------------------------------------------
bdos_setdata:
    LD    HL, CPM_VARS.bdos_dmaad
    JP    bdos_set_dma

; -------------------------------------------------------
; Set directory dma address
; -------------------------------------------------------
bdos_setdir:
    LD    HL, CPM_VARS.bdos_buffa

; -------------------------------------------------------
; HL -> dma address to set (i.e., buffa or dmaad)
; -------------------------------------------------------
bdos_set_dma:
    LD    C, (HL)
    INC   HL
    LD    B, (HL)
    JP    BIOS.setdma_f

; -------------------------------------------------------
; Copy the directory entry to the user buffer
; after call to search or searchn by user code
; -------------------------------------------------------
bdos_dir_to_user:
    LD    HL, (CPM_VARS.bdos_buffa)
    EX    DE, HL
    LD    HL, (CPM_VARS.bdos_dmaad)
    LD    C, DIR_BUFF_SIZE
    JP    bdos_move_dehl

; -------------------------------------------------------
; return zero flag if at end of directory, non zero
; if not at end (end of dir if dcnt = 0ffffh)
; -------------------------------------------------------
bdos_end_of_dir:
    LD   HL, CPM_VARS.bdos_dcnt
    LD   A, (HL)
    INC  HL
    CP   (HL)
    RET  NZ
    INC  A
    RET

; -------------------------------------------------------
; Set dcnt to the end of the directory
; -------------------------------------------------------
bdos_set_end_dir:
    LD   HL, ENDDIR
    LD   (CPM_VARS.bdos_dcnt), HL
    RET

; -------------------------------------------------------
; Read next directory entry, with C=true if initializing
; -------------------------------------------------------
bdos_read_dir:
    LD   HL, (CPM_VARS.bdos_dirmax)
    EX   DE, HL
    LD   HL, (CPM_VARS.bdos_dcnt)
    INC  HL
    LD   (CPM_VARS.bdos_dcnt), HL
    CALL bdos_de_sub_hl
    JP   NC, bdrd_l0
    JP   bdos_set_end_dir
bdrd_l0:
    LD   A, (CPM_VARS.bdos_dcnt)
    AND  DSK_MSK
    LD   B, FCB_SHF
bdrd_l1:
    ADD  A, A
    DEC  B
    JP   NZ, bdrd_l1
    LD   (CPM_VARS.bdos_dptr), A
    OR   A
    RET  NZ
    PUSH BC
    CALL bdos_seekdir
    CALL bdos_rd_dir
    POP  BC
    JP   bdos_checksum

; -------------------------------------------------------
; Given allocation vector position BC, return with byte
; containing BC shifted so that the least significant
; bit is in the low order accumulator position.  HL is
; the address of the byte for possible replacement in
; memory upon return, and D contains the number of shifts
; required to place the returned value back into position
; -------------------------------------------------------
bdos_getallocbit:
    LD   A, C
    AND  00000111b
    INC  A
    LD   E, A
    LD   D, A
    LD   A, C
    RRCA
    RRCA
    RRCA
    AND  00011111b
    LD   C, A
    LD   A, B
    ADD  A, A
    ADD  A, A
    ADD  A, A
    ADD  A, A
    ADD  A, A
    OR   C
    LD   C, A
    LD   A, B
    RRCA
    RRCA
    RRCA
    AND  00011111b
    LD   B, A
    LD   HL, (CPM_VARS.bdos_alloca)                  ; Base address of allocation vector
    ADD  HL, BC
    LD   A, (HL)
ga_rotl:
    RLCA
    DEC  E
    JP   NZ, ga_rotl
    RET

; -------------------------------------------------------
; BC is the bit position of ALLOC to set or reset.  The
; value of the bit is in register E.
; -------------------------------------------------------
bdos_setallocbit:
    PUSH DE
    CALL bdos_getallocbit
    AND  11111110b
    POP  BC
    OR   C

; -------------------------------------------------------
; Rotate and replace
; Inp: A - Byte value from ALLOC
;      D - bit position (1..8)
;      HL - target ALLOC position
; -------------------------------------------------------
bdos_sab_rotr:
    RRCA
    DEC  D
    JP   NZ, bdos_sab_rotr
    LD   (HL), A
    RET

; -------------------------------------------------------
; Set or clear space used bits in allocation map
; Inp: C=0 - clear, C=1 - set
; -------------------------------------------------------
bdos_scandm:
    CALL bdos_getdptra
    ; HL addresses the beginning of the directory entry
    LD   DE, DSK_MAP
    ADD  HL, DE                                     ; HL -> block number byte
    PUSH BC
    LD   C, 17                                      ; fcblen-dskmap+1
    ; scan all 17 bytes
.scan_next:
    POP  DE
    DEC  C
    RET  Z                                          ; return if done

    PUSH DE
    ; small or bug disk?
    LD   A, (CPM_VARS.bdos_small_disk)
    OR   A
    JP   Z, .scan_big
    PUSH BC
    PUSH HL
    ; small, one byte
    LD   C, (HL)
    LD   B, 0
    JP   .scan_cmn
.scan_big:
    ; big, two byte
    DEC  C
    PUSH BC
    LD   C, (HL)
    INC  HL
    LD   B, (HL)
    PUSH HL

.scan_cmn:
    ; this block used?
    LD   A, C
    OR   B
    JP   Z, .not_used
    ; have free space on disk?
    LD   HL, (CPM_VARS.bdos_maxall)
    LD   A, L
    SUB  C
    LD   A, H
    SBC  A, B
    CALL NC, bdos_setallocbit                       ; enough space, set bit

.not_used:
    POP  HL                                         ; HL -> next block in fcb
    INC  HL
    POP  BC
    JP   .scan_next

; -------------------------------------------------------
; Initialize the current disk
; lret = false, set to true if $ file exists
; compute the length of the allocation vector - 2
; -------------------------------------------------------
bdos_initialize:
    ; compute size of alloc table
    LD   HL, (CPM_VARS.bdos_maxall)
    LD   C, 3
    CALL bdos_hlrotr                                ; HL = disk size / 8 + 1
    INC  HL
    LD   B, H
    LD   C, L                                       ; BC = alloc table size
    LD   HL, (CPM_VARS.bdos_alloca)                 ; address of allocation table
    ; fill with zeroes
.alt_fill_0:
    LD   (HL), 0
    INC  HL
    DEC  BC
    LD   A, B
    OR   C
    JP   NZ, .alt_fill_0

    LD   HL, (CPM_VARS.bdos_dirblk)                 ; DE = initial space, used by directory
    EX   DE, HL
    LD   HL, (CPM_VARS.bdos_alloca)                 ; HL -> allocation map
    LD   (HL), E
    INC   HL
    LD   (HL), D                                    ; [HL] = DE
    CALL bdos_home                                  ; home drive, set initial head, track, sector

    LD   HL, (CPM_VARS.bdos_cdrmax)
    LD   (HL), 3                                    ; next dir read
    INC  HL
    LD   (HL), 0
    CALL bdos_set_end_dir                           ; mark end of dir

.rd_next_f:
    ; read next file name
    LD   C, 0xff
    CALL bdos_read_dir
    CALL bdos_end_of_dir                            ; is have another file?
    RET  Z
    ; get params of file, and check deleted
    CALL bdos_getdptra
    LD   A, FILE_DELETED
    CP   (HL)
    JP   Z, .rd_next_f
    ; check user code
    LD   A, (CPM_VARS.bdos_userno)
    CP   (HL)
    JP   NZ, .set_as_used
    INC  HL
    LD   A, (HL)
    SUB  '$'                                        ; check for $name file
    JP   NZ, .set_as_used
    ; dollar file found, mark in lret
    DEC  A
    LD   (CPM_VARS.bdos_aret), A

.set_as_used:
    LD   C, 1
    CALL bdos_scandm
    CALL bdos_setcdr
    JP   .rd_next_f

; -------------------------------------------------------
; Return directory location as lret
; used in delete, rename, ...
; -------------------------------------------------------
bdos_ret_dirloc:
    LD   A, (CPM_VARS.bdos_dirloc)
    JP   bdos_ret_a

; -------------------------------------------------------
; Compare extent# in A with that in C, return nonzero
; if they do not match
; -------------------------------------------------------
bdos_compext:
    PUSH BC
    PUSH AF
    LD   A, (CPM_VARS.bdos_extmsk)
    CPL
    LD   B, A
    LD   A, C
    AND  B
    LD   C, A
    POP  AF
    AND  B
    SUB  C
    AND  MAX_EXT                                    ; check only bits [4:0] (MAX_EXT)
    POP  BC
    RET

; -------------------------------------------------------
; Search for directory element of length C at info
; -------------------------------------------------------
bdos_search:
    LD   A, 0xff
    LD   (CPM_VARS.bdos_dirloc), A
    LD   HL, CPM_VARS.bdos_searchl                  ; length in bytes to match
    LD   (HL), C
    LD   HL, (CPM_VARS.bdos_info)                   ; address filename to match
    LD   (CPM_VARS.bdos_searcha), HL
    CALL bdos_set_end_dir
    CALL bdos_home

; -------------------------------------------------------
; Search for the next directory element, assuming
; a previous call on search which sets searcha and
; searchl
; -------------------------------------------------------
bdos_search_nxt:
    LD   C, 0
    CALL bdos_read_dir                              ; get next filename entry
    CALL bdos_end_of_dir                            ; at end? pos=0xffff
    JP   Z, .not_found                              ; jump at end

    LD   HL, (CPM_VARS.bdos_searcha)
    EX   DE, HL
    ; skip if file deleted
    LD   A, (DE)
    CP   FILE_DELETED
    JP   Z, .search_next

    PUSH DE
    CALL bdos_dir_is_free
    POP  DE
    JP   NC, .not_found                             ; Is full, no more file entries

.search_next:
    CALL bdos_getdptra                              ; get address of FCB
    LD   A, (CPM_VARS.bdos_searchl)                 ; BC=length to compare
    LD   C, A
    LD   B, 0

    ; compare loop
.search_loop:
    LD   A, C
    OR   A
    JP   Z, .search_end                             ; search completed?
    ; check byte
    LD   A, (DE)
    CP   '?'
    JP   Z, .search_ok                              ; match if wildcard
    LD   A, B
    CP   13                                         ; ignore 13th byte
    JP   Z, .search_ok
    CP   FCB_EXT                                    ; extent byte
    LD   A, (DE)
    JP   Z, .search_ext
    SUB  (HL)                                       ; compare bytes
    AND  0x7f                                       ; mask 7th bit
    JP   NZ, bdos_search_nxt                        ; if not match, check next file entry
    JP   .search_ok

    ; compare ext
.search_ext:
    PUSH BC
    LD   C, (HL)
    CALL bdos_compext
    POP  BC
    JP   NZ, bdos_search_nxt                        ; if not match, check next file entry

    ; current bytes matches, increment pointers, decrement counter
.search_ok:
    INC  DE
    INC  HL
    INC  B
    DEC  C
    JP   .search_loop                               ; compare next byte

    ; entiry name matches, return dir position
.search_end:
    LD   A, (CPM_VARS.bdos_dcnt)
    AND  0x3
    LD   (CPM_VARS.bdos_aret), A
    LD   HL, CPM_VARS.bdos_dirloc
    LD   A, (HL)
    RLA
    RET  NC
    XOR  A
    LD   (HL), A
    RET

    ; end of directory, or empty name
.not_found:
    CALL bdos_set_end_dir
    LD   A, 0xff
    JP   bdos_ret_a

; -------------------------------------------------------
; Delete the currently addressed file
; -------------------------------------------------------
bdos_era_file:
    CALL bdos_check_write                           ; check write rotection
    LD   C, 12                                      ; length of filename
    CALL bdos_search                                ; search file in directory (with wildcards)
.del_next:
    CALL bdos_end_of_dir
    RET  Z                                          ; no more dir entries - exit
    CALL bdos_check_rodir                           ; check file RO
    CALL bdos_getdptra                              ; get file info
    LD   (HL), FILE_DELETED                         ; set deleted marker at first symbol
    LD   C, 0
    CALL bdos_scandm                                ; clear space at map
    CALL bdos_wrdir                                 ; write directory to disk
    CALL bdos_search_nxt                            ; find next file
    JP   .del_next

; -------------------------------------------------------
; Given allocation vector position BC, find the zero bit
; closest to this position by searching left and right.
; if found, set the bit to one and return the bit position
; in hl.  if not found (i.e., we pass 0 on the left, or
; maxall on the right), return 0000 in hl
; -------------------------------------------------------
bdos_get_block:
    LD    D, B
    LD    E, C
.prev_test:
    LD    A, C
    OR    B
    JP    Z, .next_test                             ; jump if block 0 specified
    ; check previous block
    DEC   BC
    PUSH  DE
    PUSH  BC
    CALL  bdos_getallocbit
    RRA
    JP    NC, .ret_block                            ; block is empty, use it

    ; not empty, check more
    POP   BC
    POP   DE

    ; look at next block
.next_test:
    ; check for free space on disk
    LD    HL, (CPM_VARS.bdos_maxall)
    LD    A, E
    SUB   L
    LD    A, D
    SBC   A, H
    JP    NC, .ret_no_block
    ; have space,  move to next block
    INC   DE
    PUSH  BC
    PUSH  DE
    LD    B, D
    LD    C, E
    CALL  bdos_getallocbit
    RRA
    JP    NC, .ret_block                            ; block is empty, use it
    POP   DE
    POP   BC
    JP    .prev_test

    ; mark block as used and return in HL
.ret_block:
    RLA
    INC   A
    CALL  bdos_sab_rotr
    POP   HL
    POP   DE
    RET

    ; no free blocks found
.ret_no_block:
    LD    A, C
    OR    B
    JP    NZ, .prev_test                            ; if BC != 0 try to find before
    LD    HL, 0x00                                  ; not found
    RET

; -------------------------------------------------------
; Copy the entire file control block
; -------------------------------------------------------
bdos_copy_fcb:
    LD    C, 0x00
    LD    E, FCB_LEN
; -------------------------------------------------------
; copy fcb information starting at C for E bytes
; into the currently addressed directory entry
; -------------------------------------------------------
dbos_copy_dir:
    PUSH  DE
    LD    B, 0x00
    LD    HL, (CPM_VARS.bdos_info)
    ADD   HL, BC
    EX    DE, HL
    CALL  bdos_getdptra
    POP   BC
    CALL  bdos_move_dehl

; -------------------------------------------------------
; Enter from close to seek and copy current element
; -------------------------------------------------------
bdos_seek_copy:
    CALL  bdos_seekdir
    JP    bdos_wrdir

; -------------------------------------------------------
; Rename the file described by the first half of
; the currently addressed FCB to new name, contained in
; the last half of the currently addressed FCB.
; -------------------------------------------------------
bdos_rename:
    CALL  bdos_check_write                          ; check for RO disk
    LD    C, 12                                     ; use 12 symbols of name to compare
    CALL  bdos_search                               ; find first file
    LD    HL, (CPM_VARS.bdos_info)                  ; file info
    LD    A, (HL)                                   ; user number
    LD    DE, 16                                    ; shift to place second file info
    ADD   HL, DE
    LD    (HL), A                                   ; set same user no

.ren_next:
    CALL  bdos_end_of_dir
    RET   Z                                         ; return if end of directory

    CALL  bdos_check_rodir                          ; check file RO flag
    LD    C, 16                                     ; start from 16th byte
    LD    E, FN_LEN                                 ; and copy 12 byte
    CALL  dbos_copy_dir
    CALL  bdos_search_nxt                           ; search next file
    JP    .ren_next

; -------------------------------------------------------
; Update file attributes for current fcb
; -------------------------------------------------------
bdos_update_attrs:
    LD    C, FN_LEN
    CALL  bdos_search                               ; search file by 12 bytes of name
.set_next:
    CALL  bdos_end_of_dir
    RET   Z                                         ; return if not found

    LD    C, 0
    LD    E, FN_LEN
    CALL  dbos_copy_dir                             ; copy name to FCB and save dir
    CALL  bdos_search_nxt
    JP    .set_next                                 ; ; do it for next file

; --------------------------------------------------
; Open file, name specified in FCB
; -------------------------------------------------------
open:
    LD   C, FCB_INFO_LEN
    CALL bdos_search                                ; search file
    CALL bdos_end_of_dir
    RET  Z                                          ; return if not found

bdos_open_copy:
    CALL bdos_getexta
    LD   A, (HL)                                    ; get extent byte
    PUSH AF                                         ; save ext
    PUSH HL                                         ; and it's address
    CALL bdos_getdptra
    EX   DE, HL
    ; move to user space
    LD   HL, (CPM_VARS.bdos_info)
    LD   C, 32
    PUSH DE
    CALL bdos_move_dehl
    CALL bdos_set_fwf                                ; set 7th bit s2 "unmodified" flag

    POP  DE
    LD   HL, FCB_EXT
    ADD  HL, DE
    LD   C, (HL)                                    ; C = extent byte

    LD   HL, FCB_RC
    ADD  HL, DE
    LD   B, (HL)                                    ; B = record count

    POP  HL
    POP  AF
    LD   (HL), A
    LD   A, C
    CP   (HL)                                       ; cmp extent bytes
    LD   A, B
    JP   Z, .set_rec_cnt
    ; user specified extent is not same, reset record count to 0
    LD   A, 0
    JP   C, .set_rec_cnt
    ; set to maximum
    LD   A, 128

.set_rec_cnt:
    ; set record count in user FCB to A
    LD   HL, (CPM_VARS.bdos_info)
    LD   DE, FCB_RC
    ADD  HL, DE
    LD   (HL), A
    RET

; --------------------------------------------------
; HL = .fcb1(i), DE = .fcb2(i),
; if fcb1(i) = 0 then fcb1(i) := fcb2(i)
; --------------------------------------------------
bdos_mergezero:
    LD   A, (HL)
    INC  HL
    OR   (HL)
    DEC  HL
    RET  NZ
    LD   A, (DE)
    LD   (HL), A
    INC  DE
    INC  HL
    LD   A, (DE)
    LD   (HL), A
    DEC  DE
    DEC  HL
    RET

; --------------------------------------------------
; Close file specified by FCB
; --------------------------------------------------
bdos_close:
    XOR  A
    ; clear status and file position bytes
    LD   (CPM_VARS.bdos_aret), A
    LD   (CPM_VARS.bdos_dcnt), A
    LD   (CPM_VARS.bdos_dcnt+1), A
    CALL bdos_nowrite                               ; get write protection
    RET  NZ                                         ; return if set

    CALL bdos_get_s2
    AND  FWF_MASK
    RET  NZ                                         ; return if not modified flag set
    ; search file
    LD   C, FCB_INFO_LEN
    CALL bdos_search
    CALL bdos_end_of_dir
    RET  Z                                          ; return if not found

    LD   BC, DSK_MAP                                ; offset of records used
    CALL bdos_getdptra
    ADD  HL, BC
    EX   DE, HL

    LD   HL, (CPM_VARS.bdos_info)                   ; same for user FCB
    ADD  HL, BC
    LD   C, 16                                      ; bytes in extent

bdos_merge0:
    LD   A, (CPM_VARS.bdos_small_disk)              ;  small/big disk flag
    OR   A
    JP   Z, bdos_merge                              ; jump if big (16 bit)
    ; small disk (8 bit)
    LD   A, (HL)                                    ; from user FCB
    OR   A
    LD   A, (DE)                                    ; from DIR FCB
    JP   NZ, bdm_fcbnzero
    LD   (HL), A                                    ; user is 0, set from directory fcb

bdm_fcbnzero:
    OR   A
    JP   NZ, bdm_buffnzero
    ; dir is 0, set from user fcb
    LD   A, (HL)
    LD   (DE), A

bdm_buffnzero:
    CP   (HL)
    JP   NZ, bdm_mergerr                            ; if both non zero, close error
    JP   bdm_dmset                                  ; merged ok, go to next fcb byte

bdos_merge:
    CALL bdos_mergezero                             ; update user fcb if it is zero
    EX   DE, HL
    CALL bdos_mergezero                             ; update dir fcb if it is zero
    EX   DE, HL
    LD   A, (DE)
    CP   (HL)
    JP   NZ, bdm_mergerr                            ; if both is same, close error

    ; next byte
    INC  DE
    INC  HL
    LD   A, (DE)
    CP   (HL)
    JP   NZ, bdm_mergerr                            ; if both is same, close error
    DEC  C

bdm_dmset:
    ; next
    INC  DE
    INC  HL
    DEC  C
    JP   NZ, bdos_merge0                            ; merge next if C>0

    LD   BC, 0xffec                                 ; -(fcblen-extnum) (-20)
    ADD  HL, BC
    EX   DE, HL
    ADD  HL, BC
    LD   A, (DE)
    CP   (HL)
    JP   C, bdm_endmerge                            ; directory extent > user extent -> end
    ; update record count in dir FCB
    LD   (HL), A
    LD   BC, 0x3                                    ; (reccnt-extnum)
    ADD  HL, BC
    EX   DE, HL
    ADD  HL, BC
    LD   A, (HL)                                    ; get from user FCB
    LD   (DE), A                                    ; set to directory FCB

bdm_endmerge:
    LD   A, 0xff
    LD   (CPM_VARS.bdos_fcb_closed), A              ; set was open and closed flag
    JP   bdos_seek_copy                             ; update directory

    ; set return status and return
bdm_mergerr:
    LD   HL, CPM_VARS.bdos_aret
    DEC  (HL)
    RET

; -------------------------------------------------------
; Create a new file by creating a directory entry
; then opening the file
; -------------------------------------------------------
bdos_make:
    CALL bdos_check_write                               ; check Write Protection
    LD   HL, (CPM_VARS.bdos_info)                       ; save user FCB
    PUSH HL                                             ; on stack
    LD   HL, CPM_VARS.bdos_efcb                         ; empty FCB
    ; Save FCB address, look for 0xE5
    LD   (CPM_VARS.bdos_info), HL                       ; set empty FCB
    ; search firs empty slot in directory
    LD   C, 0x1                                         ; compare one byte 0xE5
    CALL bdos_search
    CALL bdos_end_of_dir                                ; found flag
    POP  HL
    LD   (CPM_VARS.bdos_info), HL                       ; restore user FCB address
    RET  Z                                              ; return if not found (no space in dir)
    EX   DE, HL
    LD   HL, FCB_RC                                    ; number or record for this file
    ADD  HL, DE

    ; clear FCB tail
    LD   C, 17                                          ; fcblen-namlen
    XOR  A
.fcb_set_0:
    LD   (HL), A
    INC  HL
    DEC  C
    JP   NZ, .fcb_set_0

    LD   HL, FCB_S1
    ADD  HL, DE
    LD   (HL), A                                        ; Current record within extent?
    CALL bdos_setcdr
    CALL bdos_copy_fcb
    JP   bdos_set_fwf

; -------------------------------------------------------
; Close the current extent, and open the next one to read
; if possible.  RMF is true if in read mod
; -------------------------------------------------------
bdos_open_reel:
    XOR  A
    LD   (CPM_VARS.bdos_fcb_closed), A                  ; clear close flag
    CALL bdos_close                                     ; close extent
    CALL bdos_end_of_dir                                ; check space
    RET  Z                                              ; ret if no more space
    ; get extent byte from user FCB
    LD   HL, (CPM_VARS.bdos_info)
    LD   BC, FCB_EXT
    ADD  HL, BC
    LD   A, (HL)
    ; and increment it, mask to 0..31 and store
    INC  A
    AND  MAX_EXT
    LD   (HL), A
    JP   Z, .overflow
    ; mask extent byte
    LD   B, A
    LD   A, (CPM_VARS.bdos_extmsk)
    AND  B
    LD   HL, CPM_VARS.bdos_fcb_closed
    AND  (HL)
    JP   Z, .read_nxt_extent                            ; read netx extent
    JP   .extent_in_mem

.overflow:
    LD   BC, 0x2                                        ; S2
    ADD  HL, BC
    INC  (HL)
    LD   A, (HL)
    AND  MAX_MOD
    JP   Z, .error

.read_nxt_extent:
    LD   C, FCB_INFO_LEN
    CALL bdos_search
    CALL bdos_end_of_dir
    JP   NZ, .extent_in_mem                             ; jump if success
    ; not extent found
    LD   A, (CPM_VARS.bdos_rfm)
    INC  A
    JP   Z, .error                                      ; can not get extent
    CALL bdos_make                                      ; make new empty dir entry for extent
    CALL bdos_end_of_dir                                ; chk no space
    JP   Z, .error                                      ; jmp to error if no space in directory
    JP   .almost_done
.extent_in_mem:
    ; open extent
    CALL bdos_open_copy
.almost_done:
    CALL bdos_stor_fcb_var                              ; move updated data to new extent
    XOR  A                                              ; clear error flag
    JP   bdos_ret_a
.error:
    CALL set_ioerr_1
    JP   bdos_set_fwf                                    ; clear bit 7 in s2

; -------------------------------------------------------
; Sequential disk read operation
; -------------------------------------------------------
bdos_seq_disk_read:
    LD   A, 0x1
    LD   (CPM_VARS.bdos_seqio), A
    ; drop through to diskread

bdos_disk_read:
    LD   A, TRUE
    LD   (CPM_VARS.bdos_rfm), A                         ; dont allow read unwritten data
    CALL bdos_stor_fcb_var
    LD   A, (CPM_VARS.bdos_vrecord)                     ; next record to read
    LD   HL, CPM_VARS.bdos_rcount                       ; number of records in extent
    ; in this extent?
    CP   (HL)
    JP   C, .recordok
    ; no, in next extent, check this exctent fully used
    CP   128
    JP   NZ, .error_opn
    ;open next extent
    CALL bdos_open_reel
    XOR  A
    ; reset record number to read
    LD   (CPM_VARS.bdos_vrecord), A
    ; check open status
    LD   A, (CPM_VARS.bdos_aret)
    OR   A
    JP   NZ, .error_opn
.recordok:
    ; compute block number to read
    CALL bdos_index
    ; check if it in bounds
    CALL bdos_allocated
    JP   Z, .error_opn

    CALL bdos_atran                                 ; convert to logical sector
    CALL bdos_seek                                  ; seec track and sector
    CALL bdos_rdbuff                                ; read sector
    JP   bdos_set_nxt_rec

.error_opn:
    JP   set_ioerr_1

; -------------------------------------------------------
; Sequential write disk
; -------------------------------------------------------
bdos_seq_disk_write:
    LD   A, 0x1
    LD   (CPM_VARS.bdos_seqio), A
bdos_disk_write:
    LD   A, FALSE
    LD   (CPM_VARS.bdos_rfm), A                     ; allow open new extent
    CALL bdos_check_write                           ; check write protection
    LD   HL, (CPM_VARS.bdos_info)                   ; HL -> FCB
    CALL bdos_check_rofile                          ; check RO file
    CALL bdos_stor_fcb_var
    LD   A, (CPM_VARS.bdos_vrecord)                 ; get record number to write to
    CP   128                                        ; lstrec+1
    JP   NC, set_ioerr_1                            ; not in range - error
    CALL bdos_index                                 ; compute block number
    CALL bdos_allocated                             ; check number
    LD   C, 0
    JP   NZ, .disk_wr1
    CALL bdos_dm_position                           ; get next block number within FCB
    LD   (CPM_VARS.bdos_dminx), A                   ; and store it
    LD   BC, 0x00                                   ; start looking for space from start
    OR   A
    JP   Z, .nop_block                            ; zero - not allocated
    ; extract previous blk from fcb
    LD   C, A
    DEC  BC
    CALL bdos_getdm
    LD   B, H
    LD   C, L
.nop_block:
    CALL bdos_get_block                             ; get next free nearest block
    LD   A, L
    OR   H
    JP   NZ, .block_ok
    ; error, no more space
    LD   A, 2
    JP   bdos_ret_a
.block_ok:
    LD   (CPM_VARS.bdos_arecord), HL                ; set record to access
    EX   DE, HL                                     ; to DE
    LD   HL, (CPM_VARS.bdos_info)                   ;
    LD   BC, FCB_AL                                 ; 16
    ADD  HL, BC
    ; small/big disk
    LD   A, (CPM_VARS.bdos_small_disk)
    OR   A
    LD   A, (CPM_VARS.bdos_dminx)                   ; rel block
    JP   Z, .alloc_big                              ; jump for 16bit
    CALL bdos_hl_add_a                              ; HL = HL + A
    LD   (HL), E                                    ; save record to access
    JP   .disk_wru

.alloc_big:
    ; save record to acces 16bit
    LD   C, A
    LD   B, 0
    ADD  HL, BC
    ADD  HL, BC
    LD   (HL), E
    INC  HL
    LD   (HL), D

    ; Disk write to previously unallocated block
.disk_wru:
    LD   C, 2                                       ; C=2 - write to unused disk space
.disk_wr1:
    LD   A, (CPM_VARS.bdos_aret)                    ; check status
    OR   A
    RET  NZ                                         ; return on error

    PUSH BC                                         ; store C write flag for BIOS
    CALL bdos_atran                                 ; convert block number to logical sector
    LD   A, (CPM_VARS.bdos_seqio)                   ; get access mode (0=random, 1=sequential, 2=special)
    DEC  A
    DEC  A
    JP   NZ, .normal_wr
    ; special
    POP  BC
    PUSH BC
    LD   A, C                                       ; A = write status flag (2=write unused space)
    DEC  A
    DEC  A
    JP   NZ, .normal_wr
    ; fill buffer with zeroes
    PUSH HL
    LD   HL, (CPM_VARS.bdos_buffa)
    LD   D, A
.fill0:
    LD   (HL), A
    INC  HL
    INC  D
    JP   P, .fill0
    CALL bdos_setdir                                ; Tell BIOS buffer address to directory access
    LD   HL, (CPM_VARS.bdos_arecord1)               ; get sector that starts current block
    LD   C, 2                                       ; write status flag (2=write unused space)
.fill1:
    LD   (CPM_VARS.bdos_arecord), HL                ; set sector to write
    PUSH BC
    CALL bdos_seek                                  ; seek track and sector number
    POP  BC
    CALL bdos_wrbuff                                ; write zeroes
    LD   HL, (CPM_VARS.bdos_arecord)                ; get sector number
    LD   C, 0                                       ; normal write flag
    LD   A, (CPM_VARS.bdos_blmsk)                   ; block mask
    ; write entire block?
    LD   B, A
    AND  L
    CP   B
    INC  HL
    JP   NZ, .fill1                                 ; continue until (BLKMSK+1) sectors written
    ; reset sector number
    POP  HL
    LD   (CPM_VARS.bdos_arecord), HL
    CALL bdos_setdata                               ; reset DMA address

    ; Normal write.
.normal_wr:
    CALL bdos_seek                                  ; Set track and sector
    POP  BC                                         ; restore C - write status flag
    PUSH BC
    CALL bdos_wrbuff                                ; write
    POP  BC
    LD   A, (CPM_VARS.bdos_vrecord)                 ; last file record
    LD   HL, CPM_VARS.bdos_rcount                   ; last written record
    CP   (HL)
    JP   C, .disk_wr2
    ; update record count
    LD   (HL), A
    INC  (HL)
    LD   C, 2
.disk_wr2:
    DEC  C                                          ; patch: NOP
    DEC  C                                          ; patch: NOP
    JP   NZ, .no_update                             ; patch: LD HL,0
    PUSH AF
    CALL bdos_get_s2                             ; set 'extent written to' flag
    AND  0x7f                                       ; clear 7th bit
    LD   (HL), A
    POP  AF                                         ; record count

.no_update:
    ; it is full
    CP   127
    JP   NZ, .set_nxt_rec
    ; sequential mode?
    LD   A, (CPM_VARS.bdos_seqio)
    CP   1
    JP   NZ, .set_nxt_rec

    CALL bdos_set_nxt_rec                           ; set next record
    CALL bdos_open_reel                             ; get space in dir
    LD   HL, CPM_VARS.bdos_aret                     ; check status
    LD   A, (HL)
    OR   A
    JP   NZ, .no_space                              ; no more space

    DEC  A
    LD   (CPM_VARS.bdos_vrecord), A                 ; -1

.no_space:
    LD   (HL), 0x00                                 ; clear status

.set_nxt_rec:
    JP   bdos_set_nxt_rec

; -------------------------------------------------------
; Random access seek operation, C=0ffh if read mode
; FCB is assumed to address an active file control block
; (modnum has been set to 1100$0000b if previous bad seek)
; -------------------------------------------------------
bdos_rseek:
    XOR  A
    LD   (CPM_VARS.bdos_seqio), A                   ; set random mode

bdos_rseek1:
    PUSH BC
    LD   HL, (CPM_VARS.bdos_info)
    EX   DE, HL
    LD   HL, FCB_RN                                 ; Random access record number
    ADD  HL, DE
    LD   A, (HL)
    AND  7Fh                                        ; [6:0] bits record number to access
    PUSH AF
    ; get bit 7
    LD   A, (HL)
    RLA
    INC  HL
    ; get high byte [3:0] bits
    LD   A, (HL)
    RLA
    AND  00011111b
    ; get extent byte
    LD   C, A
    LD   A, (HL)
    RRA
    RRA
    RRA
    RRA
    AND  0x0f
    LD   B, A
    POP  AF
    INC  HL
    ; get next byte
    LD   L, (HL)
    ; check overflow
    INC  L
    DEC  L
    LD   L, 6
    JP   NZ, .seek_err                              ; error 6 if overflow

    ; save current record in FCB
    LD   HL, FCB_CR
    ADD  HL, DE
    LD   (HL), A

    ; check extent byte
    LD   HL, FCB_EXT
    ADD  HL, DE
    LD   A, C
    SUB  (HL)
    JP   NZ, .close                                 ; not same as previous
    ; check extra s2 byte
    LD   HL, FCB_S2
    ADD  HL, DE
    LD   A, B
    SUB  (HL)
    AND  0x7f
    JP   Z, .seek_ok                                ; same, is ok

.close:
    PUSH BC
    PUSH DE
    CALL bdos_close                                 ; close current extent
    POP  DE
    POP  BC
    LD   L, 3                                       ; Cannot close error #3
    LD   A, (CPM_VARS.bdos_aret)                    ; check status
    INC  A
    JP   Z, .bad_seek                               ; status != 0 - error
    ; set extent byte to FCB
    LD   HL, FCB_EXT
    ADD  HL, DE
    LD   (HL), C
    ; set S2 - extra extent byte
    LD   HL, FCB_S2
    ADD  HL, DE
    LD   (HL), B

    CALL open
    LD   A, (CPM_VARS.bdos_aret)
    INC  A
    JP   NZ, .seek_ok
    POP  BC
    PUSH BC
    LD   L, 0x4                                         ; Seek to unwritten extent #4
    INC  C
    JP   Z, .bad_seek
    CALL bdos_make
    LD   L, 0x5                                         ; Cannot create new extent #5
    LD   A, (CPM_VARS.bdos_aret)
    INC  A
    JP   Z, .bad_seek
.seek_ok:
    POP  BC
    XOR  A
    JP   bdos_ret_a
.bad_seek:
    PUSH HL
    CALL bdos_get_s2
    LD   (HL), 11000000b
    POP  HL
.seek_err:
    POP  BC
    LD   A,L
    LD   (CPM_VARS.bdos_aret), A
    JP   bdos_set_fwf

; -------------------------------------------------------
; Random disk read operation
; -------------------------------------------------------
bdos_rand_disk_read:
    LD   C, 0xff                                    ; mode
    CALL bdos_rseek                                 ; positioning
    CALL Z, bdos_disk_read                          ; read if ok
    RET

; -------------------------------------------------------
; Random disk write operation
; -------------------------------------------------------
bdos_rand_disk_write:
    LD   C, 0                                       ; mode
    CALL bdos_rseek                                 ; positioning
    CALL Z, bdos_disk_write                         ; read if ok
    RET

; -------------------------------------------------------
; Compute random record position
; Inp: HL -> FCB
;      DE - relative location of record number
; Out: C - r0 byte
;      B - r1 byte
;      A - r2 byte
;      ZF set - Ok, reset - overflow
; -------------------------------------------------------
bdos_compute_rr:
    EX   DE, HL                                     ; DE -> FCB
    ADD  HL, DE                                     ; HL = FCB+DE
    LD   C, (HL)                                    ; get record number
    LD   B, 0                                       ; in BC
    LD   HL, FCB_EXT
    ADD  HL, DE
    LD   A, (HL)                                    ; A = extent
    ; calculate BC = recnum + extent*128
    RRCA                                            ; A[0] -> A[7]
    AND  0x80                                       ; ignore other bits
    ADD  A, C                                       ; add to record number
    LD   C, A                                       ; C=r0
    LD   A, 0
    ADC  A, B
    LD   B, A                                       ; B=r1
    ;
    LD   A, (HL)                                    ; A = extent
    RRCA
    AND  0x0f                                       ; A = EXT[4:1]
    ADD  A, B                                       ; add to r1 byte
    LD   B, A

    LD   HL, FCB_S2
    ADD  HL, DE
    LD   A, (HL)                                    ; A = extra extent bits
    ADD  A, A                                       ; *2
    ADD  A, A                                       ; *4
    ADD  A, A                                       ; *8
    ADD  A, A                                       ; *16
    PUSH AF                                         ; save flags (need only CF)
    ADD  A, B                                       ; add to r1
    LD   B, A
    PUSH AF                                         ; save flags
    POP  HL                                         ; flags on L
    LD   A, L                                       ; A bit 0 - CF
    POP  HL
    OR   L                                          ; merge both carry flags
    AND  0x1                                        ; mask only CF
    RET

; -------------------------------------------------------
; Compute logical file size for current FCB.
; Setup r0, r1, r2 bytes - maximum record number
; -------------------------------------------------------
bdos_get_file_size:
    LD   C, FN_LEN
    CALL bdos_search                                ; get first dir record (first extent)
    LD   HL, (CPM_VARS.bdos_info)                   ; HL -> FCB
    ; zeroing r0, r1, r2
    LD   DE, FCB_RN                                 ; D=0
    ADD  HL, DE
    PUSH HL
    LD   (HL), D
    INC  HL
    LD   (HL), D
    INC  HL
    LD   (HL), D
    ;
.get_extent:
    CALL bdos_end_of_dir
    JP   Z, .done                                   ; if not more extents found, return
    CALL bdos_getdptra                              ; HL -> FCB
    LD   DE, FCB_RC
    CALL bdos_compute_rr                            ; compute random access parameters
    POP  HL
    PUSH HL
    ; Now let's compare these values ​​with those indicated
    LD   E, A
    LD   A, C
    SUB  (HL)
    INC  HL
    LD   A, B
    SBC  A, (HL)
    INC  HL
    LD   A, E
    SBC  A, (HL)
    JP   C, .less_size
    ; found larger extent (size), save it to fcb
    LD   (HL), E
    DEC  HL
    LD   (HL), B
    DEC  HL
    LD   (HL), C
.less_size:
    CALL bdos_search_nxt
    JP   .get_extent
.done:
    POP  HL
    RET

; -------------------------------------------------------
; (F_RANDREC) - Update random access pointer
; Set the random record count bytes of the FCB to the number
; of the last record read/written by the sequential I/O calls.
; -------------------------------------------------------
bdos_set_random:
    LD   HL, (CPM_VARS.bdos_info)                   ; HL -> FCB
    LD   DE, FCB_CR                                 ; Current record within extent
    CALL bdos_compute_rr                            ; Compute random access parameters
    LD   HL, FCB_RN
    ADD  HL, DE                                     ; HL -> Random access record number
    ; set r0, r1, r2
    LD   (HL), C
    INC  HL
    LD   (HL), B
    INC  HL
    LD   (HL), A
    RET

; -------------------------------------------------------
; Select disk to bdos_curdsk for subsequent input or
; output ops
; -------------------------------------------------------
bdos_select:
    LD   HL, (CPM_VARS.bdos_dlog)                   ; login vector
    LD   A, (CPM_VARS.bdos_curdsk)                  ; current drive
    LD   C, A
    CALL bdos_hlrotr                                ; shift active bit for this drive
    PUSH HL
    EX   DE, HL
    CALL bdos_sel_dsk                               ; select drive
    POP  HL
    CALL Z, bdos_sel_error                          ; check for error drive
    ; new active drive?
    LD   A, L
    RRA
    RET  C                                          ; no, return
    ; yes, update login vector
    LD   HL, (CPM_VARS.bdos_dlog)
    LD   C, L
    LD   B, H
    CALL bdos_set_dsk_bit                           ; set bits in vector
    LD   (CPM_VARS.bdos_dlog), HL                   ; store new vector

    JP   bdos_initialize

; -------------------------------------------------------
; Select disc
; Inp: C=0x0E
;      E=drive number 0-A, 1-B .. 15-P
; Out: L=A=0 - ok or 0xFF - error
; -------------------------------------------------------
bdos_select_disk:
    ; check disk change
    LD   A, (CPM_VARS.bdos_linfo)
    LD   HL, CPM_VARS.bdos_curdsk
    CP   (HL)
    RET  Z                                          ; is same, return
    ; login new disk
    LD   (HL), A
    JP   bdos_select

; -------------------------------------------------------
; Auto select disk by FCB_DR
; -------------------------------------------------------
bdos_reselect:
    LD   A, TRUE
    LD   (CPM_VARS.bdos_resel), A                   ; set flag
    LD   HL, (CPM_VARS.bdos_info)                   ; HL -> FCB
    LD   A, (HL)                                    ; get specified disk
    AND  0x1f                                       ; A = A[4:0]
    DEC  A                                          ; adjust for 0-A, 1-B and so on
    LD   (CPM_VARS.bdos_linfo), A                   ; set as parameter
    CP   0x1e                                       ; no change?
    JP   NC, .no_select
    ; change, but save current to old
    LD   A, (CPM_VARS.bdos_curdsk)
    LD   (CPM_VARS.bdos_olddsk), A
    ; save FCB_DR byte
    LD   A, (HL)
    LD   (CPM_VARS.bdos_fcbdsk), A
    ; clear FCB_DR[4:0] bits - user code
    AND  11100000b
    LD   (HL), A
    CALL bdos_select_disk

.no_select:
    ; set user to FCB_DR low bits
    LD   A, (CPM_VARS.bdos_userno)
    LD   HL, (CPM_VARS.bdos_info)
    OR   (HL)
    LD   (HL), A
    RET

; -------------------------------------------------------
; Return version number
; -------------------------------------------------------
bdos_get_version:
    LD   A, CPM_VERSION                                   ; 0x22 - v2.2
    JP   bdos_ret_a

; -------------------------------------------------------
; Reset disk system - initialize to disk 0
; -------------------------------------------------------
bdos_reset_disks:
    LD   HL, 0x00
    LD   (CPM_VARS.bdos_rodsk), HL                      ; clear ro flags
    LD   (CPM_VARS.bdos_dlog), HL                       ; clear login vector
    XOR  A                                              ; disk 0
    LD   (CPM_VARS.bdos_curdsk), A                      ; set disk 'A' as current
    LD   HL, dma_buffer                                 ; set 'DMA' buffer address to default
    LD   (CPM_VARS.bdos_dmaad), HL
    CALL bdos_setdata
    JP   bdos_select                                    ; select drive 'A'

; -------------------------------------------------------
; Open file
; Inp: DE -> FCB
; Out: BA and HL - error.
; -------------------------------------------------------
bdos_open_file:
    CALL bdos_clear_s2
    CALL bdos_reselect
    JP   open

; -------------------------------------------------------
; Close file
; Inp: DE -> FCB
; Out: BA and HL - error.
; -------------------------------------------------------
bdos_close_file:
    CALL bdos_reselect
    JP   bdos_close

; -------------------------------------------------------
; Search for first occurrence of a file in directory
; Inp: DE -> FCB
; Out: BA and HL - error.
; -------------------------------------------------------
bdos_search_first:
    LD   C, 0x00                                    ; special search
    EX   DE, HL
    LD   A, (HL)
    CP   '?'                                        ; '?' is first byte?
    JP   Z, .qselect                                ; return first matched entry

    CALL bdos_getexta                               ; get extent byte from FCB
    LD   A, (HL)
    CP   '?'                                        ; it is '?'
    CALL NZ, bdos_clear_s2                          ; set S2 to 0 if not
    CALL bdos_reselect                              ; select disk by FCB
    LD   C, FCB_INFO_LEN                            ; match 1first 15 bytes
.qselect:
    CALL bdos_search                                ; search first entry
    JP   bdos_dir_to_user                           ; move entry to user space

; -------------------------------------------------------
; Search for next occurrence of a file name
; Out: BA and HL - error.
; -------------------------------------------------------
bdos_search_next:
    LD   HL, (CPM_VARS.bdos_searcha)                ; restore FCB address
    LD   (CPM_VARS.bdos_info), HL                   ; set as parameter
    CALL bdos_reselect                              ; select drive by FCB
    CALL bdos_search_nxt                            ; search next
    JP   bdos_dir_to_user                           ; move entry to user space

; -------------------------------------------------------
; Remove directory
; Inp: DE -> FCB
; Out: BA and HL - error.
; -------------------------------------------------------
bdos_rm_file:
    CALL bdos_reselect                              ; Select drive by FCB
    CALL bdos_era_file                              ; Delete file
    JP   bdos_ret_dirloc                            ; Return directory location in A

; -------------------------------------------------------
; Read next 128b record
; Inp: DE -> FCB
; Out: BA and HL - error.
; A=0 - Ok,
; 1 - end of file,
; 9 - invalid FCB,
; 10 - media changed,
; 0FFh - hardware error.
; -------------------------------------------------------
bdos_read_file:
    CALL bdos_reselect                              ; select drive
    JP   bdos_seq_disk_read                         ; and read

; -------------------------------------------------------
; Write next 128b record
; Inp: DE -> FCB
; Out: BA and HL - error.
; A=0 - Ok,
; 1 - directory full,
; 2 - disc full,
; 9 - invalid FCB,
; 10 - media changed,
; 0FFh - hardware error.
; -------------------------------------------------------
bdos_write_file:
    CALL bdos_reselect                              ; select drive
    JP   bdos_seq_disk_write                        ; and write

; -------------------------------------------------------
; Create file
; Inp: DE -> FCB.
; Out: Error in BA and HL
; A=0 - Ok,
; 0FFh - directory is full.
; -------------------------------------------------------
bdos_make_file:
    CALL bdos_clear_s2                              ; clear S2 byte
    CALL bdos_reselect                              ; select drive
    JP   bdos_make                                  ; and make file

; -------------------------------------------------------
; Rename file. New name, stored at FCB+16
; Inp: DE -> FCB.
; Out: Error in BA and HL
; A=0-3 if successful;
; A=0FFh if error.
; -------------------------------------------------------
bdos_ren_file:
    CALL bdos_reselect                              ; select drive
    CALL bdos_rename                                ; rename file
    JP   bdos_ret_dirloc                            ; Return directory location in A

; -------------------------------------------------------
; Return bitmap of logged-in drives
; Out: bitmap in HL.
; -------------------------------------------------------
bdos_get_login_vec:
    LD   HL, (CPM_VARS.bdos_dlog)
    JP   sthl_ret

; -------------------------------------------------------
; Return current drive
; Out: A - currently selected drive. 0 => A:, 1 => B: etc.
; -------------------------------------------------------
bdos_get_cur_drive:
    LD   A, (CPM_VARS.bdos_curdsk)
    JP   bdos_ret_a

; -------------------------------------------------------
; Set DMA address
; Inp: DE - address of DMA buffer
; -------------------------------------------------------
bdos_set_dma_addr:
    EX   DE, HL
    LD   (CPM_VARS.bdos_dmaad), HL
    JP   bdos_setdata

; -------------------------------------------------------
; fn27: Return the address of cur disk allocation map
; Out: HL - address
; -------------------------------------------------------
bdos_get_alloc_addr:
    LD   HL, (CPM_VARS.bdos_alloca)
    JP   sthl_ret

; -------------------------------------------------------
; Get write protection status
; -------------------------------------------------------
bdos_get_wr_protect:
    LD   HL, (CPM_VARS.bdos_rodsk)
    JP   sthl_ret

; -------------------------------------------------------
; Set file attributes (ro, system)
; -------------------------------------------------------
bdos_set_attr:
    CALL bdos_reselect
    CALL bdos_update_attrs
    JP   bdos_ret_dirloc

; -------------------------------------------------------
; Return address of disk parameter block of the
; current drive
; -------------------------------------------------------
bdos_get_dpb:
    LD   HL, (CPM_VARS.bdos_dpbaddr)

; -------------------------------------------------------
; Common code to return Value from BDOS functions
; -------------------------------------------------------
sthl_ret:
    LD   (CPM_VARS.bdos_aret), HL
    RET

; -------------------------------------------------------
; Get/set user number
; Inp: E - 0..15 - set user. 0xFF - get user
; Out: A - returns user number
; -------------------------------------------------------
bdos_set_user:
    LD   A, (CPM_VARS.bdos_linfo)
    CP   0xff
    JP   NZ, .set_user
    LD   A, (CPM_VARS.bdos_userno)
    JP   bdos_ret_a
.set_user:
    AND  0x1f                                      ; mask for 0..31  but will be < 16
    LD   (CPM_VARS.bdos_userno), A
    RET

; -------------------------------------------------------
; Random read. Record specified in the random record
; count area of the FCB, at the DMA address
; Inp: DE -> FCB
; Out: Error codes in BA and HL.
; -------------------------------------------------------
bdos_rand_read:
    CALL bdos_reselect                              ; select drive by FCB
    JP   bdos_rand_disk_read                        ; and read

; -------------------------------------------------------
; Random access write record.
; Record specified in the random record count area of the FCB, at the DMA address
; Inp: DE -> FCB
; Out: Error codes in BA and HL.
; -------------------------------------------------------
bdos_rand_write:
    CALL bdos_reselect                              ; select drive by FCB
    JP   bdos_rand_disk_write                       ; and write

; -------------------------------------------------------
; Compute file size.
; Set the random record count bytes of the FCB to the
; number of 128-byte records in the file.
; -------------------------------------------------------
bdos_compute_fs:
    CALL bdos_reselect                              ; select drive by FCB
    JP   bdos_get_file_size                         ; and get file size

; -------------------------------------------------------
; Selectively logoff (reset) disc drives
; Out: A=0 - Ok, 0xff if error
; -------------------------------------------------------
bdos_reset_drives:
    LD   HL, (CPM_VARS.bdos_info)                   ; HL - drives to logoff map
    LD   A, L
    CPL
    LD   E, A
    LD   A, H
    CPL
    LD   HL, (CPM_VARS.bdos_dlog)                   ; get vector
    AND  H                                          ; reset in hi byte
    LD   D, A                                       ; and store in D
    LD   A, L                                       ; reset low byte
    AND  E
    LD   E, A                                       ; and store in E
    LD   HL, (CPM_VARS.bdos_rodsk)                  ; HL - disk RO map
    EX   DE, HL
    LD   (CPM_VARS.bdos_dlog), HL                   ; store new login vector
    ; reset RO flags
    LD   A, L
    AND  E
    LD   L, A
    LD   A, H
    AND  D
    LD   H, A
    ; and store new value
    LD   (CPM_VARS.bdos_rodsk), HL
    RET

; -------------------------------------------------------
; Reach this point at the end of processing
; to return the data to the user.
; -------------------------------------------------------
bdos_goback:
    LD   A, (CPM_VARS.bdos_resel)                   ; check for selection drive by user FCB
    OR   A
    JP   Z, bdos_ret_mon                            ; return if not selected

    LD   HL, (CPM_VARS.bdos_info)                   ; HL -> FCB_DR
    LD   (HL), 0
    LD   A, (CPM_VARS.bdos_fcbdsk)
    OR   A                                          ; restore drive
    JP   Z, bdos_ret_mon                            ; return if default
    LD   (HL), A                                    ; restore
    LD   A, (CPM_VARS.bdos_olddsk)                  ; previous drive
    LD   (CPM_VARS.bdos_linfo), A                   ; set parameter
    CALL bdos_select_disk                           ; select previous drive

; -------------------------------------------------------
; Return from the disk monitor
; -------------------------------------------------------
bdos_ret_mon:
    LD   HL, (CPM_VARS.bdos_usersp)                 ; return user stack
    LD   SP, HL
    LD   HL, (CPM_VARS.bdos_aret)                   ; get return value
    LD   A, L                                       ; ver 1.4 CP/M compatibility
    LD   B, H
    RET

; -------------------------------------------------------
; Random disk write with zero fill of
; unallocated block
; -------------------------------------------------------
bdos_rand_write_z:
    CALL bdos_reselect                              ; select drive
    LD   A, 2                                       ; special write mode
    LD   (CPM_VARS.bdos_seqio), A
    LD   C, FALSE                                   ; write indicator
    CALL bdos_rseek1                                ; seek position in a file
    CALL Z, bdos_disk_write                         ; write if no errors
    RET

; -------------------------------------------------------
; Unuser initialized data
; -------------------------------------------------------
filler:
    DB   0xF1, 0xE1                                 ; POP AF, POP HL

; -------------------------------------------------------
; Filler to align blocks in ROM
; -------------------------------------------------------
LAST      EQU $
CODE_SIZE EQU LAST-0xC800
FILL_SIZE EQU 0xE00-CODE_SIZE

    DISPLAY "| BDOS\t| ",/H,bdos_start,"  | ",/H,CODE_SIZE," | ",/H,FILL_SIZE," |"

    ASSERT bdos_rand_write_z = 0xd55e


FILLER
    DS    FILL_SIZE, 0xFF

    ENDMODULE

    IFNDEF    BUILD_ROM
        OUTEND
    ENDIF
