; *** Variables ***
; Value of `SP` when basic was first invoked. This is where SP is going back to
; on restarts.
.equ	BAS_INITSP	BAS_RAMSTART
; Pointer to next line to run. If nonzero, it means that the next line is
; the first of the list. This is used by GOTO to indicate where to jump next.
; Important note: this is **not** a line number, it's a pointer to a line index
; in buffer. If it's not zero, its a valid pointer.
.equ	BAS_PNEXTLN	@+2
; Points to a routine to call when a command isn't found in the "core" cmd
; table. This gives the opportunity to glue code to configure extra commands.
.equ	BAS_FINDHOOK	@+2
.equ	BAS_RAMEND	@+2

; *** Code ***
basInit:
	ld	(BAS_INITSP), sp
	call	varInit
	call	bufInit
	xor	a
	ld	(BAS_PNEXTLN), a
	ld	(BAS_PNEXTLN+1), a
	ld	hl, unsetZ
	ld	(BAS_FINDHOOK), hl
	ret

basStart:
	ld	hl, .welcome
	call	printstr
	call	printcrlf
	jr	basLoop

.welcome:
	.db "Collapse OS", 0

basLoop:
	ld	hl, .sPrompt
	call	printstr
	call	stdioReadLine
	call	printcrlf
	call	parseDecimalC
	jr	z, .number
	ld	de, basCmds1
	call	basCallCmds
	jr	z, basLoop
	; Error
	call	basERR
	jr	basLoop
.number:
	call	rdSep
	call	bufAdd
	jp	nz, basERR
	jr	basLoop
.sPrompt:
	.db "> ", 0

; Tries to find command specified in (DE) (must be null-terminated) in cmd
; table in (HL). If found, sets IX to point to the associated routine. If
; not found, calls BAS_FINDHOOK so that we look through extra commands
; configured by glue code.
; Destroys HL.
; Z is set if found, unset otherwise.
basFindCmd:
.loop:
	call	strcmp
	call	strskip
	inc	hl		; point to routine
	jr	z, .found	; Z from strcmp
	inc	hl \ inc hl	; skip routine
	ld	a, (hl)
	inc	a		; was it 0xff?
	jr	nz, .loop	; no
	dec	a		; unset Z
	ret
.found:
	call	intoHL
	push	hl \ pop ix
	ret

; Call command in (HL) after having looked for it in cmd table in (DE).
; If found, jump to it. If not found, try (BAS_FINDHOOK). If still not found,
; unset Z. We expect commands to set Z on success. Therefore, when calling
; basCallCmd results in NZ, we're not sure where the error come from, but
; well...
basCallCmd:
	; let's see if it's a variable assignment.
	call	varTryAssign
	ret	z	; Done!
	push	de		; --> lvl 1.
	ld	de, SCRATCHPAD
	call	rdWord
	; cmdname to find in (DE)
	; How lucky, we have a legitimate use of "ex (sp), hl"! We have the
	; cmd table in the stack, which we want in HL and we have the rest of
	; the cmdline in (HL), which we want in the stack!
	ex	(sp), hl
	call	basFindCmd
	jr	z, .skip
	; not found, try BAS_FINDHOOK
	ld	ix, (BAS_FINDHOOK)
	call	callIX
.skip:
	; regardless of the result, we need to balance the stack.
	; Bring back rest of the command string from the stack
	pop	hl		; <-- lvl 1
	ret	nz
	; cmd found, skip whitespace and then jump!
	call	rdSep
	jp	(ix)

; Call a series of ':'-separated commands in (HL) using cmd table in (DE).
; Stop processing as soon as one command unsets Z.
basCallCmds:
	; Commands are not guaranteed at all to preserve HL and DE, so we
	; preserve them ourselves here.
	push	hl	; --> lvl 1
	push	de	; --> lvl 2
	call	basCallCmd
	pop	de	; <-- lvl 2
	pop	hl	; <-- lvl 1
	ret	nz
	call	toEnd
	ret	z	; no more cmds
	; we met a ':', we have more cmds
	inc	hl
	call	basCallCmds
	; move the the end of the string so that we don't run cmds following a
	; ':' twice.
	call	strskip
	ret

basERR:
	ld	hl, .sErr
	call	printstr
	jp	printcrlf
.sErr:
	.db	"ERR", 0

; *** Commands ***
; A command receives its argument through (HL), which is already placed to
; either:
; 1 - the end of the string if the command has no arg.
; 2 - the beginning of the arg, with whitespace properly skipped.
;
; Commands are expected to set Z on success.
basLIST:
	call	bufFirst
	jr	nz, .end
.loop:
	ld	e, (ix)
	ld	d, (ix+1)
	ld	hl, SCRATCHPAD
	call	fmtDecimal
	call	printstr
	ld	a, ' '
	call	stdioPutC
	call	bufStr
	call	printstr
	call	printcrlf
	call	bufNext
	jr	z, .loop
.end:
	cp	a		; ensure Z
	ret


basRUN:
	call	.maybeGOTO
	jr	nz, .loop	; IX already set
	call	bufFirst
	ret	nz
.loop:
	call	bufStr
	ld	de, basCmds2
	push	ix		; --> lvl 1
	call	basCallCmds
	pop	ix		; <-- lvl 1
	jp	nz, .err
	call	.maybeGOTO
	jr	nz, .loop	; IX already set
	call	bufNext
	jr	z, .loop
	cp	a		; ensure Z
	ret
.err:
	; Print line number, then return NZ (which will print ERR)
	ld	e, (ix)
	ld	d, (ix+1)
	ld	hl, SCRATCHPAD
	call	fmtDecimal
	call	printstr
	ld	a, ' '
	call	stdioPutC
	jp	unsetZ

; This returns the opposite Z result as the one we usually see: Z is set if
; we **don't** goto, unset if we do. If we do, IX is properly set.
.maybeGOTO:
	ld	de, (BAS_PNEXTLN)
	ld	a, d
	or	e
	ret	z
	; we goto
	push	de \ pop ix
	; we need to reset our goto marker
	ld	de, 0
	ld	(BAS_PNEXTLN), de
	ret

basPRINT:
	; Do we have arguments at all? if not, it's not an error, just print
	; crlf
	ld	a, (hl)
	or	a
	jr	z, .end
	; Is our arg a string literal?
	call	spitQuoted
	jr	z, .chkAnother	; string printed, skip to chkAnother
	ld	de, SCRATCHPAD
	call	rdWord
	push	hl		; --> lvl 1
	ex	de, hl
	call	parseExpr
	jr	nz, .parseError
	ld	hl, SCRATCHPAD
	call	fmtDecimalS
	call	printstr
	pop	hl		; <-- lvl 1
.chkAnother:
	; Do we have another arg?
	call	rdSep
	jr	z, .another
	; no, we can stop here
.end:
	cp	a		; ensure Z
	jp	printcrlf
.another:
	; Before we jump to basPRINT, let's print a space
	ld	a, ' '
	call	stdioPutC
	jr	basPRINT
.parseError:
	; unwind the stack before returning
	pop	hl		; <-- lvl 1
	ret


basGOTO:
	ld	de, SCRATCHPAD
	call	rdWord
	ex	de, hl
	call	parseExpr
	ret	nz
	call	bufFind
	jr	nz, .notFound
	push	ix \ pop de
	; Z already set
	jr	.end
.notFound:
	ld	de, 0
	; Z already unset
.end:
	ld	(BAS_PNEXTLN), de
	ret

; evaluate truth condition at (HL) and set A to its value
; Z for success (but not truth!)
_basEvalCond:
	push	hl	; --> lvl 1. original arg
	ld	de, SCRATCHPAD
	call	rdWord
	ex	de, hl
	call	parseTruth
	pop	hl	; <-- lvl 1. restore
	ret

basIF:
	call	_basEvalCond
	ret	nz	; error
	or	a
	ret	z
	; expr is true, execute next
	; (HL) back to beginning of args, skip to next arg
	call	toSepOrEnd
	call	rdSep
	ret	nz
	ld	de, basCmds2
	jp	basCallCmds

basWHILE:
	push	hl	; --> lvl 1
	call	_basEvalCond
	jr	nz, .stop	; error
	or	a
	jr	z, .stop
	ret	z
	; expr is true, execute next
	; (HL) back to beginning of args, skip to next arg
	call	toSepOrEnd
	call	rdSep
	ret	nz
	ld	de, basCmds2
	call	basCallCmds
	pop	hl	; <-- lvl 1
	jr	basWHILE
.stop:
	pop	hl	; <-- lvl 1
	ret

basINPUT:
	; If our first arg is a string literal, spit it
	call	spitQuoted
	call	rdSep
	call	stdioReadLine
	call	parseExpr
	ld	(VAR_TBL), de
	call	printcrlf
	cp	a		; ensure Z
	ret

basPEEK:
	call	basDEEK
	ret	nz
	; set MSB to 0
	xor	a		; sets Z
	ld	(VAR_TBL+1), a
	ret

basPOKE:
	call	rdExpr
	ret	nz
	; peek address in IX. Save it for later
	push	ix		; --> lvl 1
	call	rdSep
	call	rdExpr
	push	ix \ pop hl
	pop	ix		; <-- lvl 1
	ret	nz
	; Poke!
	ld	(ix), l
	ret

basDEEK:
	call	rdExpr
	ret	nz
	; peek address in IX. Let's peek and put result in DE
	ld	e, (ix)
	ld	d, (ix+1)
	ld	(VAR_TBL), de
	cp	a		; ensure Z
	ret

basDOKE:
	call	basPOKE
	ld	(ix+1), h
	ret

basOUT:
	call	rdExpr
	ret	nz
	; out address in IX. Save it for later
	push	ix		; --> lvl 1
	call	rdSep
	call	rdExpr
	push	ix \ pop hl
	pop	bc		; <-- lvl 1
	ret	nz
	; Out!
	out	(c), l
	cp	a		; ensure Z
	ret

basIN:
	call	rdExpr
	ret	nz
	push	ix \ pop bc
	ld	d, 0
	in	e, (c)
	ld	(VAR_TBL), de
	; Z set from rdExpr
	ret

basGETC:
	call	stdioGetC
	ld	(VAR_TBL), a
	xor	a
	ld	(VAR_TBL+1), a
	ret

basPUTC:
	call	rdExpr
	ret	nz
	push	ix \ pop hl
	ld	a, l
	call	stdioPutC
	xor	a	; set Z
	ret

basPUTH:
	call	rdExpr
	ret	nz
	push	ix \ pop hl
	ld	a, l
	call	printHex
	xor	a	; set Z
	ret

basSLEEP:
	call	rdExpr
	ret	nz
	push	ix \ pop hl
.loop:
	ld	a, h	; 4T
	or	l	; 4T
	ret	z	; 5T
	dec	hl	; 6T
	jr	.loop	; 12T

basADDR:
	call	rdWord
	ex	de, hl
	ld	de, .specialTbl
.loop:
	ld	a, (de)
	or	a
	jr	z, .notSpecial
	cp	(hl)
	jr	z, .found
	inc	de \ inc de \ inc de
	jr	.loop
.notSpecial:
	; not found, find cmd. needle in (HL)
	ex	de, hl		; now in (DE)
	ld	hl, basCmds1
	call	basFindCmd
	jr	z, .foundCmd
	; no core command? let's try the find hook.
	ld	ix, (BAS_FINDHOOK)
	call	callIX
	ret	nz
.foundCmd:
	; We have routine addr in IX
	ld	(VAR_TBL), ix
	cp	a		; ensure Z
	ret
.found:
	; found special thing. Put in "A".
	inc	de
	call	intoDE
	ld	(VAR_TBL), de
	ret		; Z set from .found jump.

.specialTbl:
	.db	'$'
	.dw	SCRATCHPAD
	.db	0

basUSR:
	call	rdExpr
	ret	nz
	push	ix \ pop iy
	; We have our address to call. Now, let's set up our registers.
	; HL comes from variable H. H's index is 7*2.
	ld	hl, (VAR_TBL+14)
	; DE comes from variable D. D's index is 3*2
	ld	de, (VAR_TBL+6)
	; BC comes from variable B. B's index is 1*2
	ld	bc, (VAR_TBL+2)
	; IX comes from variable X. X's index is 23*2
	ld	ix, (VAR_TBL+46)
	; and finally, A
	ld	a, (VAR_TBL)
	call	callIY
basR2Var:	; Just send reg to vars. Used in basPgmHook
	; Same dance, opposite way
	ld	(VAR_TBL), a
	ld	(VAR_TBL+46), ix
	ld	(VAR_TBL+2), bc
	ld	(VAR_TBL+6), de
	ld	(VAR_TBL+14), hl
	cp	a		; USR never errors out
	ret

; Command table format: Null-terminated string followed by a 2-byte routine
; pointer.

; direct only
basCmds1:
	.db	"list", 0
	.dw	basLIST
	.db	"run", 0
	.dw	basRUN
	.db	"clear", 0
	.dw	bufInit
; statements
basCmds2:
	.db	"print", 0
	.dw	basPRINT
	.db	"goto", 0
	.dw	basGOTO
	.db	"if", 0
	.dw	basIF
	.db	"while", 0
	.dw	basWHILE
	.db	"input", 0
	.dw	basINPUT
	.db	"peek", 0
	.dw	basPEEK
	.db	"poke", 0
	.dw	basPOKE
	.db	"deek", 0
	.dw	basDEEK
	.db	"doke", 0
	.dw	basDOKE
	.db	"out", 0
	.dw	basOUT
	.db	"in", 0
	.dw	basIN
	.db	"getc", 0
	.dw	basGETC
	.db	"putc", 0
	.dw	basPUTC
	.db	"puth", 0
	.dw	basPUTH
	.db	"sleep", 0
	.dw	basSLEEP
	.db	"addr", 0
	.dw	basADDR
	.db	"usr", 0
	.dw	basUSR
	.db	0xff	; end of table