; grid - abstraction for grid-like video output
;
; Collapse OS doesn't support curses-like interfaces: too complicated. However,
; in cases where output don't have to go through a serial interface before
; being displayed, we have usually have access to a grid-like interface.
;
; Direct access to this kind of interface allow us to build an abstraction layer
; that is very much alike curses but is much simpler underneath. This unit is
; this abstraction.
;
; The principle is simple: we have a cell grid of X columns by Y rows and we
; can access those cells by their (X, Y) address. In addition to this, we have
; the concept of an active cursor, which will be indicated visually if possible.
;
; This module provides PutC and GetC routines, suitable for plugging into stdio.
; PutC, for obvious reasons, GetC, for less obvious reasons: We need to wrap
; GetC because we need to update the cursor before calling actual GetC, but
; also, because we need to know when a bulk update ends.
;
; *** Defines ***
;
; GRID_COLS: Number of columns in the grid
; GRID_ROWS: Number of rows in the grid
; GRID_SETCELL: Pointer to routine that sets cell at row D and column E with
;               character in A. If C is nonzero, this cell must be displayed,
;               if possible, as the cursor. This routine is never called with
;		A < 0x20.
; GRID_GETC: Routine that gridGetC will wrap around.
;
; *** Consts ***
.equ	GRID_SIZE	GRID_COLS*GRID_ROWS

; *** Variables ***
; Cursor's column
.equ	GRID_CURX	GRID_RAMSTART
; Cursor's row
.equ	GRID_CURY	@+1
; Whether we scrolled recently. We don't refresh the screen immediately when
; scrolling in case we have many lines being spit at once (refreshing the
; display is then very slow). Instead, we wait until the next gridGetC call
.equ	GRID_SCROLLED	@+1
; Grid's in-memory buffer of the contents on screen. Because we always push to
; display right after a change, this is almost always going to be a correct
; representation of on-screen display.
; The buffer is organized as a rows of columns. The cell at row Y and column X
; is at GRID_BUF+(Y*GRID_COLS)+X.
.equ	GRID_BUF	@+1
.equ	GRID_RAMEND	@+GRID_SIZE

; *** Code ***

gridInit:
	xor	a
	ld	b, GRID_RAMEND-GRID_RAMEND
	ld	hl, GRID_RAMSTART
	jp	fill

; Place HL at row D and column E in the buffer
; Destroys A
_gridPlaceCell:
	ld	hl, GRID_BUF
	ld	a, d
	or	a
	jr	z, .setcol
	push	de		; --> lvl 1
	ld	de, GRID_COLS
.loop:
	add	hl, de
	dec	a
	jr	nz, .loop
	pop	de		; <-- lvl 1
.setcol:
	; We're at the proper row, now let's advance to cell
	ld	a, e
	jp	addHL

; Ensure that A >= 0x20
_gridAdjustA:
	cp	0x20
	ret	nc
	ld	a, 0x20
	ret

; Push row D in the buffer onto the screen.
gridPushRow:
	push	af
	push	bc
	push	de
	push	hl
	; Cursor off
	ld	c, 0
	ld	e, c
	call	_gridPlaceCell
	ld	b, GRID_COLS
.loop:
	ld	a, (hl)
	call	_gridAdjustA
	; A, C, D and E have proper values
	call	GRID_SETCELL
	inc	hl
	inc	e
	djnz	.loop

	pop	hl
	pop	de
	pop	bc
	pop	af
	ret

; Clear row D and push contents to screen
gridClrRow:
	push	af
	push	bc
	push	de
	push	hl
	ld	e, 0
	call	_gridPlaceCell
	ld	a, ' '
	ld	b, GRID_COLS
	call	fill
	call	gridPushRow
	pop	hl
	pop	de
	pop	bc
	pop	af
	ret

gridPushScr:
	push	de
	ld	d, GRID_ROWS-1
.loop:
	call	gridPushRow
	dec	d
	jp	p, .loop
	pop	de
	ret

; Set character under cursor to A. C is passed to GRID_SETCELL as-is.
gridSetCur:
	push	de
	push	hl
	push	af		; --> lvl 1
	ld	a, (GRID_CURY)
	ld	d, a
	ld	a, (GRID_CURX)
	ld	e, a
	call	_gridPlaceCell
	pop	af \ push af	; <--> lvl 1
	ld	(hl), a
	call	_gridAdjustA
	call	GRID_SETCELL
	pop	af		; <-- lvl 1
	pop	hl
	pop	de
	ret

; Call gridSetCur with C = 1.
gridSetCurH:
	push	bc
	ld	c, 1
	call	gridSetCur
	pop	bc
	ret

; Call gridSetCur with C = 0.
gridSetCurL:
	push	bc
	ld	c, 0
	call	gridSetCur
	pop	bc
	ret

; Clear character under cursor
gridClrCur:
	push	af
	ld	a, ' '
	call	gridSetCurL
	pop	af
	ret

gridLF:
	call	gridClrCur
	push	de
	push	af
	ld	a, (GRID_CURY)
	; increase A
	inc	a
	cp	GRID_ROWS
	jr	nz, .noscroll
	; bottom reached, stay on last line and scroll screen
	push	hl
	push	de
	push	bc
	ld	de, GRID_BUF
	ld	hl, GRID_BUF+GRID_COLS
	ld	bc, GRID_SIZE-GRID_COLS
	ldir
	ld	hl, GRID_SCROLLED
	inc	(hl)			; mark as scrolled
	pop	bc
	pop	de
	pop	hl
	dec	a
.noscroll:
	; A has been increased properly
	ld	d, a
	call	gridClrRow
	ld	(GRID_CURY), a
	xor	a
	ld	(GRID_CURX), a
	pop	af
	pop	de
	ret

gridBS:
	call	gridClrCur
	push	af
	ld	a, (GRID_CURX)
	or	a
	jr	z, .lineup
	dec	a
	ld	(GRID_CURX), a
	pop	af
	ret
.lineup:
	; end of line, we need to go up one line. But before we do, are we
	; already at the top?
	ld	a, (GRID_CURY)
	or	a
	jr	z, .end
	dec	a
	ld	(GRID_CURY), a
	ld	a, GRID_COLS-1
	ld	(GRID_CURX), a
.end:
	pop	af
	ret

gridPutC:
	cp	LF
	jr	z, gridLF
	cp	BS
	jr	z, gridBS
	cp	' '
	ret	c		; ignore unhandled control characters

	call	gridSetCurL
	push	af		; --> lvl 1
	; Move cursor
	ld	a, (GRID_CURX)
	cp	GRID_COLS-1
	jr	z, .incline
	; We just need to increase X
	inc	a
	ld	(GRID_CURX), a
	pop	af		; <-- lvl 1
	ret
.incline:
	; increase line and start anew
	call	gridLF
	pop	af		; <-- lvl 1
	ret

gridGetC:
	ld	a, (GRID_SCROLLED)
	or	a
	jr	z, .nopush
	; We've scrolled recently, update screen
	xor	a
	ld	(GRID_SCROLLED), a
	call	gridPushScr
.nopush:
	ld	a, ' '
	call	gridSetCurH
	jp	GRID_GETC