-- File: EchoUserTool.mesa,  Last Edit: HGM  January 30, 1981  2:58 PM
-- Please don't forget to update the herald....

DIRECTORY
  Inline USING [BITNOT, BITAND],
  Process USING [SetPriority],
  Storage USING [Node, String, FreeNodeNil, FreeString],
  String USING [AppendString, AppendChar, AppendNumber],
  Time USING [AppendCurrent, Current],

  Event USING [Item, Reason, AddNotifier],
  FormSW USING [
    ClientItemsProcType, ProcType, AllocateItemDescriptor, newLine, Display,
    FindItem, CommandItem, BooleanItem, StringItem, NumberItem],
  MsgSW USING [Post],
  Put USING [Char, CR, Text, Line, LongDecimal, LongNumber],
  Tool USING [
    Create, MakeSWsProc, UnusedLogName, MakeMsgSW, MakeFormSW, MakeFileSW,
    AddThisSW],
  ToolWindow USING [TransitionProcType, DisplayProcType, CreateSubwindow],
  Window USING [Handle, Box, DisplayData, DisplayInvert, DisplayWhite],

  StatsDefs USING [StatPrintCurrent, StatReady, StatSince],
  PupDefs USING [
    PupPackageMake, PupPackageDestroy, GetFreePupBuffer, ReturnFreePupBuffer,
    PupBuffer, PupSocket, PupSocketDestroy, PupSocketMake, SecondsToTocks,
    SetPupContentsWords, GetPupContentsBytes, DataWordsPerPupBuffer,
    AppendPupAddress, GetPupAddress, PupNameTrouble],
  PupTypes USING [PupAddress, fillInSocketID, echoSoc, maxDataWordsPerGatewayPup];

EchoUserTool: PROGRAM
  IMPORTS
    Inline, Process, Storage, String, Time, Event, FormSW, MsgSW, Put, Tool,
    ToolWindow, Window, StatsDefs, PupDefs =
  BEGIN OPEN PupDefs, PupTypes;

  msg, form, boxes, log: Window.Handle;
  eventItem: Event.Item ← [eventMask: 177777B, eventProc: Broom];

  -- Be sure to initialize it when it is allocated!!!!
  data: POINTER TO Data ← NIL; -- NIL when we are inactive

  Data: TYPE = RECORD [
    length: CARDINAL ← 100,
    fixedLength: BOOLEAN ← FALSE,
    where: PupAddress ← [[0], [0], PupTypes.echoSoc],

    picks: ARRAY [0..16] OF CARDINAL ← ALL[0],
    drops: ARRAY [0..16] OF CARDINAL ← ALL[0],
    sent, good, missed, late, bad, horrible, error, words: LONG CARDINAL ← 0,
    pleaseStop: BOOLEAN ← FALSE,
    alignTheFink: WORD ← NULL,
    lowPriority: BOOLEAN ← FALSE,
    alignTheFinkAgain: WORD ← NULL,
    verbose: BOOLEAN ← FALSE,
    alignTheFinkYetAgain: WORD ← NULL,
    doStats: BOOLEAN ← FALSE,
    alignTheFinkStillAgain: WORD ← NULL,
    checkit: BOOLEAN ← TRUE,
    echoer: PROCESS ← NULL,
    indicator: {left, right, off} ← off,
    running: BOOLEAN ← FALSE,
    target: STRING ← NULL];

  Initialize: PROCEDURE =
    BEGIN
    [] ← Tool.Create[
      name: "Pup Echo User of January 30, 1981"L, makeSWsProc: MakeSWs,
      clientTransition: ClientTransition];
    Event.AddNotifier[@eventItem];
    END;

  EchoUserOn: PROCEDURE =
    BEGIN
    IF data.length > PupDefs.DataWordsPerPupBuffer[] THEN
      BEGIN MsgSW.Post[msg, "Length is too long."L]; RETURN; END;
    WriteCR[];
    WriteCurrentDateAndTime[];
    WriteString["  Echoing to "L];
    IF ~FindPath[] THEN RETURN;
    data.running ← TRUE;
    UpdatePicture[];
    data.echoer ← FORK DoIt[];
    END;

  EchoUserOff: PROCEDURE =
    BEGIN
    IF data = NIL THEN RETURN;
    data.pleaseStop ← TRUE;
    JOIN data.echoer[];
    data.running ← data.pleaseStop ← FALSE;
    UpdatePicture[];
    END;

  UpdatePicture: PROCEDURE =
    BEGIN
    FormSW.FindItem[form, startIX].flags.invisible ← data.running;
    FormSW.FindItem[form, stopIX].flags.invisible ← ~data.running;
    FormSW.Display[form];
    END;

  FindPath: PROCEDURE RETURNS [BOOLEAN] =
    BEGIN OPEN data;
    WriteString[target];
    WriteChar['=];
    GetPupAddress[
      @where, target !
      PupNameTrouble =>
	BEGIN MsgSW.Post[msg, e]; WriteLine[e]; GOTO Trouble; END];
    PrintPupAddress[where];
    WriteLine["."L];
    RETURN[TRUE];
    EXITS Trouble => RETURN[FALSE];
    END;

  ClearCounters: PROCEDURE =
    BEGIN OPEN data;
    sent ← good ← missed ← late ← bad ← horrible ← error ← words ← 0;
    picks ← ALL[0];
    drops ← ALL[0];
    END;

  AddToHist: PROCEDURE [hist: POINTER TO ARRAY [0..16] OF CARDINAL, bits: WORD] =
    BEGIN OPEN data;
    i: CARDINAL;
    IF bits = 0 THEN RETURN;
    SELECT bits FROM
      1 => i ← 15;
      2 => i ← 14;
      4 => i ← 13;
      10B => i ← 12;
      20B => i ← 11;
      40B => i ← 10;
      100B => i ← 9;
      200B => i ← 8;
      400B => i ← 7;
      1000B => i ← 6;
      2000B => i ← 5;
      4000B => i ← 4;
      10000B => i ← 3;
      20000B => i ← 2;
      40000B => i ← 1;
      100000B => i ← 0;
      ENDCASE => i ← 16;
    hist[i] ← hist[i] + 1;
    END;

  PrintSummary: PROCEDURE [howLong: LONG CARDINAL] =
    BEGIN OPEN data;
    WriteCR[];
    WriteLongDecimal[sent];
    WriteLine[" packets sent."L];
    IF howLong # 0 THEN
      BEGIN
      WriteLongDecimal[sent/howLong];
      WriteLine[" packets per second."L];
      WriteLongDecimal[16*words/howLong];
      WriteLine[" data bits per second."L];
      END;
    IF sent # 0 THEN
      BEGIN
      ShowPercent[good, "good packets received."L];
      ShowPercent[missed, "packets missed."L];
      ShowPercent[late, "late (or??) packets received."L];
      ShowPercent[bad, "bad packets received."L];
      ShowPercent[horrible, "packets received with more than 10 words wrong."L];
      ShowPercent[error, "error packets received."L];
      END;
    IF bad # 0 THEN
      BEGIN
      i: CARDINAL;
      x: WORD ← 100000B;
      WriteCR[];
      WriteLine["   Bit  Picked Dropped"L];
      FOR i IN [0..16] DO
	IF picks[i] # 0 OR drops[i] # 0 THEN
	  BEGIN
	  IF i = 16 THEN WriteString[" Other"L] ELSE O6[x];
	  D8[picks[i]];
	  D8[drops[i]];
	  WriteCR[];
	  END;
	x ← x/2;
	ENDLOOP;
      END;
    END;

  ShowPercent: PROCEDURE [n: LONG CARDINAL, s: STRING] =
    BEGIN OPEN data;
    IF n = 0 THEN RETURN;
    WriteLongDecimal[n];
    WriteString[" ("L];
    WriteLongDecimal[n*100/sent];
    WriteString["%) "L];
    WriteLine[s];
    END;

  DoIt: PROCEDURE =
    BEGIN OPEN data;
    k: CARDINAL;
    b: PupBuffer;
    cycle, maxLength, myLength: CARDINAL;
    packetNumber: CARDINAL ← LAST[CARDINAL];
    mySoc: PupSocket ← PupSocketMake[fillInSocketID, where, SecondsToTocks[2]];
    start, stop: LONG CARDINAL;
    maxLength ← MIN[
      PupDefs.DataWordsPerPupBuffer[], PupTypes.maxDataWordsPerGatewayPup];
    IF lowPriority THEN Process.SetPriority[0];
    ClearCounters[];
    myLength ← MIN[length, maxLength];
    IF fixedLength THEN
      BEGIN
      WriteString["Packet length is "L];
      WriteDecimal[myLength];
      WriteLine[" words."L];
      END;
    SetupBoxes[];
    IF doStats THEN StatsDefs.StatReady[];
    start ← Time.Current[];
    UNTIL pleaseStop DO
      -- NB: No check for short buffers
      FOR cycle IN [0..256) UNTIL pleaseStop DO
	b ← GetFreePupBuffer[];
	myLength ← IF fixedLength THEN maxLength ELSE cycle;
	FOR k IN [0..myLength) DO b.pupWords[k] ← k + cycle*400B; ENDLOOP;
	b.pupID.a ← b.pupID.b ← (packetNumber ← packetNumber + 1);
	b.pupType ← echoMe;
	SetPupContentsWords[b, myLength];
	mySoc.put[b];
	sent ← sent + 1;
	words ← words + myLength;
	UNTIL (b ← mySoc.get[]) = NIL DO
	  -- Until timeout, or we find the expected one
	  SELECT TRUE FROM
	    (b.pupType = error) =>
	      BEGIN error ← error + 1; WriteCR[]; PrintErrorPup[b]; END;
	    ((b.pupType # iAmEcho) OR (b.pupID.a # packetNumber) OR
	      (b.pupID.b # packetNumber) OR (GetPupContentsBytes[b] # 2*myLength))
	      => BEGIN late ← late + 1; WriteChar['#]; END;
	    ENDCASE =>
	      BEGIN
	      hits: CARDINAL ← 0;
	      FlipBoxes[];
	      IF data.checkit THEN
		FOR k IN [0..myLength) DO
		  IF b.pupWords[k] # k + cycle*400B THEN
		    BEGIN OPEN Inline;
		    expected, found, picked, dropped: WORD;
		    IF hits = 0 THEN
		      BEGIN
		      WriteCR[];
		      WriteCurrentDateAndTime[];
		      WriteString["  Data compare error(s) on packet number "L];
		      WriteDecimal[packetNumber];
		      WriteLine["."L];
		      WriteLine["Idx Expected    Found   Picked  Dropped"L];
		      END;
		    expected ← k + cycle*400B;
		    found ← b.pupWords[k];
		    picked ← BITAND[found, BITNOT[expected]];
		    dropped ← BITAND[expected, BITNOT[found]];
		    AddToHist[@picks, picked];
		    AddToHist[@drops, dropped];
		    IF hits < 10 THEN
		      BEGIN
		      O3[k];
		      O9[expected];
		      O9[found];
		      O9[picked];
		      O9[dropped];
		      WriteCR[];
		      END;
		    hits ← hits + 1;
		    END;
		  ENDLOOP;
	      IF hits = 0 THEN good ← good + 1 ELSE bad ← bad + 1;
	      IF hits = 0 AND verbose THEN WriteChar['!];
	      IF hits > 10 THEN
		BEGIN horrible ← horrible + 1; WriteLine["...."L]; END;
	      EXIT; -- found the expected one

	      END;
	  ReturnFreePupBuffer[b];
	  ENDLOOP;
	IF b # NIL THEN ReturnFreePupBuffer[b]
	ELSE BEGIN missed ← missed + 1; WriteChar['?]; END;
	ENDLOOP;
      IF verbose THEN WriteCR[] ELSE WriteChar['.];
      ENDLOOP;
    stop ← Time.Current[];
    WriteCR[];
    PupSocketDestroy[mySoc];
    SetDownBoxes[];
    IF doStats THEN StatsDefs.StatSince[log];
    PrintSummary[stop - start];
    END;

  -- IO things

  WriteChar: PROCEDURE [c: CHARACTER] = BEGIN Put.Char[log, c]; END;

  WriteCR: PROCEDURE = BEGIN Put.CR[log]; END;

  WriteString: PROCEDURE [s: STRING] = BEGIN Put.Text[log, s]; END;

  WriteLine: PROCEDURE [s: STRING] = BEGIN Put.Line[log, s]; END;

  WriteLongDecimal: PROCEDURE [n: LONG CARDINAL] =
    BEGIN Put.LongDecimal[log, n]; END;

  WriteDecimal: PROCEDURE [n: CARDINAL] = INLINE BEGIN WriteNumber[n, 10, 0]; END;

  WriteOctal: PROCEDURE [n: CARDINAL] = INLINE BEGIN WriteNumber[n, 8, 0]; END;

  WriteNumber: PROCEDURE [n, radix, width: CARDINAL] = INLINE
    BEGIN
    temp: STRING = [25];
    String.AppendNumber[temp, n, radix];
    THROUGH [temp.length..width) DO WriteChar[' ]; ENDLOOP;
    WriteString[temp];
    END;

  D8: PROCEDURE [n: CARDINAL] = BEGIN WriteNumber[n, 10, 8]; END;

  O3: PROCEDURE [n: CARDINAL] = BEGIN WriteNumber[n, 8, 3]; END;

  O6: PROCEDURE [n: CARDINAL] = BEGIN WriteNumber[n, 8, 3]; END;

  O9: PROCEDURE [n: CARDINAL] = BEGIN WriteNumber[n, 8, 9]; END;

  WriteCurrentDateAndTime: PROCEDURE =
    BEGIN time: STRING = [20]; Time.AppendCurrent[time]; WriteString[time]; END;

  PrintPupAddress: PROCEDURE [a: PupAddress] =
    BEGIN temp: STRING = [40]; AppendPupAddress[temp, a]; WriteString[temp]; END;

  PrintErrorPup: PUBLIC PROCEDURE [b: PupDefs.PupBuffer] =
    BEGIN
    i, len: CARDINAL;
    temp: STRING = [100];
    String.AppendString[temp, "Error Pup, code="L];
    String.AppendNumber[temp, LOOPHOLE[b.errorCode], 8];
    String.AppendString[temp, ", from: "L];
    AppendPupAddress[temp, b.source];
    String.AppendString[temp, ": "L];
    len ← PupDefs.GetPupContentsBytes[b];
    FOR i IN [0..len - 2*(10 + 1 + 1)) UNTIL temp.length = temp.maxlength DO
      String.AppendChar[temp, b.errorText[i]]; ENDLOOP;
    WriteLine[temp];
    MsgSW.Post[msg, temp];
    END;

  indicatorBox: Window.Box = [[25, 10], [16, 16]];
  DisplayBoxes: ToolWindow.DisplayProcType =
    BEGIN
    pattern: ARRAY [0..1] OF ARRAY [0..8) OF WORD;
    left: WORD = 177400B;
    right: WORD = 000377B;
    SELECT data.indicator FROM
      left => pattern ← [ALL[left], ALL[right]];
      right => pattern ← [ALL[right], ALL[left]];
      off => pattern ← [ALL[0], ALL[0]];
      ENDCASE;
    Window.DisplayData[window, indicatorBox, @pattern, 1]
    END;

  SetupBoxes: PROCEDURE = BEGIN data.indicator ← left; DisplayBoxes[boxes]; END;

  FlipBoxes: PROCEDURE =
    BEGIN
    SELECT data.indicator FROM
      left => data.indicator ← right;
      off, right => data.indicator ← left;
      ENDCASE;
    Window.DisplayInvert[boxes, indicatorBox];
    END;

  SetDownBoxes: PROCEDURE =
    BEGIN data.indicator ← off; Window.DisplayWhite[boxes, indicatorBox]; END;

  MakeBoxesSW: PROCEDURE [window: Window.Handle] =
    BEGIN
    boxes ← ToolWindow.CreateSubwindow[parent: window, display: DisplayBoxes];
    boxes.box.dims.h ← 36;
    Tool.AddThisSW[window: window, sw: boxes, swType: vanilla];
    END;

  Start: FormSW.ProcType = BEGIN EchoUserOn[]; END;

  Stop: FormSW.ProcType = BEGIN EchoUserOff[]; END;

  Ready: FormSW.ProcType = BEGIN StatsDefs.StatReady[]; END;

  Since: FormSW.ProcType = BEGIN StatsDefs.StatSince[log]; END;

  Totals: FormSW.ProcType = BEGIN StatsDefs.StatPrintCurrent[log]; END;

  MakeSWs: Tool.MakeSWsProc =
    BEGIN
    logFileName: STRING = [40];
    msg ← Tool.MakeMsgSW[window: window, lines: 5];
    form ← Tool.MakeFormSW[window: window, formProc: MakeForm];
    MakeBoxesSW[window];
    Tool.UnusedLogName[logFileName, "EchoUser.log$"L];
    log ← Tool.MakeFileSW[window: window, name: logFileName];
    END;

  startIX: CARDINAL = 0;
  stopIX: CARDINAL = 1;
  runningIX: CARDINAL = 2;
  MakeForm: FormSW.ClientItemsProcType =
    BEGIN
    nParams: CARDINAL = 12;
    items ← FormSW.AllocateItemDescriptor[nParams];
    items[0] ← FormSW.CommandItem[
      tag: "Start"L, proc: Start, place: FormSW.newLine];
    items[1] ← FormSW.CommandItem[
      tag: "Stop"L, proc: Stop, place: FormSW.newLine, invisible: TRUE];
    items[2] ← FormSW.BooleanItem[tag: "LowPriority"L, switch: @data.lowPriority];
    items[3] ← FormSW.BooleanItem[
      tag: "DoStats"L, switch: @data.doStats, place: FormSW.newLine];
    items[4] ← FormSW.CommandItem[tag: "Ready"L, proc: Ready];
    items[6] ← FormSW.CommandItem[tag: "Recent"L, proc: Since];
    items[5] ← FormSW.CommandItem[tag: "Totals"L, proc: Totals];
    items[7] ← FormSW.BooleanItem[
      tag: "Verbose"L, switch: @data.verbose, place: FormSW.newLine];
    items[8] ← FormSW.BooleanItem[tag: "Checkit"L, switch: @data.checkit];
    items[9] ← FormSW.BooleanItem[
      tag: "FixedLength"L, switch: @data.fixedLength, place: FormSW.newLine];
    items[10] ← FormSW.NumberItem[tag: "(Max)Length"L, value: @data.length];
    items[11] ← FormSW.StringItem[
      tag: "Target"L, string: @data.target, place: FormSW.newLine];
    RETURN[items, TRUE];
    END;

  AlreadyActive: ERROR = CODE;
  NotActive: ERROR = CODE;

  ClientTransition: ToolWindow.TransitionProcType =
    BEGIN
    SELECT TRUE FROM
      old = inactive =>
	BEGIN
	IF data # NIL THEN ERROR AlreadyActive;
	data ← Storage.Node[SIZE[Data]];
	data↑ ← [];
	data.target ← Storage.String[20];
	String.AppendString[data.target, "ME"L];
	PupDefs.PupPackageMake[];
	END;
      new = inactive =>
	BEGIN
	IF data = NIL THEN ERROR NotActive;
	IF data.running THEN EchoUserOff[];
	PupDefs.PupPackageDestroy[];
	Storage.FreeString[data.target];
	data ← Storage.FreeNodeNil[data];
	END;
      ENDCASE;
    END;

  Broom: PROCEDURE [why: Event.Reason] =
    BEGIN
    SELECT why FROM
      makeImage, makeCheck =>
	BEGIN
	IF data = NIL THEN RETURN;
	IF data.running THEN EchoUserOff[];
	PupDefs.PupPackageDestroy[];
	END;
      startImage, restartCheck, continueCheck =>
	BEGIN IF data = NIL THEN RETURN; PupDefs.PupPackageMake[]; END;
      ENDCASE => NULL;
    END;

  -- Main Body

  Initialize[];
  END.