;
; SYSLIB Module Name:  SSORT
; Author:  Richard Conn
; SYSLIB Version Number:  2.0
; Module Version Number:  1.0
; Module Entry Points:
;	SSBINI		SORT
; Module External References:
;	MOVEB		PRINT
;
;*
;*  EXTERNALS
;*
	EXT	MOVEB
	EXT	PRINT

;*
;*  BASIC EQUATES
;*
CPM	EQU	0	; CP/M WARM BOOT
BIOS	EQU	CPM+1	; BIOS BASE ADDRESS
BDOS	EQU	CPM+5	; BDOS ENTRY POINT
ESIZE	EQU	16	; NUMBER OF BYTES/ENTRY IN MEMORY BUFFER
BUFF	EQU	CPM+80H	; DEFAULT DMA BUFFER

;*
;*  SSBINI -- Initialize SSB
;*	This routine sets the value of FIRSTP and ORDER for the SORT
;* routine.  On Entry, HL pts to first available byte after user's
;* program (beginning of buffer area) and DE pts to an SSB.  On exit,
;* HL pts to first available byte for the user's in-memory data file
;* (same address as FIRSTP).  If the TPA is overflowed by the ORDER
;* table that would be generated by SORT, then the Z flag is returned set
;* (Z); if no error, NZ is returned.
;*	If the user has already loaded his data and just wants to
;* allocate an ORDER table, then he should have HL before calling SSBINI,
;* restore HL upon return from SSBINI, and then store HL into his FIRSTP
;* entry before calling SORT.
;*
SSBINI::
	PUSH	D	; SAVE REGS
	PUSH	B
	PUSH	H	; SAVE NEXT AVAILABLE BYTE ADDRESS
	XCHG		; MAKE HL PT TO USER'S SSB
	SHLD	USRSSB	; SAVE PTR TO USER'S SSB
	LXI	D,SSB	; COPY INTO MY SSB
	MVI	B,12	; 12 BYTES
	CALL	MOVEB
	POP	H	; GET NEXT AVAILABLE BYTE ADDRESS
	SHLD	ORDER	; SET PTR TO IT
	LHLD	RECNT	; GET NUMBER OF RECORDS
	MOV	A,L	; DOUBLE IT WITH ERROR CHECKING
	ADD	L
	MOV	L,A
	MOV	A,H
	ADC	H
	MOV	H,A
	JC	SSBOVFL	; OVERFLOW IF CARRY
	XCHG		; SIZE OF ORDER TABLE IN DE
	LHLD	ORDER	; BASE ADDRESS OF ORDER TABLE IN HL
	MOV	A,L	; ADD HL TO DE WITH ERROR CHECKING
	ADD	E
	MOV	L,A
	MOV	A,H
	ADC	D
	MOV	H,A	; HL IS NOW ADDRESS OF BYTE AFTER ORDER TABLE
	JC	SSBOVFL	; OVERFLOW IF CARRY
	SHLD	FIRSTP	; SET PTR TO NEXT AVAILABLE BYTE
	LHLD	USRSSB	; PT TO USER'S SSB
	XCHG		; ... IN DE
	LXI	H,SSB	; PT TO USER'S NEW SSB
	MVI	B,12	; COPY IT BACK
	CALL	MOVEB
	POP	B	; RESTORE REGS
	POP	D
	LHLD	FIRSTP	; PT TO NEXT AVAILABLE MEMORY ADDRESS
	MVI	A,0FFH	; SET NO ERROR CODE
	ORA	A
	RET
SSBOVFL:
	POP	B	; RESTORE REGS
	POP	D
	LHLD	ORDER	; RESTORE PTR TO USER'S BUFFER
	XRA	A	; ERROR CODE
	RET
USRSSB:
	DS	2	; BUFFER FOR ADDRESS OF USER'S SSB

;*
;*  SORT -- Sort memory-resident file of fixed-length records in an order
;*	specified by the user;  On entry, DE pts to a SORT Specification
;*	Block (SSB) which contains the following information:
;*
;*		Bytes 0&1:  Starting Address of File (1st byte of 1st record)
;*		Bytes 2&3:  Number of Records in the File
;*		Bytes 4&5:  Size of Each Record (in Bytes)
;*		Bytes 6&7:  Address of a Compare Routine Provided by the User
;*				This routine compares two records, one pted
;*				to by HL and the other pted to by DE; if the
;*				record pted to by DE is less in sorting order
;*				than that pted to by HL, this routine returns
;*				with Carry Set (C); if the records are equal
;*				in sorting order, this routine returns with
;*				Zero Set (Z); no registers asside from the PSW
;*				are to be affected by this routine
;*		Bytes 8&9:  Address of ORDER Buffer (RECNT*2 in size)
;*		Byte 10:  Flag; 0 means to use pointers, 0FFH means to not
;*				use pointers
;*		Byte 11:  Unused
;*
;*	SORT does not have any net affect on any registers
;*

;
;  SORT SPECIFICATION BLOCK (SSB) FOR USE BY SORT ROUTINE
;
SSB:
FIRSTP:
	DS	2	; POINTER TO FIRST RECORD IN FILE
RECNT:
	DS	2	; NUMBER OF RECORDS IN FILE
RECSIZ:
	DS	2	; SIZE OF EACH RECORD
CMPADR:
	DS	2	; ADDRESS OF COMPARE ROUTINE
ORDER:
	DS	2	; ADDRESS OF ORDER TABLE
SFLAG:
	DS	1	; USE POINTERS? 0=NO
	DS	1	; UNUSED, BUT RESERVED, AT THIS TIME

;
;  SORT AND POINTER BUFFERS
;
N:
	DS	2	; NUMBER OF RECS
GAP:
	DS	2	; VARIABLE GAP
J:
	DS	2	; REC PTR 1
JG:
	DS	2	; REC PTR 2
PTPTR:
	DS	2	; 2-LEVEL INDIRECT PTR
PTFIL:
	DS	2	; REC PTR

;*
;*  START OF SORT ROUTINE
;*
SORT::
	PUSH	H	; SAVE REGS
	PUSH	D
	PUSH	B
	PUSH	PSW
	XCHG		; PTR IN HL
	LXI	D,SSB	; COPY HIS SORT BLOCK INTO MINE FOR EASIER ACCESS
	MVI	B,12	; 12 BYTES LONG
	CALL	MOVEB
	LHLD	RECNT	; GET RECORD COUNT
	SHLD	N	; SET "N"
	MOV	B,H	; ... IN BC
	MOV	C,L

;*
;*  CHECK FOR TRIVIAL CASE - 0 OR 1; DON'T DO IT IF SO
;*
	MOV	A,B	; SEE IF BC=1
	ORA	A	; B=0?
	JNZ	SHELL	; DO SORT IF B<>0
	MOV	A,C	; C=0 OR 1?
	CPI	2
	JC	SEXIT

;*
;*  SHELL SORT --
;*    THIS SORT ROUTINE IS ADAPTED FROM "SOFTWARE TOOLS"
;*    BY KERNIGAN AND PLAUGHER, PAGE 106.  COPYRIGHT, 1976, ADDISON-WESLEY.
;*  ON ENTRY, BC=NUMBER OF ENTRIES
;*
SHELL:
	LDA	SFLAG	; USE POINTERS?
	ORA	A	; 0=NO
	JZ	SORT2
	LHLD	FIRSTP	; PT TO FIRST RECORD
	XCHG		; POINTER TO 1ST RECORD IN DE
	LHLD	ORDER	; PT TO ORDER TABLE
;*
;*  SET UP ORDER TABLE; HL PTS TO NEXT ENTRY IN ORDER TABLE, DE PTS TO NEXT
;*    REC IN FILE, BC = NUMBER OF RECS REMAINING
;*
SORT1:
	MOV	A,B	; DONE?
	ORA	C	; 0 IF SO
	JZ	SORT2
	DCX	B	; COUNT DOWN
	MOV	M,E	; STORE LOW-ORDER ADDRESS
	INX	H	; PT TO NEXT ORDER BYTE
	MOV	M,D	; STORE HIGH-ORDER ADDRESS
	INX	H	; PT TO NEXT ORDER ENTRY
	PUSH	H	; SAVE PTR
	LHLD	RECSIZ	; HL=NUMBER OF BYTES/ENTRY
	DAD	D	; PT TO NEXT DIR1 ENTRY
	XCHG		; DE PTS TO NEXT ENTRY
	POP	H	; GET PTR TO ORDER TABLE
	JMP	SORT1
;*
;*  THIS IS THE MAIN SORT LOOP FOR THE SHELL SORT IN "SOFTWARE TOOLS" BY K&P
;*
SORT2:
	LHLD	N	; NUMBER OF ITEMS TO SORT
	SHLD	GAP	; SET INITIAL GAP TO N FOR FIRST DIVISION BY 2

;*  FOR (GAP = N/2; GAP > 0; GAP = GAP/2)
SRTL0:
	ORA	A	; CLEAR CARRY
	LHLD	GAP	; GET PREVIOUS GAP
	MOV	A,H	; ROTATE RIGHT TO DIVIDE BY 2
	RAR
	MOV	H,A
	MOV	A,L
	RAR
	MOV	L,A

;*  TEST FOR ZERO
	ORA	H
	JZ	SDONE	; DONE WITH SORT IF GAP = 0

	SHLD	GAP	; SET VALUE OF GAP
	SHLD	I	; SET I=GAP FOR FOLLOWING LOOP

;*  FOR (I = GAP + 1; I <= N; I = I + 1)
SRTL1:
	LHLD	I	; ADD 1 TO I
	INX	H
	SHLD	I

;*  TEST FOR I <= N
	XCHG		; I IS IN DE
	LHLD	N	; GET N
	MOV	A,L	; COMPARE BY SUBTRACTION
	SUB	E
	MOV	A,H
	SBB	D	; CARRY SET MEANS I > N
	JC	SRTL0	; DON'T DO FOR LOOP IF I > N

	LHLD	I	; SET J = I INITIALLY FOR FIRST SUBTRACTION OF GAP
	SHLD	J

;*  FOR (J = I - GAP; J > 0; J = J - GAP)
SRTL2:
	LHLD	GAP	; GET GAP
	XCHG		; ... IN DE
	LHLD	J	; GET J
	MOV	A,L	; COMPUTE J - GAP
	SUB	E
	MOV	L,A
	MOV	A,H
	SBB	D
	MOV	H,A
	SHLD	J	; J = J - GAP
	JC	SRTL1	; IF CARRY FROM SUBTRACTIONS, J < 0 AND ABORT
	MOV	A,H	; J=0?
	ORA	L
	JZ	SRTL1	; IF ZERO, J=0 AND ABORT

;*  SET JG = J + GAP
	XCHG		; J IN DE
	LHLD	GAP	; GET GAP
	DAD	D	; J + GAP
	SHLD	JG	; JG = J + GAP

;*  IF (V(J) <= V(JG))
	CALL	ICOMPARE	; J IN DE, JG IN HL

;*  ... THEN BREAK
	JC	SRTL1

;*  ... ELSE EXCHANGE
	LHLD	J	; SWAP J, JG
	XCHG
	LHLD	JG
	CALL	ISWAP	; J IN DE, JG IN HL

;*  END OF INNER-MOST FOR LOOP
	JMP	SRTL2

;*
;*  SORT IS DONE -- RESTRUCTURE FILE IN SORTED ORDER IN PLACE
;*
SDONE:
	LDA	SFLAG	; USE POINTERS?
	ORA	A	; 0=NO
	JZ	SEXIT	; DONE IF NO POINTERS
	LHLD	RECNT	; NUMBER OF RECORDS
	SHLD	J	; SAVE COUNT IN J
	LHLD	ORDER	; PTR TO ORDERED POINTER TABLE
	SHLD	PTPTR	; SET PTR PTR
	LHLD	FIRSTP	; PTR TO UNORDERED FILE
	SHLD	PTFIL	; SET PTR TO FILE BUFFER

;*  FIND PTR TO NEXT FILE ENTRY
SRTDN:
	LHLD	J	; GET ENTRY COUNT
	MOV	B,H	; ... IN BC
	MOV	C,L
	LHLD	PTPTR	; PT TO REMAINING POINTERS
	XCHG		; ... IN DE
	LHLD	PTFIL	; HL PTS TO NEXT FILE ENTRY

;*  FIND PTR TABLE ENTRY
SRTDN1:
	LDAX	D	; GET CURRENT POINTER TABLE ENTRY VALUE
	INX	D	; PT TO HIGH-ORDER POINTER BYTE
	CMP	L	; COMPARE AGAINST FILE ADDRESS LOW
	JNZ	SRTDN2	; NOT FOUND YET
	LDAX	D	; LOW-ORDER BYTES MATCH -- GET HIGH-ORDER POINTER BYTE
	CMP	H	; COMPARE AGAINST FILE ADDRESS HIGH
	JZ	SRTDN3	; MATCH FOUND
SRTDN2:
	INX	D	; PT TO NEXT PTR TABLE ENTRY
	DCX	B	; COUNT DOWN
	MOV	A,C	; END OF TABLE?
	ORA	B
	JNZ	SRTDN1	; CONTINUE IF NOT

;*  FATAL ERROR -- INTERNAL ERROR; POINTER TABLE NOT CONSISTENT
FERR$PTR:
	CALL	PRINT
	DB	0DH,0AH,'SORT Pointer Error',0
	JMP	CPM

;*  FOUND THE POINTER TABLE ENTRY WHICH POINTS TO THE NEXT UNORDERED FILE ENTRY
;*    MAKE BOTH POINTERS (PTR TO NEXT, PTR TO CURRENT UNORDERED FILE ENTRY)
;*    POINT TO SAME LOCATION (PTR TO NEXT FILE ENTRY TO BE ORDERED)
SRTDN3:
	LHLD	PTPTR	; GET PTR TO NEXT ORDERED ENTRY
	DCX	D	; DE PTS TO LOW-ORDER POINTER ADDRESS
	MOV	A,M	; MAKE PTR TO NEXT UNORDERED REC PT TO BUFFER FOR
	STAX	D	;   FILE REC TO BE MOVED TO NEXT UNORDERED FILE POS
	INX	H	; PT TO NEXT PTR ADDRESS
	INX	D
	MOV	A,M	; MAKE HIGH POINT SIMILARLY
	STAX	D

;*  INTERCHANGE NEXT ENTRY IN LINE WITH THE ENTRY CURRENTLY IN ITS POSITION
	LHLD	RECSIZ	; HL=NUMBER OF BYTES/ENTRY
	MOV	B,H	; BC=NUMBER OF BYTES/ENTRY
	MOV	C,L
	LHLD	PTFIL	; PT TO ENTRY
	XCHG

;*  COPY TO-BE-ORDERED FILE ENTRY TO NEXT ORDERED FILE POSITION
	LHLD	PTPTR	; POINT TO ITS POINTER
	MOV	A,M	; GET LOW-ADDRESS POINTER
	INX	H
	MOV	H,M	; GET HIGH-ADDRESS POINTER
	MOV	L,A	; HL IS ADDRESS OF UNORDERED ENTRY
	XCHG		; HL PTS TO ORDERED ENTRY TO BE MOVED, DE PTS TO REPL
SRTDN4:
	PUSH	B	; SAVE COUNT
	LDAX	D	; GET BYTES
	MOV	C,M
	XCHG		; EXCHANGE PTRS
	STAX	D	; PUT BYTE
	MOV	M,C
	XCHG		; RESTORE PTRS TO ORIGINAL
	POP	B	; GET COUNT
	INX	H	; PT TO NEXT
	INX	D
	DCX	B	; COUNT DOWN
	MOV	A,B	; DONE?
	ORA	C
	JNZ	SRTDN4

;*  POINT TO NEXT TARGET RECORD POSITION
	LHLD	RECSIZ	; GET SIZE OF RECORD
	XCHG		; ... IN DE
	LHLD	PTFIL	; PT TO ENTRY JUST PLACED
	DAD	D	; PT TO NEXT ENTRY
	SHLD	PTFIL	; SET POINTER FOR NEXT LOOP

;*  POINT TO NEXT ENTRY IN POINTER TABLE
	LHLD	PTPTR	; POINTER TO CURRENT REC
	INX	H	; PT TO NEXT REC
	INX	H
	SHLD	PTPTR

;*  COUNT DOWN
	LHLD	J	; GET COUNT
	DCX	H	; COUNT DOWN
	SHLD	J	; PUT COUNT
	MOV	A,H	; DONE?
	ORA	L
	JNZ	SRTDN

;*  EXIT
SEXIT:
	POP	PSW	; RESTORE REGS
	POP	B
	POP	D
	POP	H
	RET		; DONE

;*
;*  ISWAP -- Perform exchange of elements whose indices are in HL and DE
;*
ISWAP:
	LDA	SFLAG	; USE POINTERS?
	ORA	A	; 0=NO
	JNZ	SWAP	; DO POINTER SWAP
	CALL	ADRCOMP	; COMPUTE ADDRESS OF RECORDS FROM THEIR INDICES
	PUSH	H	; SAVE HL PTR
	LHLD	RECSIZ	; SIZE OF RECORD
	MOV	B,H	; ... IN BC
	MOV	C,L
	POP	H	; GET HL PTR
SWAPC:
	PUSH	B	; SAVE COUNT
	LDAX	D	; GET BYTES
	MOV	C,M
	MOV	M,A	; PUT BYTES
	MOV	A,C
	STAX	D	; PUT BYTES
	INX	H	; PT TO NEXT
	INX	D
	POP	B	; GET COUNT
	DCX	B	; COUNT DOWN
	MOV	A,B	; DONE?
	ORA	C
	JNZ	SWAPC
	RET

;*
;*  GIVEN INDICES IN HL AND DE, RETURN ADDRESSES OF RECORDS PTED TO BY
;*    THESE INDICES IN HL AND DE, RESP
;*
ADRCOMP:
	PUSH	H	; SAVE HL RECORD
	CALL	ADROFF	; COMPUTE OFFSET TO RECORD PTED TO BY DE
	SHLD	REC1	; SAVE ADDRESS OF RECORD
	POP	D	; GET OLD HL INDEX
	CALL	ADROFF	; COMPUTE OFFSET TO THE DESIRED RECORD
	XCHG		; ADDRESS IN DE
	LHLD	REC1	; GET ADDRESS
	XCHG		; ORIGINAL ORDER
	RET
REC1:
	DS	2	; TEMP STORAGE FOR SWAPC

ADROFF:
	LHLD	RECSIZ	; SIZE OF RECORD
	MOV	B,H	; ... IN BC
	MOV	C,L
	LXI	H,0	; OFFSET IN HL
ADRO1:
	DCX	D	; DECREMENT BY 1 SO BASE-RELATIVE
	MOV	A,D	; DONE WITH LOOP?
	ORA	E
	JZ	ADRO2
	DAD	B	; ADD IN RECORD SIZE
	JMP	ADRO1
ADRO2:
	XCHG		; OFFSET IN DE
	LHLD	FIRSTP	; PT TO FIRST ENTRY
	DAD	D	; ADD IN OFFSET
	RET

;*
;*  SWAP (Exchange) the pointers in the ORDER table whose indexes are in
;*    HL and DE
;*
SWAP:
	PUSH	H		; SAVE HL
	LHLD	ORDER		; ADDRESS OF ORDER TABLE
	MOV	B,H		; ... IN BC
	MOV	C,L
	POP	H
	DCX	H		; ADJUST INDEX TO 0...N-1 FROM 1...N
	DAD	H		; HL PTS TO OFFSET ADDRESS INDICATED BY INDEX
				;   OF ORIGINAL HL (0, 2, 4, ...)
	DAD	B		; HL NOW PTS TO POINTER INVOLVED
	XCHG			; DE NOW PTS TO POINTER INDEXED BY HL
	DCX	H		; ADJUST INDEX TO 0...N-1 FROM 1...N
	DAD	H		; HL PTS TO OFFSET ADDRESS INDICATED BY INDEX
				;   OF ORIGINAL DE (0, 2, 4, ...)
	DAD	B		; HL NOW PTS TO POINTER INVOLVED
	MOV	C,M		; EXCHANGE POINTERS -- GET OLD (DE)
	LDAX	D		; -- GET OLD (HL)
	XCHG			; SWITCH
	MOV	M,C		; PUT NEW (HL)
	STAX	D		; PUT NEW (DE)
	INX	H		; PT TO NEXT BYTE OF POINTER
	INX	D
	MOV	C,M		; GET OLD (HL)
	LDAX	D		; GET OLD (DE)
	XCHG			; SWITCH
	MOV	M,C		; PUT NEW (DE)
	STAX	D		; PUT NEW (HL)
	RET

;*
;*  ICOMPARE - Compares entries whose indices are in HL and DE; on exit,
;*	Carry Set means ((DE)) < ((HL)), Zero Set means ((HL)) = ((DE))
;*
ICOMPARE:
	LDA	SFLAG	; USE POINTERS?
	ORA	A	; 0=NO
	JNZ	COMPARE
	CALL	ADRCOMP	; COMPUTE ADDRESSES
	JMP	CALLCMP	; CALL COMPARE ROUTINE OF USER

;*
;*  COMPARE compares the entry pointed to by the pointer pointed to by HL
;*    with that pointed to by DE (1st level indirect addressing); on entry,
;*    HL and DE contain the numbers of the elements to compare (1, 2, ...);
;*    on exit, Carry Set means ((DE)) < ((HL)), Zero Set means ((HL)) = ((DE)),
;*    and Non-Zero and No-Carry means ((DE)) > ((HL))
;*
COMPARE:
	PUSH	H		; SAVE HL
	LHLD	ORDER		; ADDRESS OF ORDER
	MOV	B,H		; ... IN BC
	MOV	C,L
	POP	H
	DCX	H		; ADJUST INDEX TO 0...N-1 FROM 1...N
	DAD	H		; DOUBLE THE ELEMENT NUMBER TO POINT TO THE PTR
	DAD	B		; ADD TO THIS THE BASE ADDRESS OF THE PTR TABLE
	XCHG			; RESULT IN DE
	DCX	H		; ADJUST INDEX TO 0...N-1 FROM 1...N
	DAD	H		; DO THE SAME WITH THE ORIGINAL DE
	DAD	B
	XCHG

;*
;*  HL NOW POINTS TO THE POINTER WHOSE INDEX WAS IN HL TO BEGIN WITH
;*  DE NOW POINTS TO THE POINTER WHOSE INDEX WAS IN DE TO BEGIN WITH
;*	FOR EXAMPLE, IF DE=5 AND HL=4, DE NOW POINTS TO THE 5TH PTR AND HL
;* TO THE 4TH POINTER
;*
	MOV	C,M		; BC IS MADE TO POINT TO THE OBJECT INDEXED TO
	INX	H		; ... BY THE ORIGINAL HL
	MOV	B,M
	XCHG
	MOV	E,M		; DE IS MADE TO POINT TO THE OBJECT INDEXED TO
	INX	H		; ... BY THE ORIGINAL DE
	MOV	D,M
	MOV	H,B		; SET HL = OBJECT PTED TO INDIRECTLY BY BC
	MOV	L,C

;*
;*  COMPARE DIR ENTRY PTED TO BY HL WITH THAT PTED TO BY DE;
;*	NO NET EFFECT ON HL, DE; RET W/CARRY SET MEANS DE<HL
;*	RET W/ZERO SET MEANS DE=HL
;*
CALLCMP:
	PUSH	H	; SAVE HL ON STACK
	LHLD	CMPADR	; GET ADDRESS OF COMPARE ROUTINE IN HL
	XTHL		; ADDRESS OF COMPARE ROUTINE ON STACK AND RESTORE HL
	RET		; "CALL" ROUTINE AND RETURN TO SORT CORRECTLY

	END
