; shell
;
; Runs a shell over a block device interface.

; The shell spits a welcome prompt, wait for input and compare the first 4 chars
; of the input with a command table and call the appropriate routine if it's
; found, an error if it's not.
;
; To determine the correct routine to call we first go through cmds in
; shellCmdTbl. This means that we first go through internal cmds, then cmds
; "grafted" by glue code.
;
; If the command isn't found, SHELL_CMDHOOK is called, which should set A to
; zero if it executes something. Otherwise, SHELL_ERR_UNKNOWN_CMD will be
; returned.
;
; See constants below for error codes.
;
; All numerical values in the Collapse OS shell are represented and parsed in
; hexadecimal form, without prefix or suffix.

; *** REQUIREMENTS ***
; err
; core
; parse
; stdio

; *** DEFINES ***
; SHELL_EXTRA_CMD_COUNT: Number of extra cmds to be expected after the regular
;                        ones. See comment in COMMANDS section for details.
; SHELL_RAMSTART

; *** CONSTS ***

; number of entries in shellCmdTbl
.equ	SHELL_CMD_COUNT		6+SHELL_EXTRA_CMD_COUNT

; maximum length for shell commands. Should be confortably below stdio's
; readline buffer length.
.equ	SHELL_MAX_CMD_LEN	0x10

; *** VARIABLES ***
; Memory address that the shell is currently "pointing at" for peek, load, call
; operations. Set with mptr.
.equ	SHELL_MEM_PTR	SHELL_RAMSTART

; Places where we store arguments specifiers and where resulting values are
; written to after parsing.
.equ	SHELL_CMD_ARGS	@+2

; Pointer to a hook to call when a cmd name isn't found
.equ	SHELL_CMDHOOK	@+PARSE_ARG_MAXCOUNT

.equ	SHELL_RAMEND	@+2

; *** CODE ***
shellInit:
	xor	a
	ld	(SHELL_MEM_PTR), a
	ld	(SHELL_MEM_PTR+1), a
	ld	hl, noop
	ld	(SHELL_CMDHOOK), hl

	; print welcome
	ld	hl, .welcome
	jp	printstr

.welcome:
	.db	"Collapse OS", ASCII_CR, ASCII_LF, "> ", 0

; Inifite loop that processes input. Because it's infinite, you should jump
; to it rather than call it. Saves two precious bytes in the stack.
shellLoop:
	call	stdioReadLine
	call	printcrlf
	call	shellParse
	ld	hl, .prompt
	call	printstr
	jr	shellLoop

.prompt:
	.db	"> ", 0

; Parse command (null terminated) at HL and calls it
shellParse:
	; first thing: is command empty?
	ld	a, (hl)
	or	a
	ret	z	; empty, nothing to do

	push	af
	push	bc
	push	de
	push	hl
	push	ix

	; Before looking for a suitable command, let's make the cmd line more
	; usable by replacing the first ' ' with a null char. This way, cmp is
	; easy to make.
	push	hl		; --> lvl 1
	ld	a, ' '
	call	findchar
	jr	z, .hasArgs
	; no arg, (HL) is zero to facilitate processing later, add a second
	; null next to that one to indicate unambiguously that we have no args.
	inc	hl
	; Oh wait, before we proceed, is our cmd length within limits? cmd len
	; is currently in A from findchar
	cp	SHELL_MAX_CMD_LEN
	jr	c, .hasArgs	; within limits
	; outside limits
	ld	a, SHELL_ERR_UNKNOWN_CMD
	jr	.error
.hasArgs:
	xor	a
	ld	(hl), a
	pop	hl		; <-- lvl 1, beginning of cmd

	ld	de, shellCmdTbl
	ld	b, SHELL_CMD_COUNT

.loop:
	push	de		; we need to keep that table entry around...
	call	intoDE		; Jump from the table entry to the cmd addr.
	ld	a, 4		; 4 chars to compare
	call	strncmp
	pop	de
	jr	z, .found
	inc	de
	inc	de
	djnz	.loop

	; exhausted loop? not found
	ld	a, SHELL_ERR_UNKNOWN_CMD
	; Before erroring out, let's try SHELL_HOOK.
	ld	ix, (SHELL_CMDHOOK)
	call	callIX
	jr	z, .end		; oh, not an error!
	; still an error. Might be different than SHELL_ERR_UNKNOWN_CMD though.
	; maybe a routine was called, but errored out.
	jr	.error

.found:
	; we found our command. DE points to its table entry. Now, let's parse
	; our args.
	call	intoDE		; Jump from the table entry to the cmd addr.

	; advance the HL pointer to the beginning of the args.
	xor	a
	call	findchar
	inc	hl		; beginning of args
	; Now, let's have DE point to the argspecs
	ld	a, 4
	call	addDE

	; We're ready to parse args
	ld	ix, SHELL_CMD_ARGS
	call	parseArgs
	or	a		; cp 0
	jr	nz, .parseerror

	; Args parsed, now we can load the routine address and call it.
	; let's have DE point to the jump line
	ld	hl, SHELL_CMD_ARGS
	ld	a, PARSE_ARG_MAXCOUNT
	call	addDE
	push	de \ pop ix
	; Ready to roll!
	call	callIX
	or	a		; cp 0
	jr	nz, .error	; if A is non-zero, we have an error
	jr	.end

.parseerror:
	ld	a, SHELL_ERR_BAD_ARGS
.error:
	call	shellPrintErr
.end:
	pop	ix
	pop	hl
	pop	de
	pop	bc
	pop	af
	ret

; Print the error code set in A (in hex)
shellPrintErr:
	push	af
	push	hl

	ld	hl, .str
	call	printstr
	call	printHex
	call	printcrlf

	pop	hl
	pop	af
	ret

.str:
	.db	"ERR ", 0

; *** COMMANDS ***
; A command is a 4 char names, followed by a PARSE_ARG_MAXCOUNT bytes of
; argument specs, followed by the routine. Then, a simple table of addresses
; is compiled in a block and this is what is iterated upon when we want all
; available commands.
;
; Format: 4 bytes name followed by PARSE_ARG_MAXCOUNT bytes specifiers,
;         followed by 3 bytes jump. fill names with zeroes
;
; When these commands are called, HL points to the first byte of the
; parsed command args.
;
; If the command is a success, it should set A to zero. If the command results
; in an error, it should set an error code in A.
;
; Extra commands: Other parts might define new commands. You can add these
;                 commands to your shell. First, set SHELL_EXTRA_CMD_COUNT to
;                 the number of extra commands to add, then add a ".dw"
;                 directive *just* after your '.inc "shell.asm"'. Voila!
;

; Set memory pointer to the specified address (word).
; Example: mptr 01fe
shellMptrCmd:
	.db	"mptr", 0b011, 0b001, 0
shellMptr:
	push	hl

	; reminder: z80 is little-endian
	ld	a, (hl)
	ld	(SHELL_MEM_PTR+1), a
	inc	hl
	ld	a, (hl)
	ld	(SHELL_MEM_PTR), a

	ld	hl, (SHELL_MEM_PTR)
	ld	a, h
	call	printHex
	ld	a, l
	call	printHex
	call	printcrlf

	pop	hl
	xor	a
	ret


; peek the number of bytes specified by argument where memory pointer points to
; and display their value. If 0 is specified, 0x100 bytes are peeked.
;
; Example: peek 2 (will print 2 bytes)
shellPeekCmd:
	.db	"peek", 0b001, 0, 0
shellPeek:
	push	bc
	push	hl

	ld	a, (hl)
	ld	b, a
	ld	hl, (SHELL_MEM_PTR)
.loop:	ld	a, (hl)
	call	printHex
	inc	hl
	djnz	.loop
	call	printcrlf

.end:
	pop	hl
	pop	bc
	xor	a
	ret

; poke specified number of bytes where memory pointer points and set them to
; bytes typed through stdioGetC. Blocks until all bytes have been fetched.
shellPokeCmd:
	.db	"poke", 0b001, 0, 0
shellPoke:
	push	bc
	push	hl

	ld	a, (hl)
	ld	b, a
	ld	hl, (SHELL_MEM_PTR)
.loop:	call	stdioGetC
	jr	nz, .loop	; nothing typed? loop
	ld	(hl), a
	inc	hl
	djnz	.loop

	pop	hl
	pop	bc
	xor	a
	ret

; Calls the routine where the memory pointer currently points. This can take two
; parameters, A and HL. The first one is a byte, the second, a word. These are
; the values that A and HL are going to be set to just before calling.
; Example: run 42 cafe
shellCallCmd:
	.db	"call", 0b101, 0b111, 0b001
shellCall:
	push	hl
	push	ix

	; Let's recap here. At this point, we have:
	; 1. The address we want to execute in (SHELL_MEM_PTR)
	; 2. our A arg as the first byte of (HL)
	; 2. our HL arg as (HL+1) and (HL+2)
	; Ready, set, go!
	ld	ix, (SHELL_MEM_PTR)
	ld	a, (hl)
	ex	af, af'
	inc	hl
	ld	a, (hl)
	exx
	ld	h, a
	exx
	inc	hl
	ld	a, (hl)
	exx
	ld	l, a
	ex	af, af'
	call	callIX

.end:
	pop	ix
	pop	hl
	xor	a
	ret

shellIORDCmd:
	.db	"iord", 0b001, 0, 0
	push	bc
	ld	a, (hl)
	ld	c, a
	in	a, (c)
	call	printHex
	xor	a
	pop	bc
	ret

shellIOWRCmd:
	.db	"iowr", 0b001, 0b001, 0
	push	bc
	ld	a, (hl)
	ld	c, a
	inc	hl
	ld	a, (hl)
	out	(c), a
	xor	a
	pop	bc
	ret

; This table is at the very end of the file on purpose. The idea is to be able
; to graft extra commands easily after an include in the glue file.
shellCmdTbl:
	.dw shellMptrCmd, shellPeekCmd, shellPokeCmd, shellCallCmd
	.dw shellIORDCmd, shellIOWRCmd