-- FILE: InterminalImpl.mesa
--  Last Edited by Stone, September 18, 1982 10:25 am
--  Last Edited by Andrew Birrell, September 8, 1982 12:46 pm  keyboard handler performance
--  Last Edited by Stone, June 23, 1982 2:21 pm  color cursor and pen
--  Last Edited by McGregor, 16-Apr-82  9:26:01  Flushed delta x&y mouse ops,
--  Last Edited by Swinehart, April 29, 1982 5:16 pm, register with multiplexor, etc. 
--  Compile /-C InterminalImpl  !!!!!!

DIRECTORY
  CedarSnapshot USING [ CheckpointProc, RollbackProc, Register ],
  ClassIncreek USING [ Action, ActionBody ],
  ClassInscript USING [ Inscript, SetWritePage, WriteEntry ],
  DisplayFace USING [ refreshRate ],
  Interminal,
  InterminalExtra USING [],	--exports only
  Intime USING [
    AdjustEventTime, DeltaTime, DeltaDeltaTime, EventTime, EventTimeDifference, maxDeltaTime,
    maxDeltaDeltaTime, msPerDeltaTick, MsTicks, MsTicksToDeltaTime, ReadEventTime,
    SubtractMsTicksFromEventTime ],
  Process USING [ Priority, GetPriority, SetPriority ],
  SpecialSpace USING [
	MakeCodeResident, MakeGlobalFrameResident ],
  TerminalMultiplex USING [ InputAction, RegisterInputController ],
  ColorDisplay USING[HideCursor, SetCursorPattern, GetCursorPattern, SetCursorPosition,
    RestoreCursor, width,height],
  UserTerminal USING [ cursor, mouse, keyboard, screenWidth, screenHeight, WaitForScanLine, SetCursorPattern, GetCursorPattern],
  Inline USING[LongMult,LongDiv];

InterminalImpl: MONITOR [inscript: ClassInscript.Inscript] -- START explicitly!

	IMPORTS	CedarSnapshot, ClassInscript, DisplayFace,
		Interminal, Intime, P: Process, SpecialSpace,
		TerminalMultiplex, UserTerminal, ColorDisplay, Inline
	EXPORTS	Interminal, InterminalExtra
	SHARES	Interminal = {
	OPEN		Interminal, ClassIncreek, ClassInscript, Intime;

--Interminal.MousePosition now has a boolean on it to declare which display the point is on
--to keep things consistent with the hardware world, we do all our internal calculations
--with InternalMP, then construct a MousePosition when recording the Action
InternalMP: TYPE = MACHINE DEPENDENT RECORD [mouseX, mouseY: INTEGER];
currentKeys: LONG POINTER TO READONLY KeyState = LOOPHOLE[UserTerminal.keyboard];
currentMousePosition: LONG POINTER TO InternalMP = LOOPHOLE[UserTerminal.mouse];
cursorPosition: LONG POINTER TO InternalMP = LOOPHOLE[UserTerminal.cursor];
wordlength: CARDINAL = 16;

Internal: ERROR[code: INTEGER] = CODE; -- "can't happen errors"
-- Values at time t, maintained correct by ActionRecorder main loop
mousePosition: InternalMP ← currentMousePosition↑;
keyState: KeyState; -- ←currentKeys↑, see init --
eventTime: EventTime; -- ←ReadEventTime[], initialized below

-- Values at time t-1, also maintained by main loop --
lastMousePosition: InternalMP;
lastKeyState: KeyState; -- ←keyState, to start, see init --
lastEventTime: EventTime;

allUp: PUBLIC KeyState ←
    [words[words: [177777B, 177777B, 177777B, 177777B, 177777B]]];

doTrack: BOOLEAN←TRUE;
hotX: INTEGER←0;
hotY: INTEGER←0;
 
 allZeros: CursorArray ← ALL[0];
  
-- Mouse grain threshhold stuff
grainDots: INTEGER;
 defaultGrainDots: INTEGER←5;
grainMsTicks: Intime.MsTicks;
 defaultGrainTicks: Intime.MsTicks←200; -- 5 actions per second
grainTicks: INTEGER;
 grainTicksLeft: INTEGER;
 	rR: INTEGER=DisplayFace.refreshRate*2; 
 grainMsPerTick: Intime.MsTicks ← (1000+rR-1)/rR; --System pulse duration in Ms.
  
SetCursorOffset: PUBLIC PROC[deltaX, deltaY: INTEGER, enableTracking: BOOLEAN←TRUE]={
    hotX←deltaX; hotY←deltaY; doTrack←enableTracking; };
GetCursorOffset: PUBLIC PROC RETURNS[deltaX, deltaY: INTEGER, trackingEnabled: BOOLEAN] = {
    RETURN[deltaX: hotX, deltaY: hotY, trackingEnabled: doTrack];} ;

hasColor: BOOLEAN ← FALSE;
DisplayRec: TYPE = RECORD[xMin,xMax,yMin,yMax: INTEGER, color: BOOLEAN];
left,right,display: POINTER TO DisplayRec;
leftRep: DisplayRec;
rightRep: DisplayRec;

  -- This procedure runs as an independent process. It constitutes
  --  the low-level action recording mechanisms for user input actions. --

ActionRecorder: PROCEDURE = { -- Runs as detached process, records terminal actions --
    DO
      WaitForTick[]; -- run once per tick

      IF hasColor THEN HardCase[]
      ELSE {
          mousePosition.mouseX ← (currentMousePosition.mouseX ←
      	    MAX[MIN[display.xMax, currentMousePosition.mouseX], display.xMin]);
          mousePosition.mouseY ← (currentMousePosition.mouseY ←
      	    MAX[MIN[display.yMax, currentMousePosition.mouseY], display.yMin]);
          IF doTrack THEN {
             cursorPosition.mouseX←mousePosition.mouseX+hotX;
	         cursorPosition.mouseY←mousePosition.mouseY+hotY};
          };
      keyState ← currentKeys↑;	--add pen here
      IF keyState # lastKeyState THEN { eventTime ← ReadEventTime[]; []←EnterKeyState[] }
      ELSE DoMousePosition[];
      ENDLOOP; };

--moves the mouse between displays
HardCase: PROC = INLINE  { --check if mouse is moving out of current display
  pattern: CursorArray;
  updateCursorBits: BOOLEAN ← FALSE;
  mouseX: INTEGER ← currentMousePosition.mouseX;
  mouseY: INTEGER ← MAX[MIN[currentMousePosition.mouseY,display.yMax], display.yMin];
    toH,fromH: INTEGER;
  hideCursor: PROC = INLINE {
      IF display.color THEN { 
        pattern ← ColorDisplay.GetCursorPattern[];
        ColorDisplay.SetCursorPattern[allZeros];
        ColorDisplay.SetCursorPosition[[-100,-100]];
        }
      ELSE {
        pattern ← UserTerminal.GetCursorPattern[];
        UserTerminal.SetCursorPattern[allZeros];
        cursorPosition↑ ← [-100,-100]};	--hides bw cursor
      };
  
  setCursorBits: PROC = INLINE {
      IF display.color THEN ColorDisplay.SetCursorPattern[pattern]
      ELSE UserTerminal.SetCursorPattern[pattern];
      };
  
SELECT TRUE FROM
      mouseX>display.xMax => { --moving right
          IF display=left THEN {  --left to right
              hideCursor[];
              fromH ← display.yMax;
              display ← right; 
              toH ← display.yMax; 
              mousePosition.mouseX ← currentMousePosition.mouseX ← display.xMin;
-- an integer divide, but we get our precision where we need it
              mouseY ← Inline.LongDiv[Inline.LongMult[mouseY,toH],fromH]; 
              setCursorBits[]}
          ELSE  mousePosition.mouseX ← currentMousePosition.mouseX ← display.xMax };
      mouseX<display.xMin => { -- moving left
          IF display=right THEN {  --right to left
              hideCursor[];
              fromH ← display.yMax;
              display ← left;
              toH ← display.yMax; 
              mousePosition.mouseX ← currentMousePosition.mouseX ← display.xMax;
-- an integer divide, but we get our precision where we need it
              mouseY ← Inline.LongDiv[Inline.LongMult[mouseY,toH],fromH];
              setCursorBits[]}
          ELSE  mousePosition.mouseX ← currentMousePosition.mouseX ← display.xMin };
      ENDCASE => mousePosition.mouseX ← currentMousePosition.mouseX ← mouseX;
  mousePosition.mouseY ← currentMousePosition.mouseY ←mouseY;
  IF doTrack THEN {
      IF ~display.color THEN {
          cursorPosition.mouseX←mousePosition.mouseX+hotX;  
          cursorPosition.mouseY←mousePosition.mouseY+hotY;}
      ELSE 
          ColorDisplay.SetCursorPosition[[mousePosition.mouseX+hotX, mousePosition.mouseY+hotY]];
      };
  };
  
-- Mouse position and keyboard state recording functions --
DoMousePosition: PROC = INLINE { -- check threshholds
  IF (grainTicksLeft←grainTicksLeft-1)>0 THEN RETURN;
  grainTicksLeft←grainTicks;
  -- grainDots test removed 4-15-82 DCS
  IF mousePosition#lastMousePosition THEN
    { eventTime ← ReadEventTime[]; []←EnterMousePosition[FALSE] };
  };
  
mouseAB: mousePosition ActionBody ←
  [contents: mousePosition[mousePosition: [mousePosition.mouseX, FALSE, mousePosition.mouseY]]];
maxDeltaMouse: INTEGER = LAST[[-8..8)];

EnterMousePosition: PROC[enterFullPos: BOOLEAN] RETURNS [enteredFull: BOOL←FALSE] = {
--in this procedure we convert the mousePosition, which corresponds to the cursor's
--coordinates to a value which is relative to the lower left corner of the display
	dX: INTEGER = mousePosition.mouseX-lastMousePosition.mouseX;
	dY: INTEGER = mousePosition.mouseY-lastMousePosition.mouseY;
	mouseA: Action;
	IF NOT enterFullPos AND (ABS[dX]<=maxDeltaMouse AND ABS[dY]<=maxDeltaMouse) THEN {
	  dMouseB: deltaMouse ActionBody ← [contents: deltaMouse[value: [dX, -dY]]]; --flip the origin
	  mouseA←@dMouseB; }
	ELSE {
	    y: INTEGER ← IF display.color
	        THEN ColorDisplay.height-mousePosition.mouseY-1 
	        ELSE UserTerminal.screenHeight-mousePosition.mouseY-1;
	    mouseAB.mousePosition←[mousePosition.mouseX, display.color, y];
	    mouseA←@mouseAB; };
    IF EnterAction[mouseA].enteredFull THEN RETURN[TRUE];
    lastMousePosition ← mousePosition; };


KMethod: TYPE = {action, state};

-- recording kbd action or recording current downness
EnterKeyState: PROCEDURE [method: KMethod ← action]
	RETURNS [enteredFull: BOOLEAN←FALSE] = {
  Kn: PROCEDURE [v: CARDINAL] RETURNS [KeyName] = INLINE {RETURN[LOOPHOLE[v]]};
  Kv: PROCEDURE [n: KeyName] RETURNS [CARDINAL] = INLINE {RETURN[LOOPHOLE[n]]};
  -- Hi-res mouse recording
  IF mousePosition#lastMousePosition AND EnterMousePosition[FALSE].enteredFull
	THEN RETURN[TRUE];
  FOR i: CARDINAL IN [0..SIZE[KeyState]) DO
    IF keyState.words[i] # lastKeyState.words[i] THEN
      FOR j: KeyName IN [Kn[i*wordlength]..Kn[(i + 1)*wordlength]) DO
	x: updown = keyState.bits[j];
	IF x = lastKeyState.bits[j] THEN LOOP; -- bit difference detected --
	{aUpDown: keyUp ActionBody ←
	  IF x = up THEN [contents: keyUp[value: j]]
	  ELSE IF method = action THEN [contents: keyDown[value: j]]
	       ELSE [contents: keyStillDown[value: j]];
	IF EnterAction[@aUpDown].enteredFull THEN RETURN[TRUE]; }; ENDLOOP; ENDLOOP;
  lastKeyState ← keyState; };

EnterFullKeyState: PROCEDURE RETURNS [enteredFull: BOOLEAN] = {
  aAllUp: keyStillDown ActionBody ← [contents: keyStillDown[value: allUp]];
  IF EnterKeyState[].enteredFull THEN RETURN[TRUE]; -- enter any new actions --
  lastKeyState ← allUp;
  IF EnterAction[@aAllUp].enteredFull THEN RETURN[TRUE];--reader should clear state next time
  RETURN[EnterKeyState[state].enteredFull]; }; -- enter current state of all down keys --

-- Unconditionally enter the current time, preferably as an increment to the
--  previous entry.  Assume that an absolute time value is entered occasionally.

EnterTime: PROCEDURE RETURNS [dTime: DeltaDeltaTime, enteredFull: BOOLEAN] = INLINE {
  a: deltaEventTime ActionBody;
  difference: MsTicks ← EventTimeDifference[@eventTime, @lastEventTime];
  deltaTime: CARDINAL; -- s/b DeltaTime, but can be out of range
  [deltaTime, difference] ← MsTicksToDeltaTime[difference];
  IF deltaTime > maxDeltaTime THEN RETURN[0, EnterFullTime[].enteredFull];
  SubtractMsTicksFromEventTime[@eventTime, difference];
  -- adjust time for roundoff in delta
  IF deltaTime <= maxDeltaDeltaTime THEN RETURN[deltaTime, FALSE];
  a ← [contents: deltaEventTime[value: deltaTime]];
  RETURN[0, EnterAction[@a].enteredFull]; };

-- Unconditionally enter the current absolute time. --

EnterFullTime: PROCEDURE RETURNS [enteredFull: BOOLEAN] = {
  a: eventTime ActionBody ← [contents: eventTime[eventTime: eventTime]];
  RETURN[EnterAction[@a].enteredFull]; };

EnterFullState: PROCEDURE = INLINE {
  IF ~EnterFullTime[].enteredFull AND
	 ~ EnterFullKeyState[].enteredFull THEN []←EnterMousePosition[TRUE]; };

-- a procedure to fake actions
-- currently exported to InterminalExtra.  Should be exported to Interminal
InsertAction: PUBLIC PROCEDURE [action: ClassIncreek.ActionBody] = {
	[] ← EnterAction[@action];
	};

EnterAction: PROCEDURE [action: Action] RETURNS [enteredFull: BOOLEAN←FALSE] = {
--may insert single action, or may record the full state of the input devices.
--enteredFull tells which.
  length: CARDINAL =
    -- Would be nice if something in the language would handle this.
    -- It's not only convenience and efficiency; the code should not
    -- have to enumerate the tag type here --
    WITH thisA: action SELECT FROM
	deltaEventTime, keyUp, keyDown, keyStillDown =>
	  SIZE[keyUp ActionBody],
	eventTime => SIZE[eventTime ActionBody],
	mousePosition => SIZE[mousePosition ActionBody],
	penPosition => SIZE[penPosition ActionBody],
	deltaMouse => SIZE[deltaMouse ActionBody],
	ENDCASE => ERROR Internal[1];
  action.deltaDeltaTime ← 0;
  WITH action SELECT FROM
    eventTime, deltaEventTime => NULL;
    ENDCASE => {
		[action.deltaDeltaTime, enteredFull] ← EnterTime[];
		IF enteredFull THEN RETURN[TRUE];  --full action record was made
		};
  IF ~WriteEntry[inscript, DESCRIPTOR[action, length]] THEN {
    -- page full, start a new one with complete information --
    IF ~SetWritePage[inscript] THEN RETURN[FALSE]; -- action lost because inscript could not be expanded 
    EnterFullState[];
    RETURN[TRUE]; };
  lastEventTime ← eventTime; };-- time only changes when something's been entered


-- used to change hardware's idea of where the mouse is; use with great care!!
SetMousePosition: PUBLIC PROC[pos: MousePosition] = {
    currentMousePosition↑←[pos.mouseX,pos.mouseY]; };
GetMousePosition: PUBLIC PROC RETURNS [pos: MousePosition] = {
    pos←[currentMousePosition.mouseX, display.color, currentMousePosition.mouseY];};

DefaultMouseGrain: PUBLIC PROC RETURNS [ticks: Intime.MsTicks, dots: INTEGER] = {
  RETURN[defaultGrainTicks, defaultGrainDots]; };
  
SetMouseGrain: PUBLIC PROC[ticks: Intime.MsTicks, dots: INTEGER] = {
  grainDots←MIN[defaultGrainDots, dots];
  grainMsTicks←MIN[defaultGrainTicks, ticks];
  grainTicks←(grainMsTicks+grainMsPerTick-1)/grainMsPerTick;
  grainTicksLeft←0; };

SetCursorPattern: PUBLIC PROC [cursorPattern: CursorArray] = {
   IF display.color THEN {
       ColorDisplay.SetCursorPattern[cursorPattern];
       ColorDisplay.RestoreCursor[]}
   ELSE UserTerminal.SetCursorPattern[cursorPattern];
   };

GetCursorPattern: PUBLIC PROC RETURNS [cursorPattern: CursorArray] = {
   IF display.color THEN RETURN[ColorDisplay.GetCursorPattern[]]
   ELSE RETURN[UserTerminal.GetCursorPattern[]];
   };

--this initializes the Color display.  If onLeft is false, it is assumed that
--the display is on the right.
TurnOnColorCursor: PUBLIC PROC[nbits: NAT, onLeft: BOOLEAN ← TRUE] = {
    color: POINTER TO DisplayRec;
    IF hasColor THEN TurnOffColorCursor[];
    IF onLeft THEN {display ← right; color ← left}
    ELSE {display ← left; color ← right};
    color↑ ← [xMin: 0, xMax: ColorDisplay.width-1,
               yMin: 0, yMax: ColorDisplay.height-1, color: TRUE];
    hasColor ← TRUE;
    };

TurnOffColorCursor: PUBLIC PROC = {
    IF hasColor THEN {
    --set current display to bw
      IF display.color THEN {
          display ← (IF display=left THEN right ELSE left);
          ColorDisplay.HideCursor[];
          UserTerminal.SetCursorPattern[ColorDisplay.GetCursorPattern[]];
          }; 
      hasColor ← FALSE;
      IF display=left THEN right↑ ← left↑ ELSE left↑ ← right↑;  --make both recs bw
      };
    };

--this declares on which display the pen is located
HasPen: PUBLIC PROC [display: HasPenType] = {};

-- Operating System Interface Procedures

WaitForTick: PROC = INLINE {
  WaitForEnabled[];
  UserTerminal.WaitForScanLine[0]};

WaitForEnabled: ENTRY PROC = INLINE {
  UNTIL terminalHandlerEnabled DO
    WAIT enableTerminalHandler;
    ENDLOOP};

-- Initialization --

terminalProcess: PROCESS;

StartActionRecorder: PROCEDURE = {
    -- <<Implementations of Intime and ClassInscript are assumed to be locked.>> --
    KeyboardPriority: P.Priority = 6;
    save: P.Priority = P.GetPriority[];
    --initialize the display records to reflect the standard display
    right ← @rightRep; left ← @leftRep;
    right↑ ← left ↑ ← [xMin: 0, xMax: UserTerminal.screenWidth-1,
                         yMin: 0, yMax: UserTerminal.screenHeight-1, color: FALSE];
    display ← right;	--most bw displays are to the right of the color monitor
    P.SetPriority[KeyboardPriority];
    terminalProcess←FORK ActionRecorder[];
    P.SetPriority[save];
    SpecialSpace.MakeGlobalFrameResident[LOOPHOLE[InterminalImpl, PROGRAM]];
    SpecialSpace.MakeCodeResident[LOOPHOLE[InterminalImpl, PROGRAM]];
    TerminalMultiplex.RegisterInputController[InscriptController];
    CedarSnapshot.Register[c: InscriptCPProc, r: InscriptRBProc];
    SetMouseGrain[defaultGrainTicks, defaultGrainDots];
    InscriptController[enable]; -- We're it.
    };

InscriptController: PROC[action: TerminalMultiplex.InputAction] = {
	SELECT action FROM
		enable => {
			IF terminalHandlerCurrent = 0 THEN EnableTerminalHandler[];
			terminalHandlerCurrent ← terminalHandlerCurrent + 1; };
		disable => {
		    terminalHandlerCurrent ← terminalHandlerCurrent - 1;
			IF terminalHandlerCurrent = 0 THEN DisableTerminalHandler[]; };
		ENDCASE=>ERROR; };

InscriptCPProc: CedarSnapshot.CheckpointProc = {
	DisableTerminalHandler[]; };
InscriptRBProc: CedarSnapshot.RollbackProc = {
	EnableTerminalHandler[]; };
 
terminalHandlerCurrent: INTEGER ← 0;   
terminalHandlerEnabled: BOOLEAN←FALSE;
enableTerminalHandler: CONDITION;

EnableTerminalHandler: ENTRY PROC = {
-- set things up --
  Intime.AdjustEventTime[TRUE];
  eventTime ← lastEventTime ← ReadEventTime[];
  keyState ← currentKeys↑;
  lastKeyState ← keyState;
  allUp.keyNames.blank ← keyState.keyNames.blank;
  EnterFullState[];
  terminalHandlerEnabled←TRUE;
  NOTIFY enableTerminalHandler; };
   
DisableTerminalHandler: ENTRY PROC = {
  terminalHandlerEnabled←FALSE; };


StartActionRecorder[];
}.