MODULE DiffLib; (** AUTHOR "negelef"; PURPOSE "Simple text diff tool"; *)

IMPORT
	Streams, Texts, TextUtilities, Commands, Strings;

CONST
	lineBufferSize = 1000;
	maxLineSize = 256;

	dirNone = 0;
	dirLeft = 1;
	dirUp = 2;
	dirRight = 4;
	dirDown = 5;
	dirDiag = 6;

VAR
	separator: BOOLEAN;

TYPE
	LineBuffer = POINTER TO RECORD
		lines: ARRAY lineBufferSize OF LONGINT;
		next: LineBuffer;
		size: LONGINT;
	END;

	Element = RECORD
		val: LONGINT;
		dir: LONGINT;
	END;

	Handler* = PROCEDURE {DELEGATE} (pos, line: LONGINT; string: Strings.String; out : Streams.Writer);
	EmptyHandler* = PROCEDURE {DELEGATE};
	SetupHandler* = PROCEDURE {DELEGATE} (nofLines: LONGINT);

	PROCEDURE GetLinePos (lineBuffer: LineBuffer; offset: LONGINT): LONGINT;
	BEGIN
		WHILE offset >= lineBuffer.size DO
			DEC (offset, lineBuffer.size);
			lineBuffer := lineBuffer.next;
		END;
		RETURN lineBuffer.lines[offset];
	END GetLinePos;

	PROCEDURE GetLineBuffer (reader: Texts.TextReader; VAR size: LONGINT): LineBuffer;
	VAR
		first, current: LineBuffer;
		ch: LONGINT;
	BEGIN
		NEW (first);
		current := first;
		current.size := 0;
		size := 0;

		REPEAT
			IF (current.size = lineBufferSize) THEN
				NEW (current.next);
				current := current.next;
				current.size := 0;
			END;
			current.lines[current.size] := reader.GetPosition ();
			INC (current.size);
			INC (size);
			REPEAT
				reader.ReadCh (ch);
			UNTIL reader.eot OR (ch = Texts.NewLineChar);
		UNTIL reader.eot;
		RETURN first;
	END GetLineBuffer;

	PROCEDURE ReadLine (pos: LONGINT; reader: Texts.TextReader): Strings.String;
	VAR
		ch, i: LONGINT;
		string: Strings.String;
	BEGIN
		reader.SetPosition (pos);
		i := 0;
		NEW (string, maxLineSize + 1);
		LOOP
			reader.ReadCh (ch);
			IF reader.eot OR (ch = Texts.NewLineChar) OR (i = maxLineSize) THEN
				EXIT
			ELSE
				string[i] := CHR (ch);
				INC (i);
			END;
		END;
		string[i] := 0X;
		RETURN string;
	END ReadLine;

	PROCEDURE Diff* (
		leftFile, rightFile: ARRAY OF CHAR;
		setup: SetupHandler; leftDiff, rightDiff, leftEqual, rightEqual: Handler; emptyLeft, emptyRight: EmptyHandler;
		out : Streams.Writer);
	VAR
		leftText, rightText: Texts.Text;
		leftReader, rightReader: Texts.TextReader;
		format, res: LONGINT;
		leftBuffer, rightBuffer, left, right: LineBuffer;
		width, height : LONGINT;
		table: POINTER TO ARRAY OF ARRAY OF Element;
		x, y: LONGINT;

		PROCEDURE CompareLines (left, right: LONGINT): BOOLEAN;
		VAR
			leftCh, rightCh: LONGINT;
		BEGIN
			leftReader.SetPosition (GetLinePos (leftBuffer, left));
			rightReader.SetPosition (GetLinePos (rightBuffer, right));

			LOOP
				leftReader.ReadCh (leftCh);
				rightReader.ReadCh (rightCh);

				IF leftReader.eot & rightReader.eot THEN RETURN TRUE END;

				IF leftCh # rightCh THEN RETURN FALSE END;

				IF (leftCh = Texts.NewLineChar) OR (rightCh = Texts.NewLineChar) THEN
					RETURN leftCh = rightCh;
				END;
			END;
		END CompareLines;

	BEGIN
		NEW (leftText);
		TextUtilities.LoadAuto(leftText, leftFile, format, res);
		leftText.AcquireRead;
		NEW (leftReader, leftText);
		leftReader.SetPosition (0);

		NEW (rightText);
		TextUtilities.LoadAuto(rightText, rightFile, format, res);
		rightText.AcquireRead;
		NEW (rightReader, rightText);
		rightReader.SetPosition (0);

		leftBuffer := GetLineBuffer (leftReader, width);
		rightBuffer := GetLineBuffer (rightReader, height);
		IF setup # NIL THEN setup(width + height); END;

		NEW (table, width + 1, height + 1);

		table[0, 0].val := 0;
		table[0, 0].dir := 0;

		FOR x := 1 TO width DO
			table[x, 0].val := 0;
			table[x, 0].dir := dirLeft;
		END;

		FOR y := 1 TO height DO
			table[0, y].val := 0;
			table[0, y].dir := dirUp;
		END;

		left := leftBuffer;
		right := rightBuffer;

		FOR y := 1 TO height DO
			FOR x := 1 TO width DO
				IF CompareLines (x - 1, y - 1) THEN
					table[x, y].val := table[x - 1, y - 1].val + 1;
					table[x, y].dir := dirDiag;
				ELSE
					format := table[x - 1, y].val;
					res := table[x, y - 1].val;
					IF format > res THEN
						table[x, y].val := format;
						table[x, y].dir := dirLeft;
					ELSE
						table[x, y].val := res;
						table[x, y].dir := dirUp;
					END;
				END;
			END;
		END;

		(* DEC (x); DEC (y); *)
		x := width; y := height;

		WHILE (x # 0) OR (y # 0) DO
			CASE table[x, y].dir OF
			dirUp:
				DEC (y); table[x, y].val := dirDown;
			| dirLeft:
				DEC (x); table[x, y].val := dirRight;
			| dirDiag:
				DEC (x); DEC (y); table[x, y].val := dirDiag;
			END
		END;

		WHILE (x # width) OR (y # height) DO
			CASE table[x, y].val OF
			dirDown:
				INC (y); Handle (y, rightReader, rightBuffer, rightDiff, out); IF emptyLeft # NIL THEN emptyLeft; END;
			| dirRight:
				INC (x); Handle (x, leftReader, leftBuffer, leftDiff, out); IF emptyRight # NIL THEN emptyRight; END;
			| dirDiag:
				INC (x); Handle (x, leftReader, leftBuffer, leftEqual, out);
				INC (y); Handle (y, rightReader, rightBuffer, rightEqual, out);
			END
		END;
	END Diff;

	PROCEDURE Handle (line: LONGINT; reader: Texts.TextReader; buffer: LineBuffer; handler: Handler; out : Streams.Writer);
	VAR
		pos: LONGINT;
	BEGIN
		IF handler # NIL THEN
			pos := GetLinePos (buffer, line - 1);
			handler (pos, line, ReadLine (pos, reader), out);
		END
	END Handle;

	PROCEDURE Left (pos, line: LONGINT; string: Strings.String; out : Streams.Writer);
	BEGIN
		out.String (		"< ("); out.Int (line, 0); out.Char (':');
		out.Int (pos, 0); out.String (") "); out.String (string^); out.Ln;
		separator := TRUE;
	END Left;

	PROCEDURE Right (pos, line: LONGINT; string: Strings.String; out : Streams.Writer);
	BEGIN
		out.String ("> ("); out.Int (line, 0); out.Char (':');
		out.Int (pos, 0); out.String (") "); out.String (string^); out.Ln;
		separator := TRUE;
	END Right;

	PROCEDURE Equal (pos, line: LONGINT; string: Strings.String; out : Streams.Writer);
	BEGIN
		IF separator THEN out.Ln; separator := FALSE END
	END Equal;

	PROCEDURE Compare* (context : Commands.Context);
	VAR
		left, right: ARRAY 64 OF CHAR;
	BEGIN
		context.arg.SkipWhitespace; context.arg.String(left);
		context.arg.SkipWhitespace; context.arg.String(right);

		context.out.String ("< "); context.out.String (left); context.out.Ln;
		context.out.String ("> "); context.out.String (right); context.out.Ln;
		context.out.Ln;

		separator := FALSE;

		Diff (left, right, NIL, Left, Right, Equal, Equal, NIL, NIL, context.out);
	END Compare;

END DiffLib.

SystemTools.Free DiffLib~
DiffLib.Compare DiffTest1.Text DiffTest2.Text~

DiffLib.Compare Configuration.XML Configuration.XML.Bk ~
