-- File: TerminalMultiplexImpl.mesa
-- Last edited by Levin: March 16, 1983 10:50 am

DIRECTORY
  DebuggerSwap USING [canSwap],
  DisplayFace USING [
    Background, Connect, Disconnect, hasBuffer, pagesForBitmap, SetBackground,
    SetCursorPattern, TurnOff, TurnOn],
  File USING [Capability, Create, nullCapability],
  Heap USING [systemZone],
  Inline USING [LongDiv],
  Keys USING [DownUp, KeyBits],
  MouseFace USING [SetPosition],
  PilotFileTypes USING [tAnonymousFile],
  Process USING [Detach, Milliseconds, MsecToTicks, Seconds, SetPriority, Ticks],
  ProcessOperations USING [Enter, Exit],
  ProcessPriorities USING [priorityClientHigh, priorityFrameFault, priorityRealTime],
  Runtime USING [GlobalFrame, Interrupt],
  RuntimeInternal USING [WorryCallDebugger],
  Space USING [CopyIn, CopyOut, Handle, nullHandle, VMPageNumber],
  SpecialSpace USING [MakeCodeResident, MakeGlobalFrameResident, MakeResident, MakeSwappable],
  System USING [GetGreenwichMeanTime],
  TerminalMultiplex USING [
    InputAction, InputController, SwapAction, Terminal, TerminalDesired, TerminalSwapNotifier],  -- EXPORTS only
  UserTerminal USING [
    Background, Coordinate, cursor, CursorArray, screenHeight, screenWidth,
    SetBackground, SetCursorPattern, keyboard, mouse, State],
  UserTerminalImpl USING [background, bitmapBase, bitmapSpace, LOCK, saveCursor, state],
  Volume USING [systemID];

TerminalMultiplexImpl: MONITOR 
  IMPORTS
    DebuggerSwap, DisplayFace, File, Heap, Inline, MouseFace, Process, ProcessOperations,
    Runtime, RuntimeInternal, Space, SpecialSpace, System, UserTerminal, Volume
  EXPORTS TerminalMultiplex
  SHARES UserTerminalImpl =

BEGIN OPEN TerminalMultiplex;

-- Gentle reader,
--  If kludges give you a sensuous thrill, you will like this module.
--  If you are offended by explicit, adult material, do not read further,
--  and send a message to your local system wizard indicating
--  that you do not want to receive any future listings of a similar character.


--*******************************--
--*                             *--
--* Types and Related Constants *--
--*                             *--
--*******************************--

TerminalInfo: TYPE = LONG POINTER TO TerminalInfoRecord;
TerminalInfoRecord: TYPE = RECORD [
  state: TerminalStateRecord ← [],
  notifierList: Notifier ← NIL,
  inputController: InputController ← NIL];

TerminalState: TYPE = LONG POINTER TO TerminalStateRecord;
TerminalStateRecord: TYPE = RECORD [
  backingFile: File.Capability ← File.nullCapability,
  bitmapBase: LONG POINTER ← NIL,
  bitmapSpace: Space.Handle ← Space.nullHandle,
  background: UserTerminal.Background ← white,
  state: UserTerminal.State ← disconnected,
  cursor: ARRAY [0..16) OF WORD ← ALL[0],
  cursorPosition: UserTerminal.Coordinate ←
    [x: UserTerminal.screenWidth/2, y: UserTerminal.screenHeight/2],
  mousePosition: UserTerminal.Coordinate ←
    [x: UserTerminal.screenWidth/2, y: UserTerminal.screenHeight/2]];

VirtualTerminals: TYPE = ARRAY Terminal OF TerminalInfo;

KeysRecord: TYPE = RECORD [
  type: SELECT OVERLAID * FROM
    bits => [bit: Keys.KeyBits],
    words => [word: ARRAY [0..keysWords) OF WORD],
    ENDCASE];

Notifier: TYPE = LONG POINTER TO NotifierItem;
NotifierItem: TYPE = RECORD [
  next, prev: Notifier,
  notifierProc: TerminalSwapNotifier];

keysWords: CARDINAL = SIZE[Keys.KeyBits];
firstStarKeysWord: CARDINAL = 5;


--********************--
--*                  *--
--* Global Variables *--
--*                  *--
--********************--

vt: VirtualTerminals;
currentTerminal: Terminal ← primary;
nextTerminal: Terminal;

lockDepth: NAT ← 1;  -- Note: swaps initially prevented

debuggerLockDepth: NAT ← 1;

terminalChanging: BOOLEAN ← FALSE;
terminalStateChange: CONDITION ← [timeout: 0];

AlreadyRegistered: ERROR = CODE;


--*********************--
--*                   *--
--* Public Procedures *--
--*                   *--
--*********************--

SelectTerminal: PUBLIC PROC [terminal: TerminalDesired ← swap, lock: BOOLEAN ← FALSE]
  RETURNS [worked: BOOLEAN] = {
  RETURN[NotifySwap[requested: terminal, lock: lock, wait: TRUE]]};

PreventSwaps: PUBLIC ENTRY PROC = {
  WaitUntilTerminalStable[];
  lockDepth ← lockDepth + 1};

PermitSwaps: PUBLIC ENTRY PROC = {
  IF lockDepth > 0 THEN lockDepth ← lockDepth - 1};

PreventDebuggerSwaps: PUBLIC ENTRY PROC = {
  WaitUntilTerminalStable[];
  debuggerLockDepth ← debuggerLockDepth + 1};

PermitDebuggerSwaps: PUBLIC ENTRY PROC = {
  IF debuggerLockDepth > 0 THEN debuggerLockDepth ← debuggerLockDepth - 1};

CurrentTerminal: PUBLIC ENTRY PROC
  RETURNS[terminal: Terminal, lockCount: NAT, debuggerLockCount: NAT] = {
  WaitUntilTerminalStable[];
  RETURN[currentTerminal, lockDepth, debuggerLockDepth]};

RegisterInputController: PUBLIC PROC [controller: InputController] = {
  PreventSwaps[];
  IF vt[currentTerminal].inputController ~= NIL THEN ERROR AlreadyRegistered;
  vt[currentTerminal].inputController ← controller;
  PermitSwaps[]};

UnregisterInputController: PUBLIC PROC RETURNS [controller: InputController] = {
  PreventSwaps[];
  controller ← vt[currentTerminal].inputController;
  vt[currentTerminal].inputController ← NIL;
  PermitSwaps[]};

RegisterNotifier: PUBLIC PROC [proc: TerminalSwapNotifier] = {
  notifier: Notifier ← Heap.systemZone.NEW[NotifierItem];
  head: Notifier;
  PreventSwaps[];
  IF (head ← vt[currentTerminal].notifierList) = NIL THEN
    notifier↑ ← [next: notifier, prev: notifier, notifierProc: proc]
  ELSE {
    notifier↑ ← [next: head, prev: head.prev, notifierProc: proc];
    head.prev.next ← notifier;
    head.prev ← notifier};
  vt[currentTerminal].notifierList ← notifier;
  PermitSwaps[]};

UnregisterNotifier: PUBLIC PROC [proc: TerminalSwapNotifier] = {
  head, notifier: Notifier;
  PreventSwaps[];
  IF (head ← notifier ← vt[currentTerminal].notifierList) = NIL THEN RETURN;
  DO
    IF notifier.notifierProc = proc THEN {
      notifier.prev.next ← notifier.next;
      notifier.next.prev ← notifier.prev;
      IF notifier = head THEN
        vt[currentTerminal].notifierList ←
          IF notifier.next = notifier THEN NIL ELSE notifier.next;
      Heap.systemZone.FREE[@notifier];
      EXIT};
    IF (notifier ← notifier.next) = head THEN EXIT;
    ENDLOOP;
  Heap.systemZone.FREE[@notifier];
  PermitSwaps[]};


--*******************--
--*                 *--
--* Synchronization *--
--*                 *--
--*******************--

NotifySwap: ENTRY PROCEDURE [requested: TerminalDesired, lock: BOOLEAN, wait: BOOLEAN]
  RETURNS [worked: BOOLEAN] = {
  WaitUntilTerminalStable[];
  IF lockDepth > 0 THEN RETURN[FALSE];
  nextTerminal ←
    IF requested = swap THEN
      IF currentTerminal = primary THEN alternate ELSE primary
    ELSE requested;
  IF nextTerminal ~= currentTerminal THEN {
    terminalChanging ← TRUE;
    BROADCAST terminalStateChange};
  IF wait THEN WaitUntilTerminalStable[];
  IF lock THEN lockDepth ← lockDepth + 1;
  RETURN[TRUE]};

WaitUntilTerminalStable: INTERNAL PROCEDURE = INLINE {
  WHILE terminalChanging DO WAIT terminalStateChange ENDLOOP};

WaitForTerminalChanging: ENTRY PROCEDURE RETURNS [new: Terminal] = INLINE {
  UNTIL terminalChanging DO WAIT terminalStateChange ENDLOOP;
  RETURN[nextTerminal]};

NotifyNewTerminal: ENTRY PROCEDURE =  INLINE {
  currentTerminal ← nextTerminal;
  terminalChanging ← FALSE;
  BROADCAST terminalStateChange};


--**********************--
--*                    *--
--* Multiplexor Proper *--
--*                    *--
--**********************--

Multiplexor: PROCEDURE =
  BEGIN
  -- To avoid having to trust InputControllers and SwapNotifiers too much, the
  -- actual multiplixor runs at priorityClientHigh.  Naturally, this doesn't
  -- guarantee that  a misbehaving client foreground process won't lock it out,
  -- but running at a higher priority would require residency in all the stuff
  -- we call.
  DoList: PROC [head: Notifier, action: SwapAction] = {
    item: Notifier ← head;
    IF head = NIL THEN RETURN;
    DO
      item.notifierProc[action];
      IF (item ← IF action = coming THEN item.next ELSE item.prev) = head THEN EXIT;
      ENDLOOP};
  Process.SetPriority[ProcessPriorities.priorityClientHigh];
  DO
    new: TerminalInfo ← vt[WaitForTerminalChanging[]];
    old: TerminalInfo ← vt[currentTerminal];
    IF old.inputController ~= NIL THEN old.inputController[disable];
    DoList[new.notifierList, coming];
    FlipTerminal[old: @old.state, new: @new.state];
    DoList[old.notifierList, going];
    IF new.inputController ~= NIL THEN new.inputController[enable];
    NotifyNewTerminal[];
    ENDLOOP;
  END;


ut: POINTER TO FRAME [UserTerminalImpl]
        ← LOOPHOLE[Runtime.GlobalFrame[UserTerminal.SetCursorPattern]];

FlipTerminal: PROCEDURE [old, new: TerminalState] = INLINE
  BEGIN
  -- This procedure is a first class crock.  Its purpose is to switch display, mouse,
  -- and cursor information beneath UserTerminalImpl.  Basically, it crams
  -- new values directly into the variables of UserTerminalImpl's global frame (using
  -- its monitor lock), then calls DisplayFace directly to cause the changes to take
  -- effect.  All this is needed because most of these variables are write-only, and
  -- because UserTerminalImpl thinks there is only one bitmap in the universe at a time.
  UNTIL ProcessOperations.Enter[@ut.LOCK] DO NULL ENDLOOP;
  old↑ ← [old.backingFile, ut.bitmapBase, ut.bitmapSpace, ut.background, ut.state, ut.saveCursor,
          UserTerminal.cursor↑, UserTerminal.mouse↑];
  SELECT old.state FROM
    on => {
      DisplayFace.TurnOff[];
      IF DisplayFace.hasBuffer THEN Space.CopyOut[old.bitmapSpace, [old.backingFile, 0]]
      ELSE SpecialSpace.MakeSwappable[old.bitmapSpace];
      DisplayFace.Disconnect[];
      };
    off => {
      IF DisplayFace.hasBuffer THEN Space.CopyOut[old.bitmapSpace, [old.backingFile, 0]];
      DisplayFace.Disconnect[];
      };
    disconnected => NULL;
    ENDCASE;
  [backingFile: , bitmapBase: ut.bitmapBase, bitmapSpace: ut.bitmapSpace,
   background: ut.background, state: ut.state, cursor: ut.saveCursor,
   cursorPosition: UserTerminal.cursor↑, mousePosition: ] ← new↑;
  MouseFace.SetPosition[[new.mousePosition.x, new.mousePosition.y]];
  DisplayFace.SetBackground[IF new.background = white THEN white ELSE black];
  DisplayFace.SetCursorPattern[@new.cursor];
  SELECT new.state FROM
    on => {
      DisplayFace.Connect[Space.VMPageNumber[new.bitmapSpace]];
      IF DisplayFace.hasBuffer THEN Space.CopyIn[new.bitmapSpace, [new.backingFile, 0]]
      ELSE SpecialSpace.MakeResident[new.bitmapSpace];
      DisplayFace.TurnOn[];
      };
    off => {
      DisplayFace.Connect[Space.VMPageNumber[new.bitmapSpace]];
      IF DisplayFace.hasBuffer THEN Space.CopyIn[new.bitmapSpace, [new.backingFile, 0]];
      };
    disconnected => NULL;
    ENDCASE;
  ProcessOperations.Exit[@ut.LOCK];
  END;


--********************--
--*                  *--
--* Keyboard Watcher *--
--*                  *--
--********************--

clockProbeInterval: Process.Seconds = 60;
wakeUpMS: Process.Milliseconds = 300;
wakeUpInterval: Process.Ticks = Process.MsecToTicks[wakeUpMS];
wakeUpsPerClockProbe: CARDINAL = Inline.LongDiv[LONG[clockProbeInterval]*1000, wakeUpMS];

KeyboardWatcher: PROCEDURE = {
  -- Note:  This procedure is forked as a separate process by the main
  -- body code.  It watches for the key combinations that request a terminal
  -- swap and a call on the debugger.  Since this process runs at high
  -- priority, everything it touches must be pinned.
  keyboard: LONG POINTER TO Keys.KeyBits = LOOPHOLE[UserTerminal.keyboard];
  Pause: ENTRY PROCEDURE = INLINE {WAIT aShortTime};
  DebuggerEnabled: ENTRY PROCEDURE RETURNS [BOOLEAN] = INLINE {RETURN[debuggerLockDepth = 0]};
  aShortTime: CONDITION ← [timeout: wakeUpInterval];
  wakeUps: CARDINAL ← 0;
  originalCanSwap: BOOL = DebuggerSwap.canSwap;
  swapKeys, ctrlSwatKeys, remoteDebuggerKeys, oldKeys, newKeys: KeysRecord;
  swapKeys.bit ← ALL[Keys.DownUp[up]];
  swapKeys.bit[Ctrl] ← down;
  ctrlSwatKeys.word ← swapKeys.word;
  swapKeys.bit[RightShift] ← swapKeys.bit[LeftShift] ← ctrlSwatKeys.bit[Spare3] ← down;
  remoteDebuggerKeys ← ctrlSwatKeys;
  remoteDebuggerKeys.bit[Spare1] ← down;
  Process.SetPriority[ProcessPriorities.priorityRealTime];
  oldKeys.bit ← keyboard↑;
  DO
    changed: BOOLEAN ← FALSE;
    swap: BOOLEAN ← TRUE;
    goToDebugger, goToRemoteDebugger: BOOLEAN ← DebuggerEnabled[];
    newKeys.bit ← keyboard↑;
    FOR word: CARDINAL IN [0..firstStarKeysWord) DO
      thisWord: WORD = newKeys.word[word];
      IF thisWord ~= oldKeys.word[word] THEN changed ← TRUE;
      IF swap AND (thisWord ~= swapKeys.word[word]) THEN swap ← FALSE;
      IF goToDebugger AND (thisWord ~= ctrlSwatKeys.word[word]) THEN
         goToDebugger ← FALSE;
      IF goToRemoteDebugger AND (thisWord ~= remoteDebuggerKeys.word[word]) THEN
         goToRemoteDebugger ← FALSE;
      ENDLOOP;
    IF changed THEN {
      oldKeys ← newKeys;
      SELECT TRUE FROM
        swap => [] ← NotifySwap[swap, FALSE, FALSE];
        goToDebugger => {
          -- Note:  This means "go to the default (world swap) debugger", which may be
          -- either local or remote depending on the state of affairs at initialization time.
          DebuggerSwap.canSwap ← originalCanSwap;
          Runtime.Interrupt[];
          };
        goToRemoteDebugger => {
          DebuggerSwap.canSwap ← FALSE;
          Runtime.Interrupt[];
          };
        ENDCASE;
      };
    IF wakeUps = wakeUpsPerClockProbe THEN {[] ← System.GetGreenwichMeanTime[]; wakeUps ← 0}
    ELSE wakeUps ← wakeUps + 1;
    Pause[];
    ENDLOOP};

WatcherOfLastResort: ENTRY PROCEDURE = {
  -- Note:  This procedure is forked as a separate process by the main
  -- body code.  It watches for the key combinations that request a panic
  -- call on the debugger.  Since this process runs at the highest priority
  -- everything it touches must be pinned and it can't even call a procedure
  -- (RuntimeInternal.WorryCallDebugger is a fixed frame).  We do this exercise
  -- in masochism to survive even if others insist on writing code to run at
  -- priority 6, but forget to pin everything they should.
  keyboard: LONG POINTER TO Keys.KeyBits = LOOPHOLE[UserTerminal.keyboard];
  oneTick: CONDITION ← [timeout: wakeUpInterval];
  Process.SetPriority[ProcessPriorities.priorityFrameFault];
  -- Look Ma, no procedure calls...
  DO
    WAIT oneTick;
    IF keyboard[Ctrl] = down AND keyboard[LeftShift] = down AND keyboard[Spare3] = down AND
     debuggerLockDepth = 0 THEN {
      oldCanSwap: BOOL = DebuggerSwap.canSwap;
      IF keyboard[Spare1] = down THEN DebuggerSwap.canSwap ← FALSE;
      RuntimeInternal.WorryCallDebugger["Whew!"L];
      DebuggerSwap.canSwap ← oldCanSwap;
      };
    ENDLOOP};



--*************--
--*           *--
--* Main Body *--
--*           *--
--*************--


Initialize: PROCEDURE =
  BEGIN
  current: TerminalState;
  vt[primary] ← Heap.systemZone.NEW[TerminalInfoRecord ←
    [state: [cursor: [001600B, 003700B, 007740B, 005640B, 001600B, 003700B, 007740B, 015660B,
		      011620B, 003700B, 007740B, 015660B, 031630B, 023710B, 003700B, 007740B]]]];
  vt[alternate] ← Heap.systemZone.NEW[TerminalInfoRecord ←
    [state: [cursor: [160016B, 015660B, 002100B, 105242B, 113322B, 060034B, 021610B, 062514B,
		      120412B, 021610B, 062514B, 120412B, 111222B, 030030B, 046544B, 041204B]]]];
  vt[special] ← Heap.systemZone.NEW[TerminalInfoRecord ←
    [state: [cursor: [001600B, 003700B, 007740B, 005640B, 001600B, 003700B, 007740B, 015660B,
		      011620B, 003700B, 007740B, 015660B, 031630B, 023710B, 003700B, 007740B]]]];
  IF DisplayFace.hasBuffer THEN {
    -- we must create backing files for saving bitmaps during terminal swaps
    FOR t: Terminal IN Terminal DO
      vt[t].state.backingFile ←
        File.Create[Volume.systemID, DisplayFace.pagesForBitmap, PilotFileTypes.tAnonymousFile];
      ENDLOOP};
  current ← @vt[currentTerminal].state;
  Process.Detach[FORK Multiplexor[]];
  -- The initial UserTerminal state is assumed to be disconnected.
  MouseFace.SetPosition[[current.mousePosition.x, current.mousePosition.y]];
  [] ← UserTerminal.SetBackground[current.background];
  UserTerminal.SetCursorPattern[current.cursor];
  UserTerminal.cursor↑ ← current.cursorPosition;
  -- The only code that needs to be resident is:
  --  KeyboardWatcher, NotifySwap, WatcherOfLastResort
  -- however, until we package this thing, it is sufficient
  -- to pin the whole module (which isn't very big anyway).
  SpecialSpace.MakeCodeResident[TerminalMultiplexImpl];
  SpecialSpace.MakeGlobalFrameResident[TerminalMultiplexImpl];
  Process.Detach[FORK WatcherOfLastResort[]];
  Process.Detach[FORK KeyboardWatcher[]];
  END;

Initialize[];

END.