;Extended CAT program for HC
;Tested on HC 2000
;(C) George Chirtoaca, george_chirtoaca(@)yahoo(.)com, 10 November 2006
;
;What it does:
;1. Reads disk catalog and saves a line in a table for each file, with info from the directory entries:
;file name, attributes, first & last allocation unit numbers used by file, record count;
;2. For each file in the table, completes file info in table with type and type-specific info read from file header:
;start line number and variable space and length for programs, start address and length for bytes,
;variable name & length for array files, exact length for text files;
;3. Walks the table and displays one line for each file;


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; Constants ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

START_ADDR		EQU	$8000		;Start address
RWTSDMA			EQU	30000		;RWTS DMA zone

;System variables for disk
DSTR1			EQU	$5CD6		;drive
FSTR1			EQU	$5CDC		;file name
NSTR1			EQU	$5CDA		;name length
HD11			EQU	$5CED		;BDOS argument
COPIES			EQU	$5CEF		;BDOS function

;File name stuff
NAMELEN			EQU	11			;name length
RO_POS			EQU	8			;read-only attribute position in name
SYS_POS			EQU	9			;system attribute position in name

;File types (first byte in header)
PROG_TYPE		EQU	0			;program
NUMB_TYPE		EQU	1			;number array
CHAR_TYPE		EQU	2			;char array
BYTE_TYPE		EQU	3			;bytes
TEXT_TYPE		EQU	4			;text

;Disk geometry stuff
DRIVE			EQU	0			;0 = use current drive
SPT				EQU	16			;sectors per track
SECT_SZ			EQU	256			;sector size in bytes
TRACK_CNT		EQU	80			;track count
HEAD_CNT		EQU	2			;disk face count
AU_SZ			EQU	2048		;allocation unit size in bytes
DIR_ENT_SZ		EQU	32			;directory entry size
DIR_TRK_CNT		EQU	1			;tracks rezerved for directory
SPAL			EQU	(AU_SZ/SECT_SZ);sectors per allocation unit
MAX_DIR_ENT		EQU	(SPT * DIR_TRK_CNT * SECT_SZ / DIR_ENT_SZ);maximum directory entries
MAX_AU_CNT		EQU	((TRACK_CNT * HEAD_CNT - DIR_TRK_CNT) * SPT * SECT_SZ)/AU_SZ ;max free allocation units
REC_SZ			EQU 128			;cp/m record size
FINISH_CHR		EQU $1A			;file terminator char

;RWTS routine commands
RWTS_CMD_POS	EQU	0			;position head
RWTS_CMD_READ	EQU	1			;read sector
RWTS_CMD_WRITE	EQU	2			;write sector
RWTS_CMD_FMT	EQU	4			;format all tracks

;File header offsets
HDR_TYPE		EQU	0
HDR_LEN			EQU 1
HDR_ADDR		EQU 3
HDR_VARLEN		EQU	5
HDR_LINE		EQU 7

;Disk cache table structure
CACHE_NAME		EQU 0
CACHE_TRACK		EQU	NAMELEN
CACHE_SECT		EQU 12
CACHE_REC_CNT	EQU 13
CACHE_LAST_AU	EQU	15
CACHE_LN_SZ		EQU 17


;BASIC disk channel structure
CH_RW_FLAG		EQU 11
CH_FCB			EQU	12
CH_DATA			EQU	50
CH_DMA			EQU CH_DATA - CH_FCB	;offset of DMA from start of FCB

;Extension structure (directory entry)
EXT_DEL_FLAG	EQU	0
EXT_NAME		EQU 1
EXT_IDX			EQU 12
EXT_S1			EQU 13
EXT_S2			EQU 14
EXT_RC			EQU	15
EXT_AU0			EQU	16
EXT_AU1			EQU	18
EXT_AU2			EQU	20
EXT_AU3			EQU	22
EXT_AU4			EQU	24
EXT_AU5			EQU	26
EXT_AU6			EQU	28
EXT_AU7			EQU	30
EXT_AU_CNT		EQU 8

;FCB structure
FCB_DRIVE		EQU 0
FCB_NAME		EQU EXT_NAME
FCB_EX_IDX		EQU EXT_IDX
FCB_S1			EQU EXT_S1
FCB_S2			EQU EXT_S2
FCB_RC			EQU	EXT_RC
FCB_AU			EQU	EXT_AU0
FCB_CR			EQU	32
FCB_R0			EQU 33
FCB_R1			EQU 34
FCB_R2			EQU 35


VERT_LINE		EQU '|'			;Spectrum char for "|"


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; Main program ;;;;;;;;;;;;;;;;;;;;;;;;;;

	ORG START_ADDR

start:
	call Init
	call CreateChannel
	call SetDMA
	call Cat
	call DestroyChannel
	call DisplayFilesInfo
	call DisplayFreeSpace
	ret

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; Char routines ;;;;;;;;;;;;;;;;;;;;;;;;;

;Checks if a key is pressed
;Cy=1 if key is pressed
KbdHit:
	rst 08
	DEFB 32
	ret


;Return read char in A
ReadChar:
	rst 08
	DEFB 27
	ret


;Print a character
;A=char to print
PrintChar:
	rst 08
	DEFB 28
	ret


;Print a string
;HL=str. addr.
;B=len
PrintStr:
	ld a, (hl)
	push hl
		call PrintChar
	pop hl
	inc hl
	djnz PrintStr
	ret


;Insert IF1 variables
InitVars:
	rst 08
	DEFB 49
	ret


Init:
	call InitVars

	ld hl, 1
	ld (FileIdx), hl				;reset file index counter


	ld hl, MAX_AU_CNT
	ld (FreeAUCnt), hl


	ld hl, FileMask
	ld b, DRIVE					;drive
	ld c, NAMELEN				;name len

	ret

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; Disk routines ;;;;;;;;;;;;;;;;;;;;;;;;;


;Create a disk channel for BDOS access (does not open the file)
;IN: HL=name addr, B=drive, C=name len
;OUT: IX=FCB
CreateChannel:
	ld (FSTR1), hl
	ld h,0
	ld l,b
	ld (DSTR1), hl
	ld l,c
	ld (NSTR1), hl
	rst 08
	DEFB 55
	ld bc, CH_FCB			;adjust to get cp/m fcb
	add ix, bc
	ret


;Destroy a BDOS channel
;IN: IX=FCB
DestroyChannel:
	push bc
	ld bc, -CH_FCB			;adjust to get the basic channel
	add ix, bc
	rst 08
	DEFB 56
	pop bc
	ret


;Generic BDOS call
;IX=arg, A=function
BDOS:
	ld (HD11), ix
	ld (COPIES), a
	rst 08
	DEFB 57
	ret


;FindFirst
;IX=fcb
FindFirst:
	ld a, 4
	jr BDOS


;FindNext
;IX=fcb
FindNext:
	ld a, 5
	jr BDOS


;Set DMA address for BDOS into the already alocated (and unused) 256 bytes channel buffer
;IX=fcb
SetDMA:
	ld bc, CH_DMA		;offset into alocated data area of the channel (256 bytes)
	push ix
		add ix, bc
		ld a, 13
		call BDOS
	pop ix
	ret

;ReadWriteTrackSector
;A=command: 0, 1, 2, 4
RWTS:
	ld (RWTSCmd), a
	ld hl, RWTSParams
	ld (HD11), hl
	rst 08
	DEFB 58
	ret


;D = sector, E = track
ReadSector:
	ld (RWTSTrack), de
	ld a, RWTS_CMD_POS + RWTS_CMD_READ
	call RWTS
	ret


WaitKey:
	or a
	call KbdHit
	jr c, WaitKey
	ret



;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; File System functions ;;;;;;;;;;;;;;;;;;;;;;;;;

;Calculates the exact file size for text files (no header). Other file types have a header, info is taken from there.
;IN: HL = last allocation number from file, DE = nr. of records used by file
;OUT: HL = AU cnt * AU_SZ + part. sect. cnt * SECT_SZ + partial sector usage (exact file size)
GetTextFileSize:
	ld a, e						;check is file has 0 size
	or d
	jr nz, Not0Size
	ld hl, 0
	ret

Not0Size:
	call AL2TS					;B = sect, C = track;
	push bc
	pop ix

	;get RecCnt * RecSz / AU Sz
	ld l, e
	ld h, d
	ld c, AU_SZ/REC_SZ
	call Div
	push hl					;hl = integral AU cnt

		;get last used sector offset in last alloc unit
		ld l, a
		xor a
		ld h, a
		ld c, SECT_SZ/REC_SZ
		call Div
		dec hl					;substract 1 from the sector count of file
		push hl

			ld a, l				;L = sector offset into alloc unit
			push ix
			pop de
			add a, d
			ld d, a				;sect = sect + sect ofs.
			call ReadSector

			;search for file terminator char
			ld hl, RWTSDMA
			ld a, FINISH_CHR
			ld bc, SECT_SZ
			cpir
			jr nz, FullSector
			inc bc				;patch to make BC have the real number of bytes <> 1Ah
FullSector:
;			ld hl, SECT_SZ
;			or a
;			sbc hl, bc
			ld hl, -SECT_SZ
			add hl, bc
			ld c, l
			ld b, h


		pop hl
		push bc
			;calc. partial sectors size
			ld a, l
			ld de, SECT_SZ
			call Mul
		pop bc
		add hl, bc

	pop de
	push hl

		ld a, e						;! this will have to work with max 8 * 32 alloc units
		ld de, AU_SZ
		call Mul
		ld c, l
		ld b, h

	pop hl
	add hl, bc

	ret



;B = count, A = char, DE = addr
Fill:
	ld (de), a
	inc de
	djnz Fill
	ret


;Displays info for each file. A table already contains lines of: FileName, file start track, file start sector, records count, last allocation unit
;IN: BC = file count
DisplayFilesInfo:
	;setup file info
	ld hl, Cache
NextFile:
	push bc
		push hl
			push hl
			pop ix
			ld e, (ix + NAMELEN)
			ld d, (ix + NAMELEN + 1)
			;check if is not an unallocated file
			ld a, d
			or e
			jr z, TxtFile

			call ReadSector
			ld a, (RWTSDMA)

			;find type
			cp BYTE_TYPE
			jr z, ByteType
			jr c, HasType
TxtFile:
			;it is a text file
			ld a, (TEXT_SYM)
			ld (FileType), a

			;fill line with blanks
			ld de, FileStart
			ld b, 5
			ld a, ' '
			call Fill

			ld a, VERT_LINE
			ld (FileStart+5), a

			;write text file len
			ld e, (ix + CACHE_REC_CNT)
			ld d, (ix + CACHE_REC_CNT + 1)

			ld l, (ix + CACHE_LAST_AU)
			ld h, (ix + CACHE_REC_CNT + 1)
			call GetTextFileSize
			jr StoreLen1

HasType:
			cp PROG_TYPE
			jr z, ProgramType

			;must be a data file, put variable name in first column
			ld l, 'v'
			ld h, 'a'
			ld (FileStart), hl
			ld l, 'r'
			ld h, ':'
			ld (FileStart+2), hl
			ld a, (RWTSDMA + 5)	;variable name
			and $7F				;clear bit 7 (probably set to signal not autorun)
			or  $40				;set to upercase (if not, the char is invalid)
			ld (FileStart + 4), a
			ld a, (RWTSDMA)
			cp NUMB_TYPE
			jr nz, CharType
			ld a, (NUMB_SYM)
			jr StoreArrType
CharType:
			ld a, (CHAR_SYM)
StoreArrType:
			ld (FileType), a
			jr StoreLen
ByteType:
			ld d, a
			ld hl, (RWTSDMA + 3);addr
			ld a, (BYTE_SYM)
			ld (FileType), a
			ld a, d
			jr StoreStart
ProgramType:
			ld a, (PROG_SYM)
			ld (FileType), a
			ld hl, (RWTSDMA+7)	;line
			bit 7, h
			jr nz, NotAutoRun
			jr StoreStart
NotAutoRun:
			ld b, 5
			ld de, FileStart
			ld a, ' '
			call Fill
			jr StoreLen
StoreStart:
			ld de, FileStart
			call N2ASCII
StoreLen:
			ld hl, (RWTSDMA + 1);get len
StoreLen1:
			ld de, FileLen
			call N2ASCII
EndTypeCheck:
			;store file number
			ld hl, (FileIdx)
			ld de, DECNum
			push hl
				call N2ASCII
			pop hl

			ld hl, FileIdx
			inc (hl)

			ld hl, DECNum + 2
			ld de, FileNo
			ld bc, 3
			ldir

		pop hl
		push hl
		pop ix
		;take care of attributes
		call CheckRO
		call CheckSYS

		;put file name in table
		ld de, FileName
		ld bc, NAMELEN
		ldir
		push hl
			;display line
			ld hl, DisplayedLine
			ld b, DisplayedLineEnd-DisplayedLine	;how many chars on one line
			call PrintStr

			call WaitKey

		pop hl
		ld bc, CACHE_LN_SZ - NAMELEN
		add hl, bc		;skip the bytes after the name (track & sector & record count & last alloc unit)

	pop bc
	dec bc				;dec. file counter
	ld a, b
	or c

	jp nz, NextFile
DispEnd:
	ret


;Checks the read-only flag and resets it, saving result in RO variable
CheckRO:
;IX=file name
	bit 7, (ix+RO_POS)
	jr z, NotRO
	ld a,  'R'
	ld (RO), a
	res 7, (ix+RO_POS)
	ret
NotRO:
	ld a, '.'
	ld (RO), a
	ret


;Checks the system flag and resets it, saving result in SYS variable
CheckSYS:
;IX=file name
	bit 7, (ix+SYS_POS)
	jr z, NotSYS
	ld a, 'H'
	ld (SYS), a
	res 7, (ix+SYS_POS)
	ret
NotSYS:
	ld a, '.'
	ld (SYS), a
	ret


;Allocation unit no. to track/sector
;Formula: T=(AllocUnit*SPAL)/SPT; Sect=T mod SPT; Track=T/2 (2 disk faces); Head=T mod 2
;IN:  HL=alloc. unit no.
;OUT: B=sector; C=track (head is determined by the sector number)
AL2TS:
	ld c, SPT/SPAL
	call Div					;A = sector
	push af
		ld c, HEAD_CNT
		call Div				;L = track, A = head (0 or 1)

		ld c, l
		ld b, 0
		or a
		jr z, Track0
		ld b, SPT
Track0:
	pop af
	or a
	jr z, FirstAU
	ld a, SPAL
FirstAU:
	add a, b
	ld  b, a

	ret



;Checks the allocated units number used in extension
;IN:	IX = extension addr
;OUT:	B = no. of allocation units used
;		C = no. of records used in ext.
;		HL = first alloc. unit no.
;		DE = last alloc. unit no.
CheckExtAlloc:
	push ix
		ld bc, EXT_RC
		add ix, bc
		ld c, (ix)			;save rec. no.
		inc ix
		ld l, (ix)
		ld h, (ix + 1)
		ld b, EXT_AU_CNT
CheckAU:
		ld a, (ix)
		or (ix + 1)
		jr z, CheckAUEnd
		ld e, (ix)
		ld d, (ix + 1)
		inc ix
		inc ix
		djnz CheckAU
CheckAUEnd:
		ld a, EXT_AU_CNT
		sub b
		ld b, a
	pop ix
	ret


;Decreases the free allocation units number, in order to calculate free space
;IN:	BC =  used alloc units
UpdateFreeAU:
	ld hl, (FreeAUCnt)
	or a
	sbc hl, bc
	ld (FreeAUCnt), hl
	ret


;Read directory and create a table (name - 11B, track - 1B, sector - 1B, record count - 2B, last alloc. unit - 2B)
;IN:	IX = fcb
;OUT:	BC = file count
Cat:
	;put "?" in extension index to signal we want all directory entries
	ld a, '?'
	ld (ix + FCB_EX_IDX), a

	call FindFirst
	ld de, Cache
	ld bc, 0					;file counter
	push bc
CatNext:
		inc a
		jp z, CatEnd			;return if A=FFh
		dec a
		;prepare found name for displaying
		push ix
			ld bc, CH_DMA
			add ix, bc
			push ix
			pop hl

			;A is offset into the buffer that has the 32 byte dir. entries
			ld b, 5
Rotate:
			rla
			djnz Rotate
			ld c, a
			add hl, bc			;hl=file name -1

			push hl
			pop ix
			ld a, (ix + EXT_IDX)
			;cp 0				;if not the first extension, handle below
			or a
			jr nz, NextExt

FirstExt:
			;put name in cache
			push de
				inc hl						;HL = file name
				ld bc, NAMELEN
				ldir						;copy name in the table

				;allocation units
				call CheckExtAlloc
				push de						;save last alloc unit
					push bc					;save AU & rec. cnt
						push hl				;save first alloc number
							;calculate free AUs
							ld c, b
							ld b, 0
							call UpdateFreeAU
						pop hl
						call AL2TS			;C=track, B=sect
					pop hl
				pop de
			pop ix

			;save cache line
			ld (ix + CACHE_TRACK), c		;save track & sector
			ld (ix + CACHE_TRACK + 1), b

			xor a
			ld h, a
			ld (ix + CACHE_REC_CNT), l	;l = record count
			ld (ix + CACHE_REC_CNT + 1), h

			ld (ix + CACHE_LAST_AU), e	;de = last alloc unit
			ld (ix + CACHE_LAST_AU + 1), d

			;advance DE
			ld bc, CACHE_LN_SZ
			push ix
			pop hl
			add hl, bc
			ld e, l
			ld d, h
		pop ix

	pop bc
		inc bc
	push bc

	jr CatCont


NextExt:
			push de
				call CheckExtAlloc
				push de						;DE == last alloc no.
					push bc					;C == rec. cnt

						push ix
						pop hl
						inc hl
						call GetCacheLineByName			;DE == cache line for file name
						push de
						pop ix

					pop bc
				pop de


				;update record count and last AU
				push bc
					ld (ix + CACHE_LAST_AU), e
					ld (ix + CACHE_LAST_AU + 1), d

					ld l, (ix + CACHE_REC_CNT)
					ld h, (ix + CACHE_REC_CNT + 1)
					ld b, 0
					add hl, bc
					ld (ix + CACHE_REC_CNT), l
					ld (ix + CACHE_REC_CNT + 1), h
				pop bc


				;update free alloc units
				ld c, b
				ld b, 0
				call UpdateFreeAU
			pop de
		pop ix

CatCont:
		;find next file
		push de
			call FindNext
		pop de
		jp CatNext

CatEnd:
	pop bc
	ret


;Compare string at HL with the one at DE, max length B
;IN: HL, DE = addr. of strings to compare, B = max. length of strings to compare
;OUT: z flag, set = match, reset = mismatch
StrCmp:
	push hl
	push de
Compare:
		ld a, (de)
		cp (hl)
		;cpi?
		jr nz, MisMatch
		inc hl
		inc de
		djnz Compare
MisMatch:
	pop de
	pop hl
	ret



;Returns the cache line for the specified file name
;IN: HL = name
GetCacheLineByName:
	ld de, Cache
NextLine:
	ld b, NAMELEN
	push de
		call StrCmp
	pop de
	ret z

	push hl
		ld hl, CACHE_LN_SZ
		add hl, de
		;ld e, l
		;ld d, h
		ex de, hl
	pop hl
	jr NextLine



;Returns the free space on disk in KB
;OUT: HL = free KB
GetFreeSpace:
	ld hl, (FreeAUCnt)
;	add hl, hl			;return FreeAUCnt*2, cos our alloc unit is 2KB in size
	ld a, AU_SZ/1024
	call Mul			;HL=FreeAuCnt * (AU_SZ/1024) = free space in KB
	ret


;Displays free space
DisplayFreeSpace:
	call GetFreeSpace

	ld de, DECNum
	call N2ASCII

	ld hl, DECNum + 2
	ld de, FreeSpaceAsc
	ld bc, 3
	ldir

	ld hl, FreeSpaceMsg
	ld b, FreeSpaceMsgEnd - FreeSpaceMsg
	call PrintStr
	ret


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; Number routines ;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;Converts the number in HL to ASCII
N2ASCII:
	push de
		call N2ASCII_DEC
	pop de

StrippLeading0:
	ld bc, $0400
	;ld c, 0
StrippLoop:
	ld a, (de)
	cp '0'
	jr nz, MarkNon0
	ld a, c
	;cp 0
	or a
	jr nz, NextChar
	ld a, ' '
	ld (de), a
	jr NextChar
MarkNon0:
	inc c
NextChar:
	inc de
	djnz StrippLoop
	ret


;The folowing 3 routines where inspired or taken from: Milos "baze" Bazelides, baze@stonline.sk
;http://map.tni.nl/sources/external/z80bits.html


;Converts the number in HL to ASCII in decimal string at DE
N2ASCII_DEC:
	ld bc, -10000
	call DigitLoop
	ld bc, -1000
	call DigitLoop
	ld bc, -100
	call DigitLoop
	ld bc, -10
	call DigitLoop
	ld bc, -1

DigitLoop:
	ld	a, '0' - 1
DivNrLoop:
	inc	a			;increase reminder
	add	hl, bc		;substract divizor
	jr	c, DivNrLoop	;still dividing?
	sbc	hl, bc		;nope, restore

	ld (de), a
	inc de
	ret


;Input: A = Multiplier, DE = Multiplicand
;Output: A:HL = Product
Mul:
	ld hl, 0
	ld bc, $0700

	add	a, a		; optimised 1st iteration
	jr	nc, MulLoop
	ld	h, d
	ld	l, e

MulLoop:
	add	hl,hl
	rla
	jr	nc, NoAdd
	add	hl,de
	adc	a,c
NoAdd:
	djnz MulLoop

	ret


;Input: HL = Dividend, C = Divisor
;Output: HL = Quotient, A = Remainder
Div:
	xor a
	ld b, 16

DivLoop:
	add	hl,hl
	rla
	cp	c
	jr	c, NoSub
	sub	c
	inc	l
NoSub:
	djnz DivLoop

	ret


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; Variables ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;


DECNum			DEFB	0,0,0,0,0	;number conversion scratchpad
FreeAUCnt		DEFW	MAX_AU_CNT	;free allocation unit counter
FileIdx			DEFW	1			;file index for display


FreeSpaceMsg:
				DEFM	"Free: "
FreeSpaceAsc	DEFB	"    KB", 13
FreeSpaceMsgEnd:


;the search mask
FileMask		DEFM	"???????????"


;storage space for one printed line
DisplayedLine:
FileNo			DEFB	"  1", VERT_LINE
FileType		DEFB	' ', VERT_LINE
FileName		DEFM	"???????????", VERT_LINE
RO				DEFB	' '
SYS				DEFB	' ', VERT_LINE
FileStart		DEFM	"00000", VERT_LINE
FileLen			DEFM	"00000"
				DEFB	13
DisplayedLineEnd:


;Symbols for file types
FileTypeSymbols:
PROG_SYM		DEFB	'P'			;program
NUMB_SYM		DEFB	'N'			;number array
CHAR_SYM		DEFB	'C'			;char array
BYTE_SYM		DEFB	'B'			;bytes
TEXT_SYM		DEFB	'T'			;text


;RWTS routine I/O block
RWTSParams:
				DEFB	01
				DEFB	DRIVE
				DEFB	0
RWTSTrack		DEFB	0
RWTSSector		DEFB	0
				DEFW	RWTSDMA
				DEFW	$2932
				DEFW	ParamTbl		;$1f2a
RWTSCmd			DEFB	RWTS_CMD_READ
RWTSRes			DEFB	0
				DEFB	0, 0, 0, 0, 0, 0


;drive parameters table
ParamTbl:
DrvType			DEFB	1
StepRate		DEFB	$0D
HeadLoad		DEFB	$23
SpinDelay		DEFB	$64
InterlvTbl		DEFW	$1F30

;here goes the cache table
Cache:
.END
