; ======================================================
; Ocean-240.2
; CP/M V2.2 R8.
; CCP (Console Command Processor) Resident part
; ORG C000 at RPM, moved to B200-BA09
;
; Disassembled by Romych 2026-02-01
; ======================================================

	INCLUDE "ram.inc"

	IFNDEF	BUILD_ROM
		OUTPUT ccp_ram.bin
	ENDIF

	MODULE	CCP_RAM

	ORG	 0xB200

ccp_ram_ent:
    JP	 ccp_ent
    JP	 ccp_clear_buff

ccp_inbuff:
    DB	 0x7F, 0x00

ccp_inp_line:
hex_length:
    DB	 ASCII_SP, ASCII_SP

msg_copyright:
hex_sectors:
    DB   " "
hex_buff:

    DB   "             COPYRIGHT (C) 1979, DIGITAL RESEARCH  ", 0x00
    DS   73, 0x0

ccp_inp_line_addr:
    DW	 ccp_inp_line

cur_name_ptr:
    DW	 0h

; ---------------------------------------------------
; Call BDOS function 2 (C_WRITE) - Console output
; Inp: A - char to output
; ---------------------------------------------------
ccp_print:
    LD	 E, A
    LD	 C, C_WRITE
    JP	 jp_bdos_enter

; ---------------------------------------------------
; Put char to console
; Inp: A - char
; ---------------------------------------------------
ccp_putc:
    PUSH BC
    CALL ccp_print
    POP	 BC
    RET

ccp_out_crlf:
    LD	 A, ASCII_CR
    CALL ccp_putc
    LD	 A, ASCII_LF
    JP	 ccp_putc
ccp_out_space:
    LD	 A,' '
    JP	 ccp_putc

; ---------------------------------------------------
; Out message from new line
; Inp: BC -> Message
; ---------------------------------------------------
ccp_out_crlf_msg:
    PUSH BC
    CALL ccp_out_crlf
    POP	 HL

; ---------------------------------------------------
; Out asciiz message
; Inp: HL -> Message
; ---------------------------------------------------
ccp_out_msg:
    LD	 A, (HL)										;= "READ ERROR"
    OR	 A
    RET	 Z
    INC	 HL
    PUSH HL
    CALL ccp_print
    POP	 HL
    JP	 ccp_out_msg

; ---------------------------------------------------
; Call BDOS function 13 (DRV_ALLRESET) - Reset discs
; ---------------------------------------------------
ccp_bdos_drv_allreset:
    LD	 C, DRV_ALLRESET
    JP	 jp_bdos_enter

; ---------------------------------------------------
; Call BDOS function 14 (DRV_SET) - Select disc
; Inp: A - disk
; ---------------------------------------------------
ccp_bdos_drv_set:
    LD	 E, A
    LD	 C, DRV_SET
    JP	 jp_bdos_enter

; ---------------------------------------------------
; Call BDOS fn and return result
; Inp: C - fn no
; Out: A - error + 1
; ---------------------------------------------------
ccp_call_bdos:
    CALL jp_bdos_enter
    LD	 (ccp_bdos_result_code), A
    INC	 A
    RET

; ---------------------------------------------------
; BDOS function 15 (F_OPEN) - Open file /Dir
; Inp: DE -> FCB
; Out: A=0 for error, or 1-4 for success
; ---------------------------------------------------
ccp_bdos_open_f:
    LD	 C, 15
    JP	 ccp_call_bdos

; ---------------------------------------------------
; Open file by current FCB
; ---------------------------------------------------
ccp_open_cur_fcb:
    XOR	 A
    LD	 (ccp_current_fcb_cr), A                    ; clear current record counter
    LD	 DE, ccp_current_fcb
    JP	 ccp_bdos_open_f

; ---------------------------------------------------
; BDOS function 16 (F_CLOSE) - Close file
; ---------------------------------------------------
ccp_bdos_close_f:
    LD	 C, 16
    JP	 ccp_call_bdos

; ---------------------------------------------------
; Call BDOS function 17 (F_SFIRST) - search for first
; Out: A = 0 in error, 1-4 if success
; ---------------------------------------------------
ccp_bdos_find_first:
    LD	 C, 17
    JP	 ccp_call_bdos

; ---------------------------------------------------
; Call BDOS function 18 (F_SNEXT) - search for next
; Out: A = 0 in error, 1-4 if success
; ---------------------------------------------------
ccp_bdos_find_next:
    LD	 C, 18
    JP	 ccp_call_bdos		                        ; BDOS 18 (F_SNEXT) - search for next ?

; ---------------------------------------------------
; Call BDOS F_FIRST with current FCB
; ---------------------------------------------------
ccp_find_first:
    LD	 DE, ccp_current_fcb
    JP	 ccp_bdos_find_first

; ---------------------------------------------------
; Call BDOS function 19 (F_DELETE) - delete file
; ---------------------------------------------------
ccp_bdos_era_file:
    LD	C, F_DELETE
    JP	jp_bdos_enter

; ---------------------------------------------------
; Call BDOS and set ZF by result
; ---------------------------------------------------
ccp_bdos_enter_zf:
    CALL jp_bdos_enter
    OR	 A
    RET

; ---------------------------------------------------
; Read next 128 bytes of file
; Inp: DE -> FCB
; Out: a = 0 - ok;
; 1 - EOF;
; 9 - invalid FCB;
; 10 - Media changed;
; 0xFF - HW error.
; ---------------------------------------------------
ccp_bdos_read_f:
    LD	 C, F_READ
    JP	 ccp_bdos_enter_zf

; ---------------------------------------------------
; Read file by current FCB
; ---------------------------------------------------
ccp_read_f_fcb:
    LD	 DE, ccp_current_fcb
    JP	 ccp_bdos_read_f

; ---------------------------------------------------
; Call BDOS function 21 (F_WRITE) - write next record
; ---------------------------------------------------
ccp_bdos_f_write:
    LD	 C, F_WRITE
    JP	 ccp_bdos_enter_zf

; ---------------------------------------------------
; Call BDOS function 22 (F_MAKE) - create file
; ---------------------------------------------------
ccp_bdos_make_f:
    LD	 C, F_MAKE
    JP	 ccp_call_bdos

; ---------------------------------------------------
; Call BDOS function 23 (F_RENAME) - Rename file
; ---------------------------------------------------
ccp_bdos_rename_f:
    LD	C, F_RENAME
    JP	jp_bdos_enter

; ---------------------------------------------------
; Call BDOS function 32 (F_USERNUM) for get user number
; Out: A - user no
; ---------------------------------------------------
ccp_bdos_get_user:
    LD	E, 0xff

; ---------------------------------------------------
; Call BDOS function 32 (F_USERNUM) set user number
; Inp: E - user no 0-15, or 0xFF for get user
; Out: A - user no
; ---------------------------------------------------
ccp_bdos_set_user:
    LD	 C, F_USERNUM
    JP	 jp_bdos_enter

; ---------------------------------------------------
; Get user no and store in upper nibble of sys var
; ---------------------------------------------------
ccp_set_cur_drv:
    CALL	ccp_bdos_get_user
    ADD	 A, A
    ADD	 A, A
    ADD	 A, A
    ADD	 A, A                                       ; user no at upper nibble
    LD	 HL, ccp_cur_drive
    OR	 (HL)
    LD	 (cur_user_drv), A
    RET

; ---------------------------------------------------
; Replace new drive in lower nibble of sys var
; reset user no
; Out: A - drive no
; ---------------------------------------------------
ccp_reset_cur_drv:
    LD	 A, (ccp_cur_drive)
    LD	 (cur_user_drv), A
    RET

; ---------------------------------------------------
; Uppercase character a..z
; Inp: A - character
; Out: A - character in upper case
; ---------------------------------------------------
char_to_upper:
    CP	 'a'
    RET	 C
    CP	 '{'
    RET	 NC
    ; character in a..z range
    AND	 0x5f
    RET

; ---------------------------------------------------
; Get user input from console or from batch
; file $$$.SUB
; ---------------------------------------------------
ccp_get_inp:
    LD	 A, (ccp_batch)
    OR	 A
    JP	 Z, ccp_gin_no_batch

    ; select drive A for $$$.SUB
    LD	 A, (ccp_cur_drive)
    OR	 A
    LD	 A, 0x0
    CALL NZ, ccp_bdos_drv_set

    ; open batch file
    LD	 DE, ccp_batch_fcb
    CALL ccp_bdos_open_f
    JP	 Z, ccp_gin_no_batch                        ; go to con inp if file not found

    ; read last record
    LD	 A, (ccp_batch_fcb_rc)
    DEC	 A
    LD	 (ccp_batch_fcb_cr), A
    LD	 DE, ccp_batch_fcb
    CALL ccp_bdos_read_f
    JP	 NZ, ccp_gin_no_batch                       ; stop on EOF

    ; move data from file to buffer
    LD	 DE, ccp_inbuff+1
    LD	 HL, dma_buffer
    LD	 B, DMA_BUFF_SIZE                           ; 0x80
    CALL ccp_mv_hlde_b
    LD	 HL, ccp_batch_fcb_s2
    LD	 (HL), 0x0
    INC	 HL
    DEC	 (HL)                                       ; decriment record count
    ; close batch file
    LD	 DE, ccp_batch_fcb
    CALL ccp_bdos_close_f
    JP	 Z, ccp_gin_no_batch
    ; reselect old drive if not 0
    LD	 A, (ccp_cur_drive)
    OR	 A
    CALL NZ, ccp_bdos_drv_set
    ; print command line
    LD	 HL, ccp_inp_line		                    ; ccp_inbuff+2
    CALL ccp_out_msg
    CALL ccp_getkey_no_wait
    JP	 Z, ccp_gin_nokey
    ; terminate batch processing on any key
    CALL ccp_del_batch
    JP	 ccp_get_command

    ; get user input from keyboard
ccp_gin_no_batch:
    CALL ccp_del_batch
    CALL ccp_set_cur_drv

    LD	 C, C_READSTR
    LD	 DE, ccp_inbuff
    ; Call BDOS C_READSTR DE -> inp buffer
    CALL jp_bdos_enter

    CALL ccp_reset_cur_drv

ccp_gin_nokey:
    LD	 HL, ccp_inbuff+1
    LD	 B, (HL)

ccp_gin_uppr:
    INC	 HL
    LD	 A, B
    OR	 A
    JP	 Z, ccp_gin_uppr_end
    LD	 A, (HL)		                            ;= 2020h
    CALL char_to_upper
    LD	 (HL), A		                            ;= 2020h
    DEC	 B
    JP	 ccp_gin_uppr

ccp_gin_uppr_end:
    LD	 (HL), A                                    ; set last character to 0
    LD	 HL, ccp_inp_line		                    ;
    LD	 (ccp_inp_line_addr), HL		                ;
    RET

; ---------------------------------------------------
; Check keyboard
; Out: A - pressed key code
;      ZF set if no key pressed
; ---------------------------------------------------
ccp_getkey_no_wait:
    LD	 C, C_STAT
	; Call BDOS (C_STAT) - Console status
    CALL jp_bdos_enter
    OR	 A
    RET	 Z				 ; ret if no character waiting
    LD	 C, C_READ
	; Call BDOS (C_READ) - Console input
    CALL jp_bdos_enter
    OR	 A
    RET

; ---------------------------------------------------
; Call BDOS function 25 (DRV_GET) - Return current drive
; Out: A - drive 0-A, 1-B...
; ---------------------------------------------------
ccp_bdos_drv_get:
    LD	 C, DRV_GET
    JP	 jp_bdos_enter

; ---------------------------------------------------
; Set disk buffer address to default buffer
; ---------------------------------------------------
cpp_set_disk_buff_addr:
    LD	 DE, dma_buffer

; ---------------------------------------------------
; Call BDOS function 26 (F_DMAOFF) - Set DMA address
; Inp: DE - address
; ---------------------------------------------------
ccp_bdos_dma_set:
    LD	 C, F_DMAOFF
    JP	 jp_bdos_enter

; ---------------------------------------------------
; Delete batch file created by submit
; ---------------------------------------------------
ccp_del_batch:
    LD	 HL, ccp_batch
    LD	 A, (HL)
    OR	 A
    RET	 Z                                          ; return if no active batch file
    LD	 (HL), 0x0                                  ; mark as inactive
    XOR	 A
    CALL ccp_bdos_drv_set                           ; select drive 0
    LD	 DE, ccp_batch_fcb
    CALL ccp_bdos_era_file
    LD	 A, (ccp_cur_drive)
    JP	 ccp_bdos_drv_set

; --------------------------------------------------
; Check "serial number" of CP/M
; --------------------------------------------------
ccp_verify_pattern:
    LD	 DE, cpm_pattern	                        ; = F9h
    LD	 HL, 0x0000
    LD	 B, 6

ccp_chk_patt_nex:
    LD	 A, (DE)
    CP	 (HL)
    NOP                                             ; JP NZ HALT was here
    NOP
    NOP
    INC	 DE
    INC	 HL
    DEC	 B
    JP	 NZ, ccp_chk_patt_nex
    RET

; --------------------------------------------------
; Print syntax error indicator
; --------------------------------------------------
print_syn_err:
    CALL ccp_out_crlf
    LD	 HL, (cur_name_ptr)
pse_next:
    LD	 A, (HL)
    CP	 ASCII_SP
    JP	 Z, pse_end
    OR	 A
    JP	 Z, pse_end
    PUSH HL

    CALL ccp_print
    POP	 HL
    INC	 HL
    JP	 pse_next
pse_end:
    LD	 A, '?'
    CALL ccp_print
    CALL ccp_out_crlf
    CALL ccp_del_batch
    JP	 ccp_get_command

; --------------------------------------------------
; Check user input characters for legal range
; Inp: [DE] - pointer to character
; --------------------------------------------------
cpp_valid_inp:
    LD	 A, (DE)
    OR	 A
    RET	 Z
    CP	ASCII_SP                                    ; >= Space
    JP	 C, print_syn_err
    RET	 Z
    CP	 '='
    RET	 Z
    CP	 '_'
    RET	 Z
    CP	 '.'
    RET	 Z
    CP	 ':'
    RET	 Z
    CP	 ';'
    RET	 Z
    CP	 '<'
    RET	 Z
    CP	 '>'
    RET	 Z
    RET

; ---------------------------------------------------
; Find non space character until end
; Inp: DE -> current character
; Out: DE -> non space
;       A - character
;       ZF set on EOL
; ---------------------------------------------------
ccp_find_no_space:
    LD	 A, (DE)
    OR	 A
    RET	 Z
    CP	 ASCII_SP
    RET	 NZ
    INC	 DE
    JP	 ccp_find_no_space

; ---------------------------------------------------
; HL=HL+A
; ---------------------------------------------------
sum_hl_a:
    ADD	 A, L
    LD	 L, A
    RET	 NC
    INC	 H                                          ; inc H if CF is set
    RET

; ---------------------------------------------------
; Convert first name to fcb
; ---------------------------------------------------
ccp_cv_first_to_fcb:
    LD	 A, 0x0

; Convert filename from cmd to fcb
; replace '*' to '?'
; Inp: A - offset in fcb filename
; Out: A - count of '?' in file name
;      ZF is set for fegular file name
ccp_cv_fcb_filename:
    LD	 HL, ccp_current_fcb
    CALL sum_hl_a
    PUSH HL
    PUSH HL
    XOR	 A
    LD	 (ccp_chg_drive), A
    LD	 HL, (ccp_inp_line_addr)		            ; HL -> input line
    EX	 DE, HL
    CALL ccp_find_no_space                          ; get next non blank char
    EX	 DE, HL
    LD	 (cur_name_ptr), HL                         ; save name ptr
    EX	 DE, HL
    POP	 HL
    LD	 A, (DE)		                            ; load first name char
    OR	 A
    JP	 Z, cur_cvf_n_end
    SBC	 A, 'A'-1			                        ; 0x40 for drive letter
    LD	 B, A
    INC	 DE
    LD	 A, (DE)
    CP	 ':'			                            ; is ':' after drive letter?
    JP	 Z, cur_cvf_drv_ltr
    DEC	 DE                                         ; no, step back

cur_cvf_n_end:
    LD	 A, (ccp_cur_drive)
    LD	 (HL), A
    JP	 cur_cvf_basic_fn

cur_cvf_drv_ltr:
    LD	 A, B
    LD	 (ccp_chg_drive), A                         ; set change drive flag
    LD	 (HL), B
    INC	 DE

cur_cvf_basic_fn:
    LD	 B, 8                                       ; file name length

cur_cvf_chr_nxt:
    CALL cpp_valid_inp
    JP	 Z, cur_cvf_sp_remains
    INC	 HL
    CP	 '*'
    JP	 NZ, cur_cvf_no_star
    LD	 (HL), '?'
    JP	 cur_cvf_nxt1

cur_cvf_no_star:
    LD	 (HL), A
    INC	 DE

cur_cvf_nxt1:
    DEC	 B
    JP	 NZ, cur_cvf_chr_nxt

cur_cvf_nxt_delim:
    CALL cpp_valid_inp
    JP	 Z, cur_cvf_ext
    INC	 DE
    JP	 cur_cvf_nxt_delim

    ; fill remains with spaces
cur_cvf_sp_remains:
    INC	 HL
    LD	 (HL), ASCII_SP
    DEC	 B
    JP	 NZ, cur_cvf_sp_remains

    ; file name extension
cur_cvf_ext:
    LD	 B, 3
    CP	 '.'
    JP	 NZ, cur_cvf_ext_fill_sp
    INC	 DE
    ; handle current ext char
cur_cvf_ext_nxt:
    CALL cpp_valid_inp
    JP	 Z, cur_cvf_ext_fill_sp
    INC	 HL
    ; change * to ?
    CP	 '*'
    JP	 NZ, cur_cvf_no_star2
    LD	 (HL), '?'
    JP	 cur_cvf_nxt2
cur_cvf_no_star2:
    LD	 (HL), A
    INC	 DE
cur_cvf_nxt2:
    DEC	 B
    JP	 NZ, cur_cvf_ext_nxt

cur_cvf_ext_skip:
    CALL cpp_valid_inp
    JP	 Z, cur_cvf_rst_attrs
    INC	 DE
    JP	 cur_cvf_ext_skip

    ; skip remains ext pos with dpaces
cur_cvf_ext_fill_sp:
    INC	 HL
    LD	 (HL), ASCII_SP
    DEC	 B
    JP	 NZ, cur_cvf_ext_fill_sp

    ; set fcb extent, res1, res2 to 0
cur_cvf_rst_attrs:
    LD	 B, 3

cur_cvf_attrs_nxt:
    INC	 HL
    LD	 (HL), 0x0
    DEC	 B
    JP	 NZ, cur_cvf_attrs_nxt

    EX	 DE, HL
    LD	 (ccp_inp_line_addr), HL		            ; save input line pointer

    ; Check for ambigeous file name
    POP	 HL                                         ; -> file name in fcb
    LD	 BC, 0x000b                                 ; b=0, c=11
cur_cvf_ambig:
    INC	 HL
    LD	 A, (HL)
    CP	 '?'
    JP	 NZ, cur_cvf_ambig_nxt1
    INC	 B
cur_cvf_ambig_nxt1:
    DEC	 C
    JP	 NZ, cur_cvf_ambig
    LD	 A, B                                       ; a = count of '?'
    OR	 A                                          ; set ZF if regular filename
    RET

; ---------------------------------------------------
; CP/M command table
; ---------------------------------------------------
cpm_cmd_tab:
    DB	"DIR "
    DB  "ERA "
    DB  "TYPE"
    DB  "SAVE"
    DB  "REN "
    DB  "USER"

cpm_pattern:
    DB	0xF9, 0x16, 0, 0, 0, 0x6B                   ; CP/M serial number?
    ;LD   SP, HL
    ;LD   D, 0x00
    ;NOP
    ;NOP
    ;LD   L,E

; ---------------------------------------------------
; Search for CP/M command
; Out: A - command number
; ---------------------------------------------------
ccp_search_cmd:
    LD	 HL, cpm_cmd_tab
    LD	 C, 0x0

ccp_sc_cmd_nxt:
    LD	 A, C
    CP	 CCP_COMMAND_CNT                            ; 6
    RET	 NC
    LD	 DE, ccp_current_fcb_fn                     ; fcb filename
    LD	 B, CCP_COMMAND_LEN                         ; max cmd len
ccp_sc_chr_nxt:
    LD	 A, (DE)
    CP	 (HL)		  								; compate fcb fn and command table
    JP	 NZ, ccp_sc_no_match                        ; cmd not match
    INC	 DE
    INC	 HL
    DEC	 B
    JP	 NZ, ccp_sc_chr_nxt
    ; last can be space for 3-letter commands
    LD	 A, (DE)
    CP	 ASCII_SP
    JP	 NZ, ccp_sc_skip_cmd
    LD	 A, C                                       ; return command number in A
    RET

    ; skip remains
ccp_sc_no_match:
    INC	 HL
    DEC	 B
    JP	 NZ, ccp_sc_no_match
    ; go to next cmd
ccp_sc_skip_cmd:
    INC	 C
    JP	 ccp_sc_cmd_nxt

; --------------------------------------------------
; Clear command buffer and go to command processor
; --------------------------------------------------
ccp_clear_buff:
    XOR	 A
    LD	 (ccp_inbuff+1), A                          ; actual buffer len = 0

; --------------------------------------------------
; Entrance to CCP
; Inp: C - current user * 16
; --------------------------------------------------
@ccp_ent:
    LD	 SP, ccp_stack
    PUSH BC                                         ;
    LD	 A, C
    ; / 16
    RRA
    RRA
    RRA
    RRA
    ; cur user no in low nibble
    AND	 0x0f                                       ; user 0..15
    LD	 E, A
    CALL ccp_bdos_set_user
    CALL ccp_bdos_drv_allreset
    LD	 (ccp_batch), A
    POP	 BC
    LD	 A, C                                        ; a = user*16
    AND	 0xf                                         ; low nibble - drive
    LD	 (ccp_cur_drive), A
    CALL ccp_bdos_drv_set
    LD	 A, (ccp_inbuff+1)
    OR	 A
    JP	 NZ, ccp_process_cmd

; --------------------------------------------------
; Out prompt and get user command from console
; --------------------------------------------------
ccp_get_command:
    LD	 SP, ccp_stack                              ; reset stack pointer
    CALL ccp_out_crlf                               ; from new line
    CALL ccp_bdos_drv_get
    ADD	 A, 65                                      ; convert drive no to character
    CALL ccp_print                                  ; print current drive letter
    LD	 A, '>'                                     ; and prompt
    CALL ccp_print
    CALL ccp_get_inp                                ; and wait string

; --------------------------------------------------
; Process command
; --------------------------------------------------
ccp_process_cmd:
    LD	 DE, dma_buffer
    CALL ccp_bdos_dma_set                           ; setup buffer
    CALL ccp_bdos_drv_get
    LD	 (ccp_cur_drive), A                         ; store cur drive
    CALL ccp_cv_first_to_fcb                        ; convert first command parameter to fcb
    CALL NZ, print_syn_err                          ; if wildcard, out error message
    LD	 A, (ccp_chg_drive)                         ; check drive change flag
    OR	 A
    JP	 NZ, ccp_unk_cmd                            ; if drive changed, handle as unknown command
    CALL ccp_search_cmd                             ; ret A = command number
    LD	 HL, ccp_cmd_addr
    ; DE = command number
    LD	 E, A
    LD	 D, 0x0
    ADD	 HL, DE
    ADD	 HL, DE                                     ; HL = HL + 2*command_number
    LD	 A, (HL)
    INC	 HL
    LD	 H, (HL)
    LD	 L, A
    JP	 (HL)                                       ; jump to command handler

ccp_cmd_addr:
    DW   cmd_dir
    DW   cmd_erase
    DW   cmd_type
    DW   cmd_save
    DW   cmd_ren
    DW   cmd_user
    DW   ccp_entry

; ---------------------------------------------------
; di+halt if serial number validation failed
; ---------------------------------------------------
cpp_halt:
    LD   HL, 0x76f3
    LD	 (ccp_ram_ent), HL
    LD	 HL, ccp_ram_ent
    JP	 (HL)

ccp_type_rd_err:
    LD	 BC, .msg_read_error	  					; BC -> "READ ERROR"
    JP	 ccp_out_crlf_msg

.msg_read_error:
    DB	 "READ ERROR", 0

; ---------------------------------------------------
; Out message 'NO FILE'
; ---------------------------------------------------
ccp_out_no_file:
    LD	 BC, .msg_no_file	      					; BC -> "NO FILE"
    JP	 ccp_out_crlf_msg
.msg_no_file:
    DB	 "NO FILE", 0

; ---------------------------------------------------
; Decode a command in form: "A>filename number"
; Out: A - number
; ---------------------------------------------------
ccp_decode_num:
    CALL ccp_cv_first_to_fcb
    LD	 A, (ccp_chg_drive)
    OR	 A
    JP	 NZ, print_syn_err                          ; error if drive letter specified
    LD	 HL, ccp_current_fcb_fn
    LD	 BC, 11                                     ; b=0, c=11 (filename len)

    ; decode number
ccp_dff_nxt_num:
    LD	 A, (HL)
    CP	 ASCII_SP
    JP	 Z, ccp_dff_num_fin                         ; space - end of number
    ; check for digit
    INC	 HL
    SUB	 '0'
    CP	 10
    JP	 NC, print_syn_err                          ; not a digit
    LD	 D, A                                       ; d = number
    LD	 A, B
    AND	 0xe0                                       ; check B (sum) overflow
    JP	 NZ, print_syn_err
    ; A=B*10
    LD	 A, B
    RLCA
    RLCA
    RLCA                                            ; *8
    ADD	 A, B                                       ; *9
    JP	 C, print_syn_err                           ; error if overflow
    ADD	 A, B                                       ; * 10
    JP	 C, print_syn_err                           ; error if overflow
    ; B = B + B*10
    ADD	 A, D
    JP	 C, print_syn_err                           ; error if overflow
    LD	 B, A
    ; to next number
    DEC	 C
    JP	 NZ, ccp_dff_nxt_num
    RET

ccp_dff_num_fin:
    LD	 A, (HL)
    CP	 ASCII_SP
    JP	 NZ, print_syn_err                          ; will be space after number
    INC	 HL
    DEC	 C
    JP	 NZ, ccp_dff_num_fin
    LD	 A, B
    RET

; --------------------------------------------------
; Move 3 bytes from [HL] to [DE]
; (Used only to move file extension)
; --------------------------------------------------
ccp_mv_hlde_3:
    LD	 B, 3

; --------------------------------------------------
; Move B bytes from [HL] to [DE]
; --------------------------------------------------
ccp_mv_hlde_b:
    LD	 A, (HL)
    LD	 (DE), A
    INC	 HL
    INC	 DE
    DEC	 B
    JP	 NZ, ccp_mv_hlde_b
    RET

; --------------------------------------------------
; Return byte from address dma_buffer[A+C]
; Out: A - byte at dma_buffer[A+C]
;      HL - dma_buffer+A+C
; --------------------------------------------------
get_std_buff_ac:
    LD	 HL, dma_buffer
    ADD	 A, C
    CALL sum_hl_a
    LD	 A, (HL)
    RET

; --------------------------------------------------
; Check drive change and select if needed
; Reset drive byte in fcb to 0 (use default)
; --------------------------------------------------
ccp_drive_sel:
    XOR	 A
    LD	 (ccp_current_fcb), A
    LD	 A, (ccp_chg_drive)
    OR	 A
    RET	 Z                                          ; no need to change cur drive
    DEC	 A
    LD	 HL, ccp_cur_drive
    CP	 (HL)
    RET	 Z                                          ; current and new drive is same
    JP	 ccp_bdos_drv_set                           ; change

; --------------------------------------------------
; Restore previous drive if changed during operation
; --------------------------------------------------
ccp_restor_drv:
    LD	 A, (ccp_chg_drive)
    OR	 A
    RET	 Z                                          ; not changed
    DEC	 A
    LD	 HL, ccp_cur_drive                          ; chk cur drive
    CP	 (HL)
    RET	 Z                                          ; new and previous drive is same
    LD	 A, (ccp_cur_drive)
    JP	 ccp_bdos_drv_set                           ; restore to previous drive

; --------------------------------------------------
; Handle user DIR command
; --------------------------------------------------
cmd_dir:
    CALL ccp_cv_first_to_fcb
    CALL ccp_drive_sel
    ; check filemask specified
    LD	 HL, ccp_current_fcb_fn
    LD	 A, (HL)
    CP	 ASCII_SP
    JP	 NZ, .dir_fmask                             ; yes specified

    ; fill with wildcard symbol
    LD	 B, 11
.fill_wc:
    LD	 (HL), '?'
    INC	 HL
    DEC	 B
    JP	 NZ, .fill_wc

.dir_fmask:
    LD	 E, 0x0                                     ; cursor at 0
    PUSH DE
    ; find first file
    CALL ccp_find_first
    CALL Z, ccp_out_no_file                         ; no file found

.dir_f_next:
    JP	 Z, .dir_f_no_more
    LD	 A, (ccp_bdos_result_code)
    ; find filename pos in direntry
    ; a = a * 32
    RRCA
    RRCA
    RRCA
    AND	 0x60

    LD	 C, A
    LD	 A, 0x0a

    ; get std_buff[10+pos*8]
    CALL get_std_buff_ac
    RLA                                             ; CF<-[7:0]<-CF
    JP	 C, .dir_dont_lst                           ; don't display sys files
    POP	 DE
    LD	 A, E                                       ; a = cursor
    INC	 E
    PUSH DE                                         ; cursor++
    AND	 0x3
    PUSH AF
    JP	 NZ, .dir_no_eol
    ; eol, print new line
    CALL ccp_out_crlf
    ; print A:
    PUSH BC
    CALL ccp_bdos_drv_get
    POP	 BC
    ADD	 A, 'A'
    CALL ccp_putc
    LD	 A, ':'
    CALL ccp_putc
    JP	 .dir_out_sp
.dir_no_eol:
    ; add space between filenames
    CALL ccp_out_space
    LD	 A, ':'
    CALL ccp_putc
.dir_out_sp:
    CALL ccp_out_space
    LD	 B, 0x1
.dir_get_one:
    LD	 A, B
    CALL get_std_buff_ac
    AND	 0x7f                                       ; mask status bit
    CP	 ASCII_SP                                   ; name end?
    JP	 NZ, .no_name_end
    ; at end of file name
    POP	 AF
    PUSH AF
    CP	 0x3
    JP	 NZ, .dir_end_ext
    LD	 A, 0x9
    CALL get_std_buff_ac                            ; chk ext
    AND	 0x7f                                       ; 7bit
    CP	 ASCII_SP
    JP	 Z, .dir_skp_sp                             ; do not print space
.dir_end_ext:
    LD	 A, ASCII_SP
.no_name_end:
    CALL ccp_putc
    INC	 B
    LD	 A, B
    CP	 12
    JP	 NC, .dir_skp_sp                            ; print until end of file ext
    CP	 9
    JP	 NZ, .dir_get_one                           ; start of file ext?
    CALL ccp_out_space                              ; print sep space
    JP	 .dir_get_one
.dir_skp_sp:
    POP	 AF
.dir_dont_lst:
    ; stop if key pressed
    CALL ccp_getkey_no_wait
    JP	 NZ, .dir_f_no_more

    ; find next directory entry
    CALL ccp_bdos_find_next
    JP	 .dir_f_next

    ; no more to print
.dir_f_no_more:
    POP	 DE
    JP	 ccp_cmdline_back

; --------------------------------------------------
; Handle user ERA command
; --------------------------------------------------
cmd_erase:
    CALL ccp_cv_first_to_fcb
    ; check for *.*
    CP	 11
    JP	 NZ, .era_no_wc
    ; confirm erase all
    LD	 BC, msg_all_yn		                        ;= "ALL (Y/N)?"
    CALL ccp_out_crlf_msg
    CALL ccp_get_inp
    LD	 HL, ccp_inbuff+1
    ; check user input
    DEC	 (HL)
    JP	 NZ, ccp_get_command
    INC	 HL
    LD	 A, (HL)		                            ; user input, first letter
    CP	 'Y'
    JP	 NZ, ccp_get_command                        ; return in not exactly 'Y'
    INC	 HL
    LD	(ccp_inp_line_addr), HL

.era_no_wc:
    CALL ccp_drive_sel                              ; select drive
    LD	 DE, ccp_current_fcb                        ; specify current fcb
    CALL ccp_bdos_era_file                            ; and delete file
    INC	 A
    CALL Z, ccp_out_no_file
    JP	 ccp_cmdline_back                           ; go back to command line

msg_all_yn:
    DB	 "ALL (Y/N)?", 0

; --------------------------------------------------
; Handle user TYPE command
; --------------------------------------------------
cmd_type:
    CALL ccp_cv_first_to_fcb
    JP	 NZ, print_syn_err                          ; error if wildcard
    ; select drive and open
    CALL ccp_drive_sel
    CALL ccp_open_cur_fcb
    JP	 Z, .not_found                              ; cant open file
    CALL ccp_out_crlf
    LD	 HL, ccp_bytes_ctr
    LD	 (HL), 0xff                                 ; 255>128 for read first sector

.cont_or_read:
    LD	 HL, ccp_bytes_ctr
    LD	 A, (HL)
    CP	 128
    JP	 C, .out_next_char

    ; read 128 bytes
    PUSH HL
    CALL ccp_read_f_fcb
    POP	 HL
    JP	 NZ, .read_no_ok
    ; clear counter
    XOR	 A
    LD	 (HL), A

.out_next_char:
    ; get next byte from buffer
    INC	 (HL)
    LD	 HL, dma_buffer
    ; calc offset
    CALL sum_hl_a
    LD	 A, (HL)
    CP	 ASCII_SUB                                  ; Ctrl+Z end of text file
    JP	 Z, ccp_cmdline_back                        ; yes, back to cmd line
    CALL ccp_print                                  ; print char to output device
    ; interrupt if key pressed
    CALL ccp_getkey_no_wait
    JP	 NZ, ccp_cmdline_back
    ;
    JP	 .cont_or_read

    ; non zero result from f_read
.read_no_ok:
    DEC	 A
    JP	 Z, ccp_cmdline_back                        ; A=1 - EOF, return to cmd line
    CALL ccp_type_rd_err                            ; else read error

.not_found:
    CALL ccp_restor_drv
    JP	 print_syn_err

; --------------------------------------------------
; Handle user SAVE command
; --------------------------------------------------
cmd_save:
    CALL ccp_decode_num                             ; get num of pages
    PUSH AF                                         ; and store
    CALL ccp_cv_first_to_fcb                        ; conv filename to fcb
    JP	 NZ, print_syn_err                          ; error if wildcard
    ; delete specified file
    CALL ccp_drive_sel
    LD	 DE, ccp_current_fcb
    PUSH DE
    CALL ccp_bdos_era_file
    POP	 DE
    ; create specified file
    CALL ccp_bdos_make_f
    JP	 Z, ccp_no_space                            ; 0xff+1 if error
    XOR	 A
    LD	 (ccp_current_fcb_cr), A                ; curr record = 0
    POP	 AF                                         ; a = num pages
    LD	 L, A
    LD	 H, 0
    ADD	 HL, HL                                     ; HL = A * 2 - number of sectors
    LD	 DE, tpa_start

.write_next:
    ; all sectors written?
    LD 	 A, H
    OR	 L
    JP	 Z, ccp_close_f_cur                         ; no more sectors to write
    DEC	 HL
    PUSH HL
    ; set buffer address to memory to write
    LD	 HL, 128
    ADD	 HL, DE
    PUSH HL
    CALL ccp_bdos_dma_set
    ; and write sector
    LD	 DE, ccp_current_fcb
    CALL ccp_bdos_f_write
    POP	 DE
    POP	 HL
    JP	 NZ, ccp_no_space                           ; check for no space left
    JP	 .write_next

; Close current file
ccp_close_f_cur:
    LD	 DE, ccp_current_fcb
    CALL ccp_bdos_close_f
    INC	 A
    JP	 NZ, rest_buf_ret_cmd

; --------------------------------------------------
; Out error message about no space left
; --------------------------------------------------
ccp_no_space:
    LD	 BC, msg_no_space		                    ; BC -> "NO SPACE"
    CALL ccp_out_crlf_msg

rest_buf_ret_cmd:
    CALL cpp_set_disk_buff_addr
    JP	 ccp_cmdline_back

msg_no_space:
    DB	"NO SPACE", 0

; --------------------------------------------------
; Handle user REN command
; --------------------------------------------------
cmd_ren:
    CALL ccp_cv_first_to_fcb                        ; get first file name
    JP	 NZ, print_syn_err                          ; error if wildcard
    LD	 A, (ccp_chg_drive)                         ; remember drive change flag
    PUSH AF
    ; check file already exists
    CALL ccp_drive_sel
    CALL ccp_find_first
    JP	 NZ, .file_exists
    ; move filename to "second slot"
    LD	 HL, ccp_current_fcb
    LD	 DE, ccp_current_fcb_fn+15
    LD	 B, 16
    CALL ccp_mv_hlde_b
    ;
    LD	 HL, (ccp_inp_line_addr)		            ; restore cmd line pointer
    EX	 DE, HL
    CALL ccp_find_no_space                          ; skip spaces between parameters
    CP	 '='
    JP	 Z, .do_rename
    CP	 '_'
    JP	 NZ, .rename_err

.do_rename:
    EX	 DE, HL
    INC	 HL                                         ; skip sep
    LD	 (ccp_inp_line_addr), HL		            ; -> second param
    CALL ccp_cv_first_to_fcb                        ; get second name
    JP	 NZ, .rename_err                            ; error if wildcard
    ; if drive specified it will be same as previous
    POP	 AF
    LD	 B, A
    LD	 HL, ccp_chg_drive
    LD	 A, (HL)
    OR	 A
    JP	 Z, .same_drive                             ; ok, it is same
    CP	 B
    LD	 (HL), B                                    ; restore first drive
    JP	 NZ, .rename_err

.same_drive:
    LD	 (HL), B
    ; check for seacond file not exists
    XOR	 A
    LD	 (ccp_current_fcb), A
    CALL ccp_find_first
    JP	 Z, .second_exists
    ; calll bdos to rename
    LD	 DE, ccp_current_fcb
    CALL ccp_bdos_rename_f
    JP	 ccp_cmdline_back

.second_exists:
    CALL ccp_out_no_file
    JP	 ccp_cmdline_back

.rename_err:
    CALL ccp_restor_drv
    JP	 print_syn_err

.file_exists:
    LD	 BC, .msg_file_exists		                ; BC -> "FILE EXISTS"
    CALL ccp_out_crlf_msg
    JP	 ccp_cmdline_back

.msg_file_exists:
    DB	 "FILE EXISTS", 0

; --------------------------------------------------
; Handle user USER command
; --------------------------------------------------
cmd_user:
    CALL ccp_decode_num                             ; get user number
    ; user will be 0..15
    CP	 16
    JP	 NC, print_syn_err                          ; >15 - error
    LD	 E, A                                       ; save in E
    ; check for other parameters
    LD	 A, (ccp_current_fcb_fn)
    CP	 ASCII_SP
    JP	 Z, print_syn_err                           ; error if other parameters specified
    ; call bdos to set current user
    CALL ccp_bdos_set_user
    JP	 ccp_cmdline_back1

ccp_unk_cmd:
    CALL ccp_verify_pattern                         ; check if system valid
    ; check for file to execute specified
    LD	 A, (ccp_current_fcb_fn)
    CP	 ASCII_SP
    JP	 NZ, .exec_file
    ; drive change?
    LD	 A, (ccp_chg_drive)
    OR	 A
    JP	 Z, ccp_cmdline_back1                       ; no, return to cmd line
    ; change drive
    DEC	 A
    LD	 (ccp_cur_drive), A
    CALL ccp_reset_cur_drv
    CALL ccp_bdos_drv_set
    JP	 ccp_cmdline_back1

.exec_file:
    ; check file extension
    LD	 DE, ccp_current_fcb_ft
    LD	 A, (DE)
    CP	 ASCII_SP
    JP	 NZ, print_syn_err
    PUSH DE
    ; select specified drive
    CALL ccp_drive_sel
    POP	 DE
    ; set file ext to 'COM'
    LD	 HL, msg_com			                    ; HL -> 'COM'
    CALL ccp_mv_hlde_3
    CALL ccp_open_cur_fcb
    JP	 Z, cant_open_exe
    LD	 HL, tpa_start                              ; load to start of TPA 0x100

.read_next_sec:
    PUSH HL                                         ; store start pointer
    EX	 DE, HL
    CALL ccp_bdos_dma_set

    LD	 DE, ccp_current_fcb
    CALL ccp_bdos_read_f
    JP	 NZ, .read_no_ok                            ; check for read error

    ; shift start pointer for sector size
    POP	 HL
    LD	 DE, 128
    ADD	 HL, DE
    ; check for enough space in RAM
    LD	 DE, ccp_ram_ent
    LD	 A, L
    SUB	 E
    LD	 A, H
    SBC	 A, D
    JP	 NC, ccp_bad_load
    JP	 .read_next_sec

.read_no_ok:
    POP	 HL
    DEC	 A
    JP	NZ, ccp_bad_load                            ; it is not EOF, is error
    ; ok, EOF
    CALL ccp_restor_drv                             ; get first filename
    CALL ccp_cv_first_to_fcb
    LD	 HL, ccp_chg_drive                          ; hl -> buff[16]
    PUSH HL
    LD	A, (HL)
    LD	(ccp_current_fcb), A                        ; set drive letter in current fcb
    LD	A, 16
    CALL ccp_cv_fcb_filename                        ; replace wildcards
    POP	 HL
    ; set drive for second file in reserved fcb area
    LD	 A, (HL)
    LD	 (ccp_current_fcb_al), A
    ; clear record count
    XOR	 A
    LD	 (ccp_current_fcb_cr), A
    ; Move current to default FCB
    LD	 DE, fcb1
    LD	 HL, ccp_current_fcb
    LD	 B, 33
    CALL ccp_mv_hlde_b

    ; move remainder of cmd line to 0x0080
    LD	 HL, ccp_inp_line
.skip_nosp:
    LD	 A, (HL)
    OR	 A
    JP	 Z, .z_or_sp
    CP	 ASCII_SP
    JP	 Z, .z_or_sp
    INC	 HL
    JP	 .skip_nosp

.z_or_sp:
    LD	 B, 0                                       ; len of cmd line for program = 0
    LD	 DE, p_cmd_line                             ; destination address for cmd line

.copy_cmd_line:
    LD	 A, (HL)
    LD	 (DE), A
    OR	 A
    JP	 Z, .stor_len
    INC	 B
    INC	 HL
    INC	 DE
    JP	 .copy_cmd_line

.stor_len:
    LD	 A, B
    LD	 (p_cmd_line_len), A
    ; next line
    CALL ccp_out_crlf
    ; set buffer to cmd line
    CALL cpp_set_disk_buff_addr
    ; set drive
    CALL ccp_set_cur_drv
    ; and call loaded program
    CALL tpa_start
    ; restore stack first
    LD	 SP, ccp_stack
    ; restore current drive
    CALL ccp_reset_cur_drv
    CALL ccp_bdos_drv_set
    ; return back to command line mode
    JP	 ccp_get_command

cant_open_exe:
    CALL ccp_restor_drv
    JP	 print_syn_err

ccp_bad_load:
    LD	 BC, .msg_bad_load		                    ; BC -> "BAD LOAD"
    CALL ccp_out_crlf_msg
    JP	 ccp_cmdline_back

.msg_bad_load:
    DB	 "BAD LOAD", 0

msg_com:
    DB	 "COM"

; --------------------------------------------------
; Return back to command line
; --------------------------------------------------
ccp_cmdline_back:
    CALL ccp_restor_drv

ccp_cmdline_back1:
    CALL ccp_cv_first_to_fcb
    LD	 A, (ccp_current_fcb_fn)
    SUB	 0x20
    LD	 HL, ccp_chg_drive
    OR	 (HL)
    JP	 NZ, print_syn_err
    JP	 ccp_get_command

    DW	0h, 0h, 0h, 0h, 0h, 0h, 0h, 0h

ccp_stack EQU $

ccp_batch:
    DB	0h

ccp_batch_fcb:
    DB	0h                                          ; drive code, 0 - default
    DB  "$$$     SUB"                               ; filename
    DB  0h                                          ; extent
    DB  0h                                          ; S1
ccp_batch_fcb_s2:
    DB	0h                                          ; S2 Extent [6:0] bits and [7] write flag
ccp_batch_fcb_rc:
    DB  0h                                          ; sectors count
ccp_batch_fcb_al:
    DS 16, 0                                        ; reserved by CPM use only
ccp_batch_fcb_cr:
    DB	0h                                          ; current sector to read/write

ccp_current_fcb:
    DB	0h
ccp_current_fcb_fn:
    DS  8, 0
ccp_current_fcb_ft:
    DS  3, 0
ccp_current_fcb_ex:
    DB  0h                                          ; extent
    DB  0h                                          ; s1
    DB	0h                                          ; s2
    DB  0h                                          ; sectors count
ccp_current_fcb_al:
    DS  16, 0                                       ; reserved by CPM use only
ccp_current_fcb_cr:
    DB	0h                                          ; current sector to read/write

ccp_bdos_result_code:
    DB	0h

ccp_cur_drive:
    DB	0h

ccp_chg_drive:                                      ; change drive flag, 0 - no change
    DB 0
ccp_bytes_ctr:
    DW 0
    ; reserved
    DS 13, 0


bdos_enter_jump		EQU	$+6

; -------------------------------------------------------
; Filler to align blocks in ROM
; -------------------------------------------------------
LAST        EQU     $
CODE_SIZE   EQU     LAST-0xB200
;FILL_SIZE   EQU     0x500-CODE_SIZE

    DISPLAY "| CCP_RAM\t| ",/H,ccp_ram_ent,"  | ",/H,CODE_SIZE," | \t    |"

    ; Check integrity
    ASSERT ccp_cmdline_back = 0xb986

	ENDMODULE

	IFNDEF	BUILD_ROM
		OUTEND
	ENDIF