; blockdev
;
; A block device is an abstraction over something we can read from, write to.
;
; A device that fits this abstraction puts the proper hook into itself, and then
; the glue code assigns a blockdev ID to that device. It then becomes easy to
; access arbitrary devices in a convenient manner.
;
; This module exposes a seek/tell/getb/putb API that is then re-routed to
; underlying drivers. There will eventually be more than one driver type, but
; for now we sit on only one type of driver: random access driver.
;
; *** Random access drivers ***
;
; Random access drivers are expected to supply two routines: GetB and PutB.
;
; GetB:
; Reads one byte at address specified in DE/HL and returns its value in A.
; Sets Z according to whether read was successful: Set if successful, unset
; if not.
;
; Unsuccessful reads generally mean that requested addr is out of bounds (we
; reached EOF).
;
; PutB:
; Writes byte in A at address specified in DE/HL. Sets Z according to whether
; the operation was successful.
;
; Unsuccessful writes generally mean that we're out of bounds for writing.
;
; All routines are expected to preserve unused registers except IX which is
; explicitly protected during GetB/PutB calls. This makes quick "handle+jump"
; definitions possible.


; *** DEFINES ***
; BLOCKDEV_COUNT: The number of devices we manage.

; *** CONSTS ***
; *** VARIABLES ***
; Pointer to the selected block device. A block device is a 8 bytes block of
; memory with pointers to GetB, PutB, and a 32-bit counter, in that order.
.equ	BLOCKDEV_SEL		BLOCKDEV_RAMSTART
.equ	BLOCKDEV_RAMEND		@+BLOCKDEV_SIZE

; *** CODE ***
; Put the pointer to the "regular" blkdev selection in DE
blkSelPtr:
	ld	de, BLOCKDEV_SEL

; Select block index specified in A and place them in routine pointers at (DE).
; For example, for a "regular" blkSel, you will want to set DE to BLOCKDEV_SEL.
; Sets Z on success, reset on error.
; If A >= BLOCKDEV_COUNT, it's an error.
blkSel:
	cp	BLOCKDEV_COUNT
	jp	nc, unsetZ	; if selection >= device count, error
	push	af
	push	de
	push	hl

	ld	hl, blkDevTbl
	or	a		; cp 0
	jr	z, .end		; index is zero? don't loop
	push	bc		; <|
	ld	b, a		;  |
.loop:				;  |
	ld	a, 4		;  |
	call	addHL		;  |
	djnz	.loop		;  |
	pop	bc		; <|
.end:
	call	blkSet
	pop	hl
	pop	de
	pop	af
	cp	a	; ensure Z
	ret

; Setup blkdev handle in (DE) using routines at (HL).
blkSet:
	push	af
	push	de
	push	hl
	push	bc

	ld	bc, 4
	ldir
	; Initialize pos
	ld	b, 4
	xor	a
	ex	de, hl
	call	fill

	pop	bc
	pop	hl
	pop	de
	pop	af
	ret

_blkInc:
	ret	nz		; don't advance when in error condition
	push	af
	push	hl
	ld	a, BLOCKDEV_SEEK_FORWARD
	ld	hl, 1
	call	_blkSeek
	pop	hl
	pop	af
	ret

; Reads one byte from selected device and returns its value in A.
; Sets Z according to whether read was successful: Set if successful, unset
; if not.
blkGetB:
	push	ix
	ld	ix, BLOCKDEV_SEL
	call	_blkGetB
	pop	ix
	ret
_blkGetB:
	push	hl
	push	de
	call	_blkTell
	call	callIXI
	pop	de
	pop	hl
	jr	_blkInc		; advance and return

; Writes byte in A in current position in the selected device. Sets Z according
; to whether the operation was successful.
blkPutB:
	push	ix
	ld	ix, BLOCKDEV_SEL
	call	_blkPutB
	pop	ix
	ret
_blkPutB:
	push	ix
	push	hl
	push	de
	call	_blkTell
	inc	ix	; make IX point to PutB
	inc	ix
	call	callIXI
	pop	de
	pop	hl
	pop	ix
	jr	_blkInc		; advance and return

; Reads B chars from blkGetB and copy them in (HL).
; Sets Z if successful, unset Z if there was an error.
blkRead:
	push	ix
	ld	ix, BLOCKDEV_SEL
	call	_blkRead
	pop	ix
	ret
_blkRead:
	push	hl
	push	bc
.loop:
	call	_blkGetB
	jr	nz, .end	; Z already unset
	ld	(hl), a
	inc	hl
	djnz	.loop
	cp	a	; ensure Z
.end:
	pop	bc
	pop	hl
	ret

; Writes B chars to blkPutB from (HL).
; Sets Z if successful, unset Z if there was an error.
blkWrite:
	push	ix
	ld	ix, BLOCKDEV_SEL
	call	_blkWrite
	pop	ix
	ret
_blkWrite:
	push	hl
	push	bc
.loop:
	ld	a, (hl)
	call	_blkPutB
	jr	nz, .end	; Z already unset
	inc	hl
	djnz	.loop
	cp	a	; ensure Z
.end:
	pop	bc
	pop	hl
	ret

; Seeks the block device in one of 5 modes, which is the A argument:
; 0 : Move exactly to X, X being the HL/DE argument.
; 1 : Move forward by X bytes, X being the HL argument (no DE)
; 2 : Move backwards by X bytes, X being the HL argument (no DE)
; 3 : Move to the end
; 4 : Move to the beginning

; Set position of selected device to the value specified in HL (low) and DE
; (high). DE is only used for mode 0.
;
; When seeking to an out-of-bounds position, the resulting position will be
; one position ahead of the last valid position. Therefore, GetB after a seek
; to end would always fail.
;
; If the device is "growable", it's possible that seeking to end when calling
; PutB doesn't necessarily result in a failure.
blkSeek:
	push	ix
	ld	ix, BLOCKDEV_SEL
	call	_blkSeek
	pop	ix
	ret
_blkSeek:
	cp	BLOCKDEV_SEEK_FORWARD
	jr	z, .forward
	cp	BLOCKDEV_SEEK_BACKWARD
	jr	z, .backward
	cp	BLOCKDEV_SEEK_BEGINNING
	jr	z, .beginning
	cp	BLOCKDEV_SEEK_END
	jr	z, .end
	; all other modes are considered absolute
	ld	(ix+4), e
	ld	(ix+5), d
	ld	(ix+6), l
	ld	(ix+7), h
	ret
.forward:
	push	bc		; <-|
	push	hl		; <||
	ld	l, (ix+6)	;  || low byte
	ld	h, (ix+7)	;  ||
	pop	bc		; <||
	add	hl, bc		;   |
	pop	bc		; <-|
	ld	(ix+6), l
	ld	(ix+7), h
	ret	nc		; no carry? no need to adjust high byte
	; carry, adjust high byte
	inc	(ix+4)
	ret	nz
	inc	(ix+5)
	ret
.backward:
	and	a		; clear carry
	push	bc		; <-|
	push	hl		; <||
	ld	l, (ix+6)	;  || low byte
	ld	h, (ix+7)	;  ||
	pop	bc		; <||
	sbc	hl, bc		;   |
	pop	bc		; <-|
	ld	(ix+6), l
	ld	(ix+7), h
	ret	nc		; no carry? no need to adjust high byte
	ld	a, 0xff
	dec	(ix+4)
	cp	(ix+4)
	ret	nz
	; we decremented from 0
	dec	(ix+5)
	ret
.beginning:
	xor	a
	ld	(ix+4), a
	ld	(ix+5), a
	ld	(ix+6), a
	ld	(ix+7), a
	ret
.end:
	ld	a, 0xff
	ld	(ix+4), a
	ld	(ix+5), a
	ld	(ix+6), a
	ld	(ix+7), a
	ret

; Returns the current position of the selected device in HL (low) and DE (high).
blkTell:
	push	ix
	ld	ix, BLOCKDEV_SEL
	call	_blkTell
	pop	ix
	ret
_blkTell:
	ld	e, (ix+4)
	ld	d, (ix+5)
	ld	l, (ix+6)
	ld	h, (ix+7)
	ret

; This label is at the end of the file on purpose: the glue file should include
; a list of device routine table entries just after the include. Each line
; has 2 word addresses: GetB and PutB. An entry could look like:
; .dw     mmapGetB, mmapPutB
blkDevTbl: