(* Copyright 2021-2024 Sebastian Lederer. See the file LICENSE.md for details *)
program xfer;

const CksumPattern = $AFFECAFE;
      SOH = #1;
      STX = #2;
      EOT = #4;
      ENQ = #5;
      ACK = #6;
      BEL = #7;
      NAK = #21;

      TimeoutTicks = 200;

var blockNo:integer;
    buf:^string;
    invalid:boolean;
    cmd:char;
    filename:string;
    size:integer;
    done:boolean;
    xferFile:file;

function calcChksum(last,this:integer):integer;
begin
	calcChksum := ((last + this) xor CksumPattern) shl 1;
end;

function readword:integer;
var b3,b2,b1,b0:char;
begin
	b3 := conin;
	b2 := conin;
	b1 := conin;
	b0 := conin;

	readword := (ord(b3) shl 24) or
		(ord(b2) shl 16) or
		(ord(b1) shl 8) or
		ord(b0);
end;

procedure serReadBlock(var success:boolean);
var w0,w1,w2,w3,w4,w5,w6,w7,w8:integer;
    chksum:integer;
    s:integer;

procedure writeByte(b:char);
begin
	if size <> 0 then
	begin
		write(xferFile,b);
		size := size - 1;
	end;
end;

procedure appendWordToFile(w:integer);
var b3,b2,b1,b0:char;
begin
	b0 := chr(w and 255);
	w := w shr 8;

	b1 := chr(w and 255);
	w := w shr 8;

	b2 := chr(w and 255);
	w := w shr 8;

	b3 := chr(w);

	writeByte(b3);
	writeByte(b2);
	writeByte(b1);
	writeByte(b0);
end;

procedure calcChksum(d:integer);
begin
	chksum := ((chksum + d) xor CksumPattern) shl 1;
end;

begin
	chksum := 0;

	w0 := readword;
	w1 := readword;
	w2 := readword;
	w3 := readword;
	w4 := readword;
	w5 := readword;
	w6 := readword;
	w7 := readword;

	s := readword;

	calcChksum(w0);
	calcChksum(w1);
	calcChksum(w2);
	calcChksum(w3);
	calcChksum(w4);
	calcChksum(w5);
	calcChksum(w6);
	calcChksum(w7);

	if s <> chksum then
	begin
		success := false;
		write(NAK);
		{ writeln('invalid chksum ', s, ' ', chksum); }
	end
	else
	begin
		success := true;
		appendWordToFile(w0);
		appendWordToFile(w1);
		appendWordToFile(w2);
		appendWordToFile(w3);
		appendWordToFile(w4);
		appendWordToFile(w5);
		appendWordToFile(w6);
		appendWordToFile(w7);
		blockNo := blockNo + 1;
		write(ACK);
	end;
end;

procedure waitForByte(var byteReceived:char; var timeoutReached:boolean);
var ticks:integer;
    done:boolean;
begin
	timeoutReached := true;
	ticks := getticks;
	done := false;

	repeat
		if conavail then
		begin
			done := true;
			timeoutReached := false;
			byteReceived := conin;
		end;
	until done or (getticks > ticks + TimeoutTicks);
end;

procedure waitForHeader(var invalid:boolean);
var done:boolean;
    timedOut:boolean;
    ch:char;
begin
	waitForByte(ch, timedOut);
	invalid := (ch <> STX) or timedOut;
end;

procedure receiveHeader(var invalid:boolean);
var ch:char;
    timedOut:boolean;
    w:integer;
    cksum:integer;
begin
	{ send protocol version, then wait for size header }
	write('1');

	waitForByte(ch, timedOut);
	if timedOut or (ch <> SOH) then
	begin
		invalid := true;
		exit;
	end;

	cksum := 0;

	w := readword;
	cksum := readword;

	if w <> (not cksum) then
	begin
		write(NAK);
		w := 0;
		writeln('h chksum error');
	end
	else
		write(ACK);

	if w > 0 then
	begin
		size := w;
		waitForHeader(invalid);
	end
	else
		invalid := true;
end;

procedure receiveFile;
var ch:char;
    invalid, timedOut:boolean;
    ok:boolean;
    done:boolean;
    errorCount:integer;
begin
	if length(filename) = 0 then
	begin
		writeln('Filename not set.');
		exit;
	end;

	errorCount := 0;

	waitForByte(ch, timedOut);
	if timedOut then
	begin
		writeln('Timeout waiting for transmission start (ENQ or STX).');
		exit;
	end;

	if ch = ENQ then
		receiveHeader(invalid)
	else
	if ch = STX then
	begin
		size := -1;
		invalid := false;
	end
	else
		invalid := true;

	if not invalid then
	begin
		open(xferFile, filename, ModeOverwrite);
		done := false;
		repeat
			serReadBlock(ok);
			if not ok then errorCount := errorCount + 1;

			waitForByte(ch, timedOut);
			if timedOut then
				writeln('Timeout waiting for next block (STX)');
			if ch = EOT then
				done := true
			else
			if ch <> STX then
			begin
				writeln('Invalid header byte (expected STX)');
				done := true;
			end;
		until done or timedOut;

		close(xferFile);

		writeln(blockNo, ' blocks received, ', errorCount, ' checksum errors. ', ord(ch));
	end
	else
		writeln('Invalid or no header received.', size);
end;

function getWordFromFile:integer;

function getCharFromFile:integer;
var c:char;
begin
	if size > 0 then
	begin
		read(xferFile,c);
		size := size - 1;
	end
	else
		c := #0;

	getCharFromFile := ord(c);
end;

begin
	getWordFromFile := getCharFromFile shl 8;
	getWordFromFile := (getWordFromFile or getCharFromFile) shl 8;
	getWordFromFile := (getWordFromFile or getCharFromFile) shl 8;
	getWordFromFile := (getWordFromFile or getCharFromFile);
end;

procedure sendword(w:integer);
var b3,b2,b1,b0:char;
begin
	b0 := chr(w and 255);
	w := w shr 8;

	b1 := chr(w and 255);
	w := w shr 8;

	b2 := chr(w and 255);
	w := w shr 8;

	b3 := chr(w and 255);

	write(b3,b2,b1,b0);
end;

procedure sendFile;
var ch:char;
    w,cksum:integer;
    wordCount:integer;
    lastSize,lastPos:integer;
    timedOut:boolean;
    done:boolean;
begin
	if length(filename) = 0 then
	begin
		writeln('Filename not set.');
		exit;
	end;

	{ wait for start byte }
	ch := conin;
	if ch <> BEL then
	begin
		writeln('Invalid start character received.');
		exit;
	end;

	open(xferFile, filename, ModeReadonly);
	if IOResult(xferFile) <> 0 then
	begin
		writeln('Error opening file: ', ErrorStr(IOResult(xferFile)));
		exit;
	end;

	size := filesize(xferFile);
	done := false;

	{ send size header: SOH, size word, inverted size word }
	write(SOH);
	sendword(size);
	sendword(not size);

	{ check for ACK }
	waitForByte(ch, timedOut);

	if timedOut then
		writeln('Timeout sending size header ')
	else
	if ch <> ACK then
		writeln('Error sending size header ', ord(ch))
	else
	repeat
		lastPos := filepos(xferFile);
		lastSize := size;

		write(STX);
		{ send a block: STX, 8 words, checksum word }
		cksum := 0;
		for wordCount := 1 to 8 do
		begin
			w := getWordFromFile;
			cksum := calcChkSum(cksum, w);
			sendword(w);
		end;
		sendword(cksum);

		{ check for ACK/NAK }
		waitForByte(ch, timedOut);
		if timedOut then
		begin
			writeln('Timeout waiting for ACK');
			done := true;
		end
		else
		if ch = NAK then
		begin
			seek(xferFile, lastPos);
			size := lastSize;
		end
		else
		if ch = ACK then
		begin
			if size = 0 then done := true;
		end
		else
		begin
			writeln('Invalid reply after sending block');
			done := true;
		end;
	until done;
	write(EOT);
	close(xferFile);
end;

procedure setFilename;
begin
	write('Filename> ');
	readln(filename);
end;

procedure listDirectory;
var volid:integer;
    error:integer;
    index:integer;
    dirs:DirectorySlot;
begin
	volid := findvolume(DefaultVolume);
	if volid < 1 then
		writeln('Volume ', DefaultVolume, ' not found.')
	else
	begin
		openvolumeid(volid);
		readdirfirst(volid, index, dirs, error);
		while (index > 0) and (error = 0) do
		begin
			writeln(dirs.name);
			readdirnext(volid, index, dirs, error);
		end;
	end;
	writeln;
end;

begin
	writeln('L) upload (receive)   D) download (send).');
	writeln('S) set filename       Y) directory         X) exit');

	done := false;

	repeat
		write('> ');
		read(cmd);
		writeln;
		case upcase(cmd) of
			'L': receiveFile;
			'D': sendFile;
			'S': setFilename;
			'X': done := true;
			'Y': listDirectory;
			else writeln('?');
		end;
	until done;
end.
