;       SEQUENTIAL FILE I/O LIBRARY
;
FILERR  SET     0000H   ;REBOOT AFTER ERROR
@BDOS   EQU     0005H   ;BDOS ENTRY POINT
@TFCB   EQU     005CH   ;DEFAULT FILE CONTROL BLOCK
@TBUF   EQU     0080H   ;DEFAULT BUFFER ADDRESS
;
;       BDOS FUNCTIONS
@MSG    EQU     9       ;SEND MESSAGE
@OPN    EQU     15      ;FILE OPEN
@CLS    EQU     16      ;FILE CLOSE
@DIR    EQU     17      ;DIRECTORY SEARCH
@DEL    EQU     19      ;FILE DELETE
@FRD    EQU     20      ;FILE READ OPERATION
@FWR    EQU     21      ;FILE WRITE OPERATION
@MAK    EQU     22      ;FILE MAKE
@REN    EQU     23      ;FILE RENAME
@DMA    EQU     26      ;SET DMA ADDRESS
;
@SECT   EQU     128     ;SECTOR SIZE
EOF     EQU     1AH     ;END OF FILE
CR      EQU     0DH     ;CARRIAGE RETURN
LF      EQU     0AH     ;LINE FEED
TAB     EQU     09H     ;HORIZONTAL TAB
;
@KEY    EQU     1       ;KEYBOARD
@CON    EQU     2       ;CONSOLE DISPLAY
@RDR    EQU     3       ;READER
@PUN    EQU     4       ;PUNCH
@LST    EQU     5       ;LIST DEVICE
;
;       KEYWORDS FOR "FILE" MACRO
INFILE  EQU     1       ;INPUT FILE
OUTFILE EQU     2       ;OUTPUTFILE
SETFILE EQU     3       ;SETUP NAME ONLY
;
;       THE FOLLOWING MACROS DEFINE SIMPLE SEQUENTIAL
;       FILE OPERATIONS:
;
FILLNAM MACRO   FC,C
;;      FILL THE FILE NAME/TYPE GIVEN BY FC FOR C CHARACTERS
@CNT    SET     C       ;;MAX LENGTH
       IRPC    ?FC,FC  ;;FILL EACH CHARACTER
;;      MAY BE END OF COUNT OR NUL NAME
       IF      @CNT=0 OR NUL ?FC
       EXITM
       ENDIF
       DB      '&?FC'  ;;FILL ONE MORE
@CNT    SET     @CNT-1  ;;DECREMENT MAX LENGTH
       ENDM            ;;OF IRPC ?FC
;;
;;      PAD REMAINDER
       REPT    @CNT    ;;@CNT IS REMAINDER
       DB      ' '     ;;PAD ONE MORE BLANK
       ENDM            ;;OF REPT
       ENDM
;
FILLDEF MACRO   FCB,?FL,?LN
;;      FILL THE FILE NAME FROM THE DEFAULT FCB
;;      FOR LENGTH ?LN (9 OR 12)
       LOCAL   PSUB
       JMP     PSUB    ;;JUMP PAST THE SUBROUTINE
@DEF:   ;;THIS SUBROUTINE FILLS FROM THE TFCB (+16)
       MOV     A,M     ;;GET NEXT CHARACTER TO A
       STAX    D       ;;STORE TO FCB AREA
       INX     H
       INX     D
       DCR     C       ;;COUNT LENGTH DOWN TO 0
       JNZ     @DEF
       RET
;;      END OF FILL SUBROUTINE
PSUB:
FILLDEF MACRO   ?FCB,?F,?L
       LXI     H,@TFCB+?F      ;;EITHER @TFCB OR @TFCB+16
       LXI     D,?FCB
       MVI     C,?L            ;;LENGTH = 9,12
       CALL    @DEF
       ENDM
       FILLDEF FCB,?FL,?LN
       ENDM
;
FILLNXT MACRO
;;      INITIALIZE BUFFER AND DEVICE NUMBERS
@NXTB   SET     0       ;;NEXT BUFFER LOCATION
@NXTD   SET     @LST+1  ;;NEXT DEVICE NUMBER
FILLNXT MACRO
       ENDM
       ENDM
;
FILLFCB MACRO   FID,DN,FN,FT,BS,BA
;;      FILL THE FILE CONTROL BLOCK WITH DISK NAME
;;      FID IS AN INTERNAL NAME FOR THE FILE,
;;      DN IS THE DRIVE NAME (A,B..), OR BLANK
;;      FN IS THE FILE NAME, OR BLANK
;;      FT IS THE FILE TYPE
;;      BS IS THE BUFFER SIZE
;;      BA IS THE BUFFER ADDRESS
       LOCAL   PFCB
;;
;;      SET UP THE FILE CONTROL BLOCK FOR THE FILE
;;      LOOK FOR FILE NAME = 1 OR 2
@C      SET     1       ;;ASSUME TRUE TO BEGIN WITH
       IRPC    ?C,FN   ;;LOOK THROUGH CHARACTERS OF NAME
       IF      NOT ('&?C' = '1' OR '&?C' = '2')
@C      SET     0       ;;CLEAR IF NOT 1 OR 2
       ENDM
;;      @C IS TRUE IF FN = 1 OR 2 AT THIS POINT
       IF      @C      ;;THEN FN = 1 OR 2
;;      FILL FROM DEFAULT AREA
       IF      NUL FT  ;;TYPE SPECIFIED?
@C      SET     12      ;;BOTH NAME AND TYPE
       ELSE
@C      SET     9       ;;NAME ONLY
       ENDIF
       FILLDEF FCB&FID,(FN-1)*16,@C    ;;TO SELECT THE FCB
       JMP     PFCB    ;;PAST FCB DEFINITION
       DS      @C      ;;SPACE FOR DRIVE/FILENAME/TYPE
       FILLNAM FT,12-@C        ;;SERIES OF DB'S
       ELSE
       JMP     PFCB    ;;PAST INITIALIZED FCB
       IF      NUL DN
       DB      0       ;;USE DEFAULT DRIVE IF NAME IS ZERO
       ELSE
       DB      '&DN'-'A'+1     ;;USE SPECIFIED DRIVE
       ENDIF
       FILLNAM FN,8    ;;FILL FILE NAME
;;      NOW GENERATE THE FILE TYPE WITH PADDED BLANKS
       FILLNAM FT,3    ;;AND THREE CHARACTER TYPE
       ENDIF
FCB&FID EQU     $-12    ;;BEGINNING OF THE FCB
       DB      0       ;;EXTENT FIELD 00 FOR SETFILE
;;      NOW DEFINE THE 3 BYTE FIELD, AND DISK MAP
       DS      20      ;;X,X,RC,DM0...DM15,CR FIELDS
;;
       IF      FID&TYP<=2      ;;IN/OUTFILE
;;      GENERATE CONSTANTS FOR INFILE/OUTFILE
       FILLNXT         ;;@NXTB=0 ON FIRST CALL
       IF      BS+0<@SECT
;;      BS NOT SUPPLIED, OR TOO SMALL
@BS     SET     @SECT   ;;DEFAULT TO ONE SECTOR
       ELSE
;;      COMPUTE EVEN BUFFER ADDRESS
@BS     SET     (BS/@SECT)*@SECT
       ENDIF
;;
;;      NOW DEFINE BUFFER BASE ADDRESS
       IF      NUL BA
;;      USE NEXT ADDRESS AFTER @NXTB
FID&BUF SET     BUFFERS+@NXTB
;;      COUNT PAST THIS BUFFER
@NXTB   SET     @NXTB+@BS
       ELSE
FID&BUF SET     BA
       ENDIF
;;      FID&BUF IS BUFFER ADDRESS
FID&ADR:
       DW      FID&BUF
;;
FID&SIZ EQU     @BS     ;;LITERAL SIZE
FID&LEN:
       DW      @BS     ;;BUFFER SIZE
FID&PTR:
       DS      2       ;;SET IN INFILE/OUTFILE
;;      SET DEVICE NUMBER
@&FID   SET     @NXTD   ;;NEXT DEVICE
@NXTD   SET     @NXTD+1
       ENDIF   ;;OF FID&TYP<=2 TEST
PFCB:   ENDM
;
FILE    MACRO   MD,FID,DN,FN,FT,BS,BA
;;      CREATE FILE USING MODE MD:
;;              INFILE = 1      INPUT FILE
;;              OUTFILE = 2     OUTPUT FILE
;;              SETFILE = 3     SETUP FCB
;;      (SEE FILLFCB FOR REMAINING PARAMETERS)
       LOCAL   PSUB,MSG,PMSG
       LOCAL   PND,EOD,EOB,PNC
;;      CONSTRUCT THE FILE CONTROL BLOCK
;;
FID&TYP EQU     MD      ;;SET MODE FOR LATER REF'S
       FILLFCB FID,DN,FN,FT,BS,BA
       IF      MD=3    ;;SETUP FCB ONLY, SO EXIT
       EXITM
       ENDIF
;;      FILE CONTROL BLOCK AND RELATED PARAMETERS
;;      ARE CREATED INLINE, NOW CREATE IO FUNCTION
       JMP     PSUB    ;;PAST INLINE SUBROUTINE
       IF      MD=1    ;;INPUT FILE
GET&FID:
       ELSE
PUT&FID:
       PUSH    PSW     ;;SAVE OUTPUT CHARACTER
       ENDIF
       LHLD    FID&LEN ;;LOAD CURRENT BUFFER LENGTH
       XCHG            ;;DE IS LENGTH
       LHLD    FID&PTR ;;LOAD NEXT TO GET/PUT TO HL
       MOV     A,L     ;;COMPUTE CUR-LEN
       SUB     E
       MOV     A,H
       SBB     D       ;;CARRY IF NEXT<LENGTH
       JC      PNC     ;;CARRY IF LEN GTR CURRENT
;;      END OF BUFFER, FILL/EMPTY BUFFERS
       LXI     H,0
       SHLD    FID&PTR ;;CLEAR NEXT TO GET/PUT
PND:
;;      PROCESS NEXT DISK SECTOR:
       XCHG            ;;FID&PTR TO DE
       LHLD    FID&LEN ;;DO NOT EXCEED LENGTH
;;      DE IS NEXT TO FILL/EMPTY, HL IS MAX LEN
       MOV     A,E     ;;COMPUTE NEXT-LEN
       SUB     L       ;;TO GET CARRY IF MORE
       MOV     A,D
       SBB     H       ;;TO FILL
       JNC     EOB
;;      CARRY GEN'ED, HENCE MORE TO FILL/EMPTY
       LHLD    FID&ADR ;;BASE OF BUFFERS
       DAD     D       ;;HL IS NEXT BUFFER ADDR
       XCHG
       MVI     C,@DMA  ;;SET DMA ADDRESS
       CALL    @BDOS   ;;DMA ADDRESS IS SET
       LXI     D,FCB&FID       ;;FCB ADDRESS TO DE
       IF      MD=1    ;;READ BUFFER FUNCTION
       MVI     C,@FRD  ;;FILE READ FUNCTION
       ELSE
       MVI     C,@FWR  ;;FILE WRITE FUNCTION
       ENDIF
       CALL    @BDOS   ;;RD/WR TO/FROM DMA ADDRESS
       ORA     A       ;;CHECK RETURN CODE
       JNZ     EOD     ;;END OF FILE/DISK?
;;      NOT END OF FILE/DISK, INCREMENT LENGTH
       LXI     D,@SECT ;;SECTOR SIZE
       LHLD    FID&PTR ;;NEXT TO FILL
       DAD     D
       SHLD    FID&PTR ;;BACK TO MEMORY
       JMP     PND     ;;PROCESS ANOTHER SECTOR
;;
EOD:
;;      END OF FILE/DISK ENCOUNTERED
       IF      MD=1    ;;INPUT FILE
       LHLD    FID&PTR ;;LENGTH OF BUFFER
       SHLD    FID&LEN ;;RESET LENGTH
       ELSE
;;      FATAL ERROR, END OF DISK
       LOCAL   EMSG
       MVI     C,@MSG  ;;WRITE THE ERROR
       LXI     D,EMSG
       CALL    @BDOS   ;;ERROR TO CONSOLE
       POP     PSW     ;;REMOVE STACKED CHARACTER
       JMP     FILERR  ;;USUALLY REBOOTS
EMSG:   DB      CR,LF
       DB      'DISK FULL: &FID'
       DB      '$'
       ENDIF
;;
EOB:
;;      END OF BUFFER, RESET DMA AND POINTER
       LXI     D,@TBUF
       MVI     C,@DMA
       CALL    @BDOS
       LXI     H,0
       SHLD    FID&PTR ;;NEXT TO GET
;;
PNC:
;;      PROCESS THE NEXT CHARACTER
       XCHG            ;;INDEX TO GET/PUT IN DE
       LHLD    FID&ADR ;;BASE OF BUFFER
       DAD     D       ;;ADDRESS OF CHAR IN HL
       XCHG            ;;ADDRESS OF CHAR IN DE
       IF      MD=1    ;;INPUT PROCESSING DIFFERS
       LHLD    FID&LEN ;;FOR EOF CHECK
       MOV     A,L     ;;0000?
       ORA     H
       MVI     A,EOF   ;;END OF FILE?
       RZ              ;;ZERO FLAG IF SO
       LDAX    D       ;;NEXT CHAR IN ACCUM
       ELSE
;;      STORE NEXT CHARACTER FROM ACCUMULATOR
       POP     PSW     ;;RECALL SAVED CHAR
       STAX    D       ;;CHARACTER IN BUFFER
       ENDIF
       LHLD    FID&PTR ;;INDEX TO GET/PUT
       INX     H
       SHLD    FID&PTR ;;POINTER UPDATED
;;      RETURN WITH NON ZERO FLAG IF GET
       RET
;;
PSUB:   ;;PAST INLINE SUBROUTINE
       XRA     A               ;;ZERO TO ACC
       STA     FCB&FID+12      ;;CLEAR EXTENT
       STA     FCB&FID+32      ;;CLEAR CUR REC
       LXI     H,FID&SIZ       ;;BUFFER SIZE
       SHLD    FID&LEN         ;;SET BUFF LEN
       IF      MD=1    ;;INPUT FILE
       SHLD    FID&PTR ;;CAUSE IMMEDIATE READ
       MVI     C,@OPN  ;;OPEN FILE FUNCTION
       ELSE            ;;OUTPUT FILE
       LXI     H,0     ;;SET NEXT TO FILL
       SHLD    FID&PTR ;;POINTER INITIALIZED
       MVI     C,@DEL
       LXI     D,FCB&FID       ;;DELETE FILE
       CALL    @BDOS   ;;TO CLEAR EXISTING FILE
       MVI     C,@MAK  ;;CREATE A NEW FILE
       ENDIF
;;      NOW OPEN (IF INPUT), OR MAKE (IF OUTPUT)
       LXI     D,FCB&FID
       CALL    @BDOS   ;;OPEN/MAKE OK?
       INR     A       ;;255 BECOMES 00
       JNZ     PMSG
       MVI     C,@MSG  ;;PRINT MESSAGE FUNCTION
       LXI     D,MSG   ;;ERROR MESSAGE
       CALL    @BDOS   ;;PRINTED AT CONSOLE
       JMP     FILERR  ;;TO RESTART
MSG:    DB      CR,LF
       IF      MD=1    ;;INPUT MESSAGE
       DB      'NO &FID FILE'
       ELSE
       DB      'NO DIR SPACE: &FID'
       ENDIF
       DB      '$'
PMSG:
       ENDM
;
PUT     MACRO   DEV
;;      WRITE CHARACTER FROM ACCUM TO DEVICE
       IF      @&DEV <= @LST
;;      SIMPLE OUTPUT
       PUSH    PSW     ;;SAVE CHARACTER
       MVI     C,@&DEV ;;WRITE CHAR FUNCTION
       MOV     E,A     ;;READY FOR OUTPUT
       CALL    @BDOS   ;;WRITE CHARACTER
       POP     PSW     ;;RESTORE FOR TESTING
       ELSE
       CALL    PUT&DEV
       ENDM
;
FINIS   MACRO   FID
;;      CLOSE THE FILE(S) GIVEN BY FID
       IRP     ?F,<FID>
;;      SKIP ALL BUT OUTPUT FILES
       IF      ?F&TYP=2
       LOCAL   EOB?,PEOF,MSG,PMSG
;;      WRITE ALL PARTIALLY FILLED BUFFERS
EOB?:   ;;ARE WE AT THE END OF A BUFFER?
       LHLD    ?F&PTR  ;;NEXT TO FILL
       MOV     A,L     ;;ON BUFFER BOUNDARY?
       ANI     (@SECT-1) AND 0FFH
       JNZ     PEOF    ;;PUT EOF IF NOT 00
       IF      @SECT>255
;;      CHECK HIGH ORDER BYTE ALSO
       MOV     A,H
       ANI     (@SECT-1) SHR 8
       JNZ     PEOF    ;;PUT EOF IF NOT 00
       ENDIF
;;      ARRIVE HERE IF END OF BUFFER, SET LENGTH
;;      AND WRITE ONE MORE BYTE TO CLEAR BUFFS
       SHLD    ?F&LEN  ;;SET TO SHORTER LENGTH
PEOF:   MVI     A,EOF   ;;WRITE ANOTHER EOF
       PUSH    PSW     ;;SAVE ZERO FLAG
       CALL    PUT&?F
       POP     PSW     ;;RECALL ZERO FLAG
       JNZ     EOB?    ;;NON ZERO IF MORE
;;      BUFFERS HAVE BEEN WRITTEN, CLOSE FILE
       MVI     C,@CLS
       LXI     D,FCB&?F        ;;READY FOR CALL
       CALL    @BDOS
       INR     A       ;;255 IF ERR BECOMES 00
       JNZ     PMSG
;;      FILE CANNOT BE CLOSED
       MVI     C,@MSG
       LXI     D,MSG
       CALL    @BDOS
       JMP     PMSG    ;;ERROR MESSAGE PRINTED
MSG:    DB      CR,LF
       DB      'CANNOT CLOSE &?F'
       DB      '$'
PMSG:
       ENDIF
       ENDM    ;;OF THE IRP
       ENDM
;
ERASE   MACRO   FID
;;      DELETE THE FILE(S) GIVEN BY FID
       IRP     ?F,<FID>
       MVI     C,@DEL
       LXI     D,FCB&?F
       CALL    @BDOS
       ENDM    ;;OF THE IRP
       ENDM
;
DIRECT  MACRO   FID
;;      PERFORM DIRECTORY SEARCH FOR FILE
;;      SETS ZERO FLAG IF NOT PRESENT
       LXI     D,FCB&FID
       MVI     C,@DIR
       CALL    @BDOS
       INR     A       ;00 IF NOT PRESENT
       ENDM
;
RENAME  MACRO   NEW,OLD
;;      RENAME FILE GIVEN BY "OLD" TO "NEW"
       LOCAL   PSUB,REN0
;;      INCLUDE THE RENAME SUBROUTINE ONCE
       JMP     PSUB
@RENS:  ;;RENAME SUBROUTINE, HL IS ADDRESS OF
       ;;OLD FCB, DE IS ADDRESS OF NEW FCB
       PUSH    H       ;;SAVE FOR RENAME
       LXI     B,16    ;;B=00,C=16
       DAD     B       ;;HL = OLD FCB+16
REN0:   LDAX    D       ;;NEW FCB NAME
       MOV     M,A     ;;TO OLD FCB+16
       INX     D       ;;NEXT NEW CHAR
       INX     H       ;;NEXT FCB CHAR
       DCR     C       ;;COUNT DOWN FROM 16
       JNZ     REN0
;;      OLD NAME IN FIRST HALF, NEW IN SECOND HALF
       POP     D       ;;RECALL BASE OF OLD NAME
       MVI     C,@REN  ;;RENAME FUNCTION
       CALL    @BDOS
       RET             ;;RENAME COMPLETE
PSUB:
RENAME  MACRO   N,O     ;;REDEFINE RENAME
       LXI     H,FCB&O ;;OLD FCB ADDRESS
       LXI     D,FCB&N ;;NEW FCB ADDRESS
       CALL    @RENS   ;;RENAME SUBROUTINE
       ENDM
       RENAME  NEW,OLD
       ENDM
;
GET     MACRO   DEV
;;      READ CHARACTER FROM DEVICE
       IF      @&DEV <= @LST
;;      SIMPLE INPUT
       MVI     C,@&DEV
       CALL    @BDOS
       ELSE
       CALL    GET&DEV
       ENDM
;