-- ConversationImpl.mesa
-- McGregor, February 2, 1983 12:38 pm
-- Paul Rovner, March 21, 1983 2:46 pm
-- Last Edited by: Greene, April 20, 1984 3:39:11 pm PST

-- Last Edited by: Rumph, June 15, 1984 4:02:16 pm PDT
-- Last Edited by: Nickell, June 26, 1984 2:10:13 pm PDT

DIRECTORY
DefaultRegistry USING [MakeRegistryExplicit],
IO USING [GetLineRope],
InputFocus USING [GetInputFocus],
Labels USING [Set],
Menus USING[MenuProc, CreateMenu],
Process USING [Detach, Pause, SecondsToTicks],
Rope USING[ROPE, Concat, Equal, Translate, Cat, Length, Substr],
RopeReader USING [Backwards, Create, SetPosition, Ref],
TalkerOpsRpcControl USING[InterfaceRecord, ImportNewInterface],
TalkerPrivate USING[Handle, Conversant, ConversantObject, EnumerateConversations,
GetLocalName],
TextLooks USING [Looks],
TEditLocks USING [LockDocAndTdd, UnlockDocAndTdd],
TEditDocument USING [TEditDocumentData],
TextNode USING [Location, LastLocWithin, NarrowToTextNode],
TextEdit USING [GetRope, InsertRope],
TiogaOps USING [CallWithLocks, Ref],
TypeScript USING [WaitUntilCharsAvail],
ViewerClasses USING [Viewer],
ViewerOps USING [PaintViewer, BlinkIcon],
ViewerTools USING [GetSelectionContents];

ConversationImpl: CEDAR MONITOR -- protects individual conversations
LOCKS h.LOCK USING h: Handle
IMPORTS DefaultRegistry, IO, Labels, Menus, Process, Rope, TalkerOpsRpcControl, TalkerPrivate,
TypeScript, ViewerOps, ViewerTools, TextNode, RopeReader, TextEdit,
TEditLocks, TiogaOps, InputFocus
EXPORTS TalkerPrivate
= BEGIN OPEN Rope, TalkerPrivate;

ropeReader: RopeReader.Ref = RopeReader.Create[];

 sentMark: ROPE ← " \t";   --Used to identify ropes that were received (space tab)

-- ******* Processes

-- FORK'd for each conversation; performs the only remote calls
Puller: PUBLIC PROC[h: Handle] = {
UNTIL PullerStopRequested[h]
DO TypeScript.WaitUntilCharsAvail[h.ts ! ANY => EXIT];
IF PullerStopRequested[h] THEN EXIT;
PullLine[h];
ENDLOOP;
};

PullerStopRequested: ENTRY PROC[h: Handle] RETURNS[BOOL] = {
ENABLE UNWIND => NULL;
RETURN[h.pullerStopRequested];
}; -- end PullerStopRequested

NewOutgoingLine: INTERNAL PROC[h: Handle, c: Conversant, line: ROPE] = {
prev: LIST OF ROPENIL;
FOR x: LIST OF ROPE ← c.outGoingLines, x.rest UNTIL x = NIL DO prev ← x ENDLOOP;
IF prev = NIL
THEN c.outGoingLines ← CONS[line, NIL]
ELSE prev.rest ← CONS[line, NIL];
NOTIFY c.newOutGoingLine;
};

PullLine: PROC[h: Handle] = {
proc: INTERNAL PROC[c: Conversant] RETURNS[stop: BOOLFALSE] =
{NewOutgoingLine[h, c, line]};
line: ROPE ← h.tsInStream.GetLineRope[! ANY => GOTO return];
WHILE Length[line]>Length[sentMark] AND
    Equal[Substr[line, 0, Length[sentMark]], sentMark] DO
  line ← h.tsInStream.GetLineRope[! ANY => GOTO return];
  ENDLOOP;
IF line = NIL THEN RETURN;
line ← Concat[line, "\n"];
[] ← EnumerateConversants[h, proc];
EXITS return => RETURN;
};

-- FORKED for each conversant
SendOutGoingLines: PUBLIC PROC[h: Handle, c: Conversant] = {
DO {
line: ROPE = WaitForLineToSend[h, c];
IF line = NIL THEN EXIT; -- kill this process
TRUSTED {c.ir.PutMyLine[GetLocalName[], line ! ANY => GOTO disConnected]};
EXITS disConnected => {DestroyConversant[h, c]; RemakeStatusLabel[h]; EXIT}
};
ENDLOOP;
};

WaitForLineToSend: ENTRY PROC[h: Handle, c: Conversant] RETURNS[line: ROPENIL] = {
ENABLE UNWIND => NULL;
UNTIL c.outGoingLines # NIL OR c.senderToStop DO WAIT c.newOutGoingLine ENDLOOP;
IF c.senderToStop THEN RETURN;
line ← c.outGoingLines.first;
c.outGoingLines ← c.outGoingLines.rest;
};


-- Blinker process: FORKED for this module; blinks talker icons that have new messages
Blinker: PROC = {
DO p: PROC[h: Handle] RETURNS[stop: BOOLFALSE] = {MaybeBlinkIt[h]};
[] ← EnumerateConversations[p];
TRUSTED {Process.Pause[Process.SecondsToTicks[6]]};
ENDLOOP
};


MaybeBlinkIt: ENTRY PROC[h: Handle] = {
ENABLE UNWIND => NULL;
IF h.shouldBlink AND (NOT h.container.destroyed) AND h.container.iconic
THEN ViewerOps.BlinkIcon[h.container];
};

BlinkOn: ENTRY PROC[h: Handle] = {
ENABLE UNWIND => NULL;
h.shouldBlink ← TRUE;
};

BlinkOff: PUBLIC ENTRY PROC[h: Handle] = {
ENABLE UNWIND => NULL;
h.shouldBlink ← FALSE;
};

-- ******* menu procedures

ShowStatusHit: PUBLIC Menus.MenuProc = {
-- [parent: REF, clientData: REF, mouseButton: MouseButton, shift, control: BOOL]
RemakeStatusLabel[NARROW[clientData, Handle]];
};

JoinConversationHit: PUBLIC Menus.MenuProc = {
-- [parent: REF, clientData: REF, mouseButton: MouseButton, shift, control: BOOL]
OPEN D: DefaultRegistry;
h: Handle ← NARROW[clientData, Handle];
conversantName: ROPED.MakeRegistryExplicit[
ViewerTools.GetSelectionContents[],
LIST[$Talk, $Default]
];
JoinConversation[h, conversantName];
RemakeStatusLabel[h];
RemoveConversantFromOtherConversations[h, conversantName];
}; -- end JoinConversationHit

JoinConversation: PUBLIC ENTRY PROC[h: Handle, conversantName: ROPE] = {
ENABLE UNWIND => NULL;
ir: TalkerOpsRpcControl.InterfaceRecord ← NIL;

IF InnerFindConversant[h, conversantName] # NIL THEN RETURN; -- redundant. No-op
ir ← TalkerOpsRpcControl.ImportNewInterface
[interfaceName: [instance: LowerRope[conversantName]]
! ANY => CONTINUE];
IF ir = NIL THEN RETURN;
h.conversants ← CONS[NEW[ConversantObject ← [ir: ir, conversantName: conversantName]],
h.conversants];
FOR cl: LIST OF Conversant ← h.conversants, cl.rest UNTIL cl = NIL
DO TRUSTED{Process.Detach[FORK cl.first.ir.JoinWith[GetLocalName[], conversantName]]};
ENDLOOP;
TRUSTED {Process.Detach[FORK SendOutGoingLines[h, h.conversants.first]]};
};

-- ******* destruction procedures

KillConversation: PUBLIC ENTRY PROC[h: Handle] = {
ENABLE UNWIND => NULL;
h.pullerStopRequested ← TRUE;
DestroyConversants[h];
FOR cl: LIST OF Conversant ← h.conversants, cl.rest UNTIL cl = NIL
DO TRUSTED {cl.first.ir.DealMeOut[GetLocalName[] ! ANY => CONTINUE]} ENDLOOP;
h.conversants ← NIL;
};

RemoveConversantFromOtherConversations:
PUBLIC PROC[h: Handle, conversantName: ROPE] = {
p: PROC[otherH: Handle] RETURNS[stop: BOOLFALSE] = {
c: Conversant;
IF otherH = h THEN RETURN;
c ← FindConversant[otherH, conversantName];
IF c # NIL THEN {DestroyConversant[otherH, c]; RETURN[TRUE]};
};
[] ← EnumerateConversations[p]; -- remove conversant from any other conversation
};

EnumerateConversants: ENTRY PROC[h: Handle,
proc: PROC[Conversant] RETURNS[stop: BOOL]]
RETURNS[c: Conversant ← NIL] = {
ENABLE UNWIND => NULL;
FOR cl: LIST OF Conversant ← h.conversants, cl.rest UNTIL cl = NIL
DO IF proc[cl.first] THEN RETURN[cl.first];
ENDLOOP;
}; -- end EnumerateConversants
DestroyConversants: INTERNAL PROC[h: Handle] = {
FOR cl: LIST OF Conversant ← h.conversants, cl.rest UNTIL cl = NIL
DO cl.first.senderToStop ← TRUE; NOTIFY cl.first.newOutGoingLine ENDLOOP;
h.container.menu ← Menus.CreateMenu[lines: 1];
ViewerOps.PaintViewer[h.container, all ! ANY => CONTINUE]
};
DestroyConversant: ENTRY PROC[h: Handle, c: Conversant] = {
prev: LIST OF Conversant ← NIL;
FOR cl: LIST OF Conversant ← h.conversants, cl.rest UNTIL cl = NIL
DO
IF cl.first = c
THEN {IF prev = NIL THEN h.conversants ← cl.rest ELSE prev.rest ← cl.rest; EXIT};
prev ← cl;
ENDLOOP;
c.senderToStop ← TRUE;
NOTIFY c.newOutGoingLine;
IF h.conversants = NIL THEN DestroyConversants[h];
};
-- ******* viewer output

PaintLine: PUBLIC PROC[h: Handle, line: ROPE] = BEGIN
tdd: TEditDocument.TEditDocumentData ← NARROW[h.ts.data];
caret: BOOL = (InputFocus.GetInputFocus[].owner=h.ts);
MakeEdit: PROC [root: TiogaOps.Ref] = BEGIN
   italic: TextLooks.Looks;
   endPos: TextNode.Location ← TextNode.LastLocWithin[tdd.text];
   italic['i] ← TRUE;
   IF endPos.where#0 THEN BEGIN-- find the last CR
   RopeReader.SetPosition[ropeReader,
          TextEdit.GetRope[TextNode.NarrowToTextNode[endPos.node]],
          endPos.where];
   WHILE endPos.where>0 AND RopeReader.Backwards[ropeReader] # 15C DO
    endPos.where ← endPos.where-1;
    ENDLOOP;
    END;
   [] ← TextEdit.InsertRope[root: tdd.text, dest: TextNode.NarrowToTextNode[endPos.node],
          inherit: FALSE, looks: italic, rope: line, destLoc: endPos.where];
   IF caret THEN h.ts.class.notify[h.ts, LIST[$NextPlaceholder]]; -- hack; force caret to end
   END;
line ← Concat[sentMark, line];
IF h.ts.destroyed THEN RETURN;
IF caret THEN TiogaOps.CallWithLocks[MakeEdit]
ELSE BEGIN
 [] ← TEditLocks.LockDocAndTdd[tdd, "Talker"];
 MakeEdit[NIL];
 TEditLocks.UnlockDocAndTdd[tdd];
END;
IF h.container.iconic THEN h.shouldBlink ← TRUE;
END;

RemakeStatusLabel: PUBLIC ENTRY PROC[h: Handle] = {
ENABLE UNWIND => NULL;
Labels.Set[h.statusLabel, InnerComputeStatus[h]];
ViewerOps.PaintViewer[h.statusLabel, all];
}; -- end NotifyLineRead


-- ******* information, search procs

ComputeStatus: PUBLIC ENTRY PROC[h: Handle] RETURNS[ROPE] = {
ENABLE UNWIND => NULL;
RETURN[InnerComputeStatus[h]];
};

InnerComputeStatus: INTERNAL PROC[h: Handle] RETURNS[status: ROPE ← "STATUS: "] = {
IF h.conversants = NIL
THEN status ← Concat[status, "No Conversants"]
ELSE {status ← Cat[status, "talking with "];
FOR cl: LIST OF Conversant ← h.conversants, cl.rest UNTIL cl = NIL
DO status ← Cat[status,
cl.first.conversantName,
IF cl.first.outGoingLines # NIL
THEN "(attempting to send line) "
ELSE " "];
ENDLOOP};
};

IsViewer: PUBLIC ENTRY PROC[h: Handle, viewer: ViewerClasses.Viewer] RETURNS[BOOL] = {
ENABLE UNWIND => NULL;
RETURN[h.container = viewer];
};

FindConversant: PUBLIC ENTRY PROC[h: Handle, conversantName: ROPE]
RETURNS[Conversant] = {
ENABLE UNWIND => NULL;
RETURN[InnerFindConversant[h, conversantName]];
};

InnerFindConversant: INTERNAL PROC[h: Handle, conversantName: ROPE]
RETURNS[c: Conversant ← NIL] = {
FOR cl: LIST OF Conversant ← h.conversants, cl.rest UNTIL cl = NIL
DO IF Equal[conversantName, cl.first.conversantName, FALSE]
THEN RETURN[cl.first];
ENDLOOP;
};

-- ******* utility procedures

LowerRope: PUBLIC PROC[r: ROPE] RETURNS[ROPE] = {
trans: PROC[old: CHAR] RETURNS[new: CHAR] = {RETURN[IF old IN ['A..'Z] THEN old + ('a - 'A) ELSE old]};
RETURN[Translate[base: r, translator: trans]];
}; -- end LowerRope


-- START HERE

-- FORK the blinker
TRUSTED{Process.Detach[FORK Blinker[]]};

END.