; 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: