MODULE WMVNCView;	(** AUTHOR "TF"; PURPOSE "VNC Viewport"; *)

IMPORT
		Raster, Rect := WMRectangles, WMWindowManager, VNCServer, Modules, KernelLog, Commands,
		Graphics := WMGraphics, Messages := WMMessages, Strings, Texts, TextUtilities;

TYPE
	Window = WMWindowManager.Window;
	Rectangle = Rect.Rectangle;
	String = Strings.String;


	VNCView = OBJECT (WMWindowManager.ViewPort)
		VAR
			server: VNCServer.Server;
			error:BOOLEAN;
			backbuffer* : Graphics.Image;
			c : Graphics.BufferCanvas;
			state : Graphics.CanvasState;
			navig : BOOLEAN;
			scrollLock : BOOLEAN;

			fx, fy, inffx, inffy, factor, intfactor : REAL;
			active : BOOLEAN;

		PROCEDURE &New*(manager:WMWindowManager.WindowManager; port, dx, dy, w, h:LONGINT; name, password:ARRAY OF CHAR);
		VAR str : ARRAY 16 OF CHAR;
		BEGIN
			NEW(backbuffer);
			Raster.Create(backbuffer, w, h, Raster.BGR565);
			NEW(c, backbuffer);
			c.SetFont(Graphics.GetDefaultFont());
			c.SaveState(state);
			SetExtents(w, h);
			width0 := w; height0 := h;
			range.l := dx; range.t := dy; range.r := dx + w; range.b := dy + h;
			Strings.IntToStr(port, str);
			desc := "VNC view on port "; Strings.Append(desc, str);
			factor := 1; intfactor := 1;
			fx := factor; fy := factor; inffx := 1 ; inffy := inffx;
			active := FALSE;
			server := VNCServer.OpenServer(port, backbuffer, name, password, MouseEvent, KeyEvent, ClipboardEvent, CheckActive);
			IF server # NIL THEN
				manager.AddView(SELF);
				manager.RefreshView(SELF);
				error := FALSE
			ELSE error := TRUE
			END;
			Texts.clipboard.onTextChanged.Add(ClipboardChanged)
		END New;

		PROCEDURE CheckActive(nof : LONGINT);
		BEGIN
			IF ~active & (nof > 0) THEN
				active := TRUE;
				manager.RefreshView(SELF)
			END
		END CheckActive;

		PROCEDURE Update(r : Rectangle; top : WMWindowManager.Window);
		BEGIN
			IF ~active THEN RETURN END;
			Draw(Rect.ResizeRect(r, 1), top.prev)
		END Update;

		PROCEDURE Refresh*(top : Window);
		BEGIN
			Update(Rect.MakeRect(ENTIER(range.l)-1, ENTIER(range.t)-1, ENTIER(range.r) + 1, ENTIER(range.b) + 1), top)
		END Refresh;

		(* in wm coordinates *)
		PROCEDURE Draw(r : Rectangle; top : Window);
		VAR cur : Window;
			wr, nr : Rectangle;

			PROCEDURE InternalDraw(r : Rectangle; cur : Window);
			VAR nr, cb, tnr, dsr : Rectangle;
			BEGIN
				IF cur.useAlpha & (cur.prev # NIL)  THEN Draw(r, cur.prev)
				ELSE
					WHILE cur # NIL DO (* draw r in wm coordinates in all the windows from cur to top *)
						nr := r; cb := cur.bounds; Rect.ClipRect(nr, cb);
						dsr.l := ENTIER((nr.l - range.l) * fx) ; dsr.t := ENTIER((nr.t - range.t) * fy);
						dsr.r := ENTIER((nr.r - range.l) * fx + 0.5); dsr.b := ENTIER((nr.b - range.t) * fy + 0.5);
						IF ~Rect.RectEmpty(dsr) THEN
							c.SetClipRect(dsr);  (* Set clip rect to dsr, clipped at current window *)
							c.ClipRectAsNewLimits(ENTIER((cur.bounds.l - range.l) * fx), ENTIER((cur.bounds.t - range.t) * fy));
							(* range can not be factored out because of rounding *)
							IF navig THEN
								cur.Draw(c, ENTIER((cb.r - range.l)* fx) - ENTIER((cb.l - range.l) * fx),
													ENTIER((cb.b - range.t) * fy) - ENTIER((cb.t - range.t) * fy), 0);
							ELSE
								cur.Draw(c, ENTIER((cb.r - range.l)* fx) - ENTIER((cb.l - range.l) * fx),
													ENTIER((cb.b - range.t) * fy) - ENTIER((cb.t - range.t) * fy), 1);
							END;
							c.RestoreState(state);
						END;
						cur := cur.next
					END;
					tnr.l := ENTIER((r.l - range.l) * fx); tnr.t := ENTIER((r.t - range.t) * fy);
					tnr.r := ENTIER((r.r - range.l) * fx + 0.5); tnr.b := ENTIER((r.b - range.t) * fy + 0.5);
					ClipAtImage(tnr, backbuffer);
					server.AddDirty(tnr)
				END
			END InternalDraw;

		BEGIN
			cur := top;
			IF (cur # NIL) & (~Rect.RectEmpty(r)) THEN
				wr := cur.bounds;
				IF ~Rect.IsContained(wr, r) THEN
					IF Rect.Intersect(r, wr) THEN
						(* r contains wr calculate r -  wr and recursively call for resulting rectangles*)
						(* calculate top rectangle *)
						IF wr.t > r.t THEN Rect.SetRect(nr, r.l, r.t, r.r, wr.t); Draw(nr, cur.prev) END;
						(* calculate bottom rectangle *)
						IF wr.b < r.b THEN Rect.SetRect(nr, r.l, wr.b, r.r, r.b); Draw(nr, cur.prev) END;
						(* calculate left rectangle *)
						IF wr.l > r.l THEN Rect.SetRect(nr, r.l, Max(r.t, wr.t), wr.l, Min(r.b, wr.b)); Draw(nr, cur.prev) END;
						(* calculate left rectangle *)
						IF wr.r < r.r THEN Rect.SetRect(nr, wr.r, Max(r.t, wr.t), r.r, Min(r.b, wr.b)); Draw(nr, cur.prev) END;
						(* calculate overlapping *)
						nr := r; Rect.ClipRect(nr, wr);
						IF ~Rect.RectEmpty(nr) THEN InternalDraw(nr, cur) END
					ELSE Draw(r, cur.prev)
					END
				ELSE InternalDraw(r, cur)
				END
			END
		END Draw;

		PROCEDURE SetExtents(w, h : LONGINT);
		BEGIN
			range.r := range.l + w; range.b := range.t + h;
		END SetExtents;

		PROCEDURE SetScaleFactor(factor : REAL);
		VAR centerX, centerY : REAL;
		BEGIN
			centerX := (range.l + range.r) / 2; centerY := (range.t + range.b) /2;
			fx := factor; fy := factor; inffx := 1 / factor; inffy := inffx;
			SELF.factor := factor;
			range.l := centerX - inffx * 0.5 * backbuffer.width;
			range.t := centerY - inffy * 0.5 * backbuffer.height;
			range.r := centerX + inffx * 0.5 * backbuffer.width;
			range.b := centerY + inffy * 0.5 * backbuffer.height
		END SetScaleFactor;

		PROCEDURE KeyEvent(ucs: LONGINT; flags : SET; keysym : LONGINT);
		VAR msg : Messages.Message;
		BEGIN
			manager.lock.AcquireWrite;
			msg.originator := SELF;
			IF keysym = 0FFC9H THEN scrollLock := ~scrollLock END;
			msg.msgType := Messages.MsgKey;
			msg.x := ucs;
			msg.y := keysym;
			msg.flags := flags;
			manager.Handle(msg);
			manager.lock.ReleaseWrite
		END KeyEvent;

		PROCEDURE MouseEvent(x, y, dz: LONGINT; keys : SET);
		VAR msg : Messages.Message;
		BEGIN
			manager.lock.AcquireWrite;
			msg.originator := SELF;
			msg.msgType := Messages.MsgPointer;
			msg.x := ENTIER(range.l + x * inffx); msg.y := ENTIER(range.t + y * inffy);
			msg.dz := dz;
			msg.flags := keys;
			IF manager # NIL THEN manager.Handle(msg) END;
			manager.lock.ReleaseWrite
		END MouseEvent;

		PROCEDURE ClipboardEvent(text : String);
		BEGIN {EXCLUSIVE}
			Texts.clipboard.onTextChanged.Remove(ClipboardChanged);
			Texts.clipboard.AcquireWrite;
			Texts.clipboard.Delete(0, Texts.clipboard.GetLength());
			TextUtilities.StrToText(Texts.clipboard, 0, text^);
			Texts.clipboard.ReleaseWrite;
			Texts.clipboard.onTextChanged.Add(ClipboardChanged)
		END ClipboardEvent;

		PROCEDURE ClipboardChanged(sender, data : ANY);
		VAR text : String;
		BEGIN {EXCLUSIVE}
			NEW(text, 16 * 1024);
			TextUtilities.TextToStr(Texts.clipboard, text^);
			IF server = NIL THEN KernelLog.String("Cann not understand how this could possibly happen :-( "); KernelLog.Ln
			ELSE
				server.SendClipboard(text)
			END
		END ClipboardChanged;

		PROCEDURE Close;
		BEGIN
			Texts.clipboard.onTextChanged.Remove(ClipboardChanged);
			manager.RemoveView(SELF); server.Close
		END Close;

	END VNCView;

TYPE
	VVList = POINTER TO RECORD
		v:VNCView;
		next:VVList
	END;

VAR v: VVList;

PROCEDURE Min(a, b:LONGINT):LONGINT;
BEGIN
	IF a<b THEN RETURN a ELSE RETURN b END;
END Min;

PROCEDURE Max(a, b:LONGINT):LONGINT;
BEGIN
	IF a>b THEN RETURN a ELSE RETURN b END;
END Max;

PROCEDURE Bound(VAR x:LONGINT; min, max:LONGINT);
BEGIN
	IF x < min THEN x := min ELSE IF x > max THEN x := max END END
END Bound;

PROCEDURE ClipAtImage(VAR x: Rectangle; img:Raster.Image);
BEGIN
	Bound(x.l, 0, img.width - 1);Bound(x.r, 0, img.width - 1);
	Bound(x.t, 0, img.height - 1);Bound(x.b, 0, img.height - 1)
END ClipAtImage;

(** name password port x y w h
name and password are strings optionally in " "
use "" for no password
*)
PROCEDURE Install*(context : Commands.Context); (** name password [port [x [ y [ width [ height ] ] ] ] ] ~ *)
VAR
	name:ARRAY 100 OF CHAR;
	password: ARRAY 32 OF CHAR;
	port, dx, dy, w, h:LONGINT;
	nv:VNCView;
	vl:VVList;
BEGIN
	context.arg.SkipWhitespace;
	context.arg.String(name);
	context.arg.SkipWhitespace;
	context.arg.String(password);
	context.arg.SkipWhitespace;

	(* port *)
	port := 5901;
	IF (context.arg.Peek() >= '0') & (context.arg.Peek() <= '9') THEN context.arg.Int(port, TRUE) END;
	context.arg.SkipWhitespace;
	(* dx *)
	dx := 0;
	IF (context.arg.Peek() >= '0') & (context.arg.Peek() <= '9') OR (context.arg.Peek()='-') THEN context.arg.Int(dx, TRUE) END;
	context.arg.SkipWhitespace;
	(* dy *)
	dy := 0; IF (context.arg.Peek() >= '0') & (context.arg.Peek() <= '9') OR (context.arg.Peek()='-') THEN context.arg.Int(dy, TRUE) END;
	context.arg.SkipWhitespace;
	(* w *)
	w := 1024; IF (context.arg.Peek() >= '0') & (context.arg.Peek() <= '9') THEN context.arg.Int(w, TRUE) END;
	context.arg.SkipWhitespace;
	(* h *)
	h := 768; IF (context.arg.Peek() >= '0') & (context.arg.Peek() <= '9') THEN context.arg.Int(h, TRUE) END;
	context.arg.SkipWhitespace;

	NEW(nv, WMWindowManager.GetDefaultManager(), port, dx, dy, w, h, name, password);
	context.out.String("VNC server started. Listening on port : "); context.out.Int(port, 4);
	context.out.Ln; context.out.String("Position (x, y): "); context.out.Int(dx, 4); context.out.String(", "); context.out.Int(dy, 4);
	context.out.Ln; context.out.String("Size (w, h): "); context.out.Int(w, 4); context.out.String(", "); context.out.Int(h, 4);
	IF ~nv.error THEN
		NEW(vl); vl.v:=nv;
		vl.next:=v; v:=vl
	END;
END Install;

PROCEDURE Uninstall*;
BEGIN
	WHILE v # NIL DO v.v.Close;  v := v.next END;
END Uninstall;

PROCEDURE Cleanup;
BEGIN
	Uninstall;
END Cleanup;

BEGIN
	Modules.InstallTermHandler(Cleanup)
END WMVNCView.

System.Free WMVNCView VNCServer~
Aos.Call WMVNCView.Install "Bluebottle VNC View1" "" 5903 0 0 1024 768~
Aos.Call WMVNCView.Install "Bluebottle VNC View1" "" 5902 1280 0 1280 1024~
Aos.Call WMVNCView.Install "Bluebottle VNC View1" "" 5903 0 0 1024 768~
Aos.Call WMVNCView.Uninstall (BYE)

