--GroupsImpl.mesa
--Last edited by Newman.es: May 6, 1983 
--Alto/Mesa/Laurel 6.1 version

DIRECTORY
  Ascii USING [CR, DEL, ESC, SP],
  BinaryTree USING [CompareTrees, DestroyTree, Enter, EnumerateTree, MergeTrees, Present, ReportProc, Tree, UserProc],
  BodyDefs USING [maxRNameLength, oldestTime, RName, Timestamp],
  exD: FROM "ExceptionDefs" USING [AppendStringToExceptionLine, ClearExceptionsRegion, DisplayExceptionStringOnLine, ExceptionLineOverflow],
  IODefs USING [LineOverflow, ReadChar, ReadID, Rubout, WriteChar, WriteDecimal, WriteLine, WriteString],
  LaurelExecDefs USING [MakeMenuCommandCallable],
  MailParseDefs USING [endOfInput, endOfList, FinalizeParse, InitializeParse, ParseHandle, ParseError, ParseNameList],
  NameInfoDefs USING [CheckStamp, Close, Enumerate, GetMembers, IsInList, MemberInfo, Membership, MembershipLevel, NameType, RListHandle],
  NameInfoSpecialDefs USING [CleanUp],
  Runtime USING [GetBcdTime],
  Storage USING [EmptyString, Node, Free, Prune],
  StreamDefs USING [FileNameError, NewByteStream, Read, StreamError, StreamHandle, userAbort],
  String USING [AppendChar, AppendString, DeleteSubString, EquivalentSubStrings, LowerCase, SubStringDescriptor],
  Time USING [Append, Unpack];

GroupsImpl: PROGRAM
  IMPORTS BinaryTree, exD, IODefs, LaurelExecDefs, NameInfoDefs, MailParseDefs, NameInfoSpecialDefs, Runtime, Storage, StreamDefs, String, Time =
  BEGIN OPEN Ascii;

Command: TYPE = {expand, isMember, union, intersect, subtract, inReg, notInReg};

Tree: TYPE = BinaryTree.Tree;

mdsZoneRecord: RECORD [POINTER TO MDSZoneProcs] ← [@mdsZoneProcs];
MDSZoneProcs: TYPE = MACHINE DEPENDENT RECORD [
  alloc: PROC [MDSZone, CARDINAL] RETURNS [POINTER],
  free: PROC [MDSZone, POINTER]];
mdsZoneProcs: MDSZoneProcs ← [Alloc, Free];
Alloc: PROC [z: MDSZone, c: CARDINAL] RETURNS [POINTER] =
  {RETURN [Storage.Node [c]]};
Free: PROC [z: MDSZone, p: POINTER] =
  {Storage.Free [p]};
z: MDSZone = LOOPHOLE [@mdsZoneRecord]; 
 
aborted: BOOLEAN ← FALSE;
level: NameInfoDefs.MembershipLevel ← upArrow;

maxRNameLength: CARDINAL = BodyDefs.maxRNameLength;

alreadySeen: Tree ← NIL;
  
  WriteString: PROC [STRING] = IODefs.WriteString;
  WriteLine: PROC [STRING] = IODefs.WriteLine;
  WriteCR: PROC = {IODefs.WriteChar [CR]};
  
  TerminateDataSink: PROC [ctMembers: CARDINAL] =
    BEGIN
    IF ctMembers = 0 THEN WriteLine [";(empty)"L]
    ELSE
      BEGIN
      WriteString["; ("L];
      IODefs.WriteDecimal[ctMembers];
      WriteString[" member"L];
      IF ctMembers > 1 THEN WriteString["s"L];
      WriteLine[")"L];
      END;
    END;
  
  DestroyTreeNil: PROC [tree: Tree, z: MDSZone] RETURNS [nil: Tree ← NIL] = INLINE {BinaryTree.DestroyTree [tree, z]};

  PutLevel: PROC [level: NameInfoDefs.MembershipLevel] =
    BEGIN
    WriteString [SELECT level FROM
      closure => "closure"L,
      upArrow => "upArrow"L,
      ENDCASE => "direct"L];
    END;  -- of PutLevel

  CompareGroups: PROC [
    group1, group2: BodyDefs.RName, option: Command] =
    BEGIN
    ctMembers: CARDINAL ← 0;
    tree1, tree2: Tree;

    Report: BinaryTree.ReportProc =  -- [id: Sv, membership: Membership]
      BEGIN
      good: BOOLEAN =
        SELECT option FROM
          intersect => (membership = both),
          subtract => (membership = first),
          union => TRUE,
          ENDCASE => ERROR;
      IF good THEN
        BEGIN
        aborted← StreamDefs.userAbort;
        IF aborted THEN RETURN;
        IF ctMembers # 0 THEN WriteString[", "L];
        WriteString[id];
        ctMembers ← ctMembers + 1;
        END;
      END;  -- of Report

    tree1 ← ExpandFileOrGroup[group1 ! BadFile => GOTO return];
    IF aborted THEN {
      BinaryTree.DestroyTree[tree1, z]; WriteLine["Aborted"L]; RETURN};
    tree2 ← ExpandFileOrGroup[
      group2 ! BadFile => {BinaryTree.DestroyTree[tree1, z]; GOTO return}];
    IF aborted THEN {
      BinaryTree.DestroyTree[tree1, z];
      BinaryTree.DestroyTree[tree2, z];
      WriteLine["Aborted"L];
      RETURN};
    WriteString[
      SELECT option FROM
        intersect => "Intersection"L,
        subtract => "Subtraction"L,
        union => "Union"L,
        ENDCASE => ERROR];
    WriteString[" of group "L];
    WriteString[group1];
    WriteString[IF option = subtract THEN " minus "L ELSE " and "L];
    WriteString["group "L];
    WriteString[group2];
    WriteString [" ("L];
    PutLevel [level];
    WriteString["): "L];
    BinaryTree.CompareTrees[tree1, tree2, Report];
    IF aborted
      THEN {WriteCR[]; WriteLine ["Aborted"L]}
      ELSE TerminateDataSink [ctMembers];
    BinaryTree.DestroyTree[tree1, z];
    BinaryTree.DestroyTree[tree2, z];
    EXITS return => {};
    END;  -- of CompareGroups

     
  RegistryFilter: PROC [group: BodyDefs.RName, registry: STRING, include: BOOLEAN] =
    BEGIN
    tree: Tree;
    ctMembers: CARDINAL ← 0;
    
    ListUser: BinaryTree.UserProc =  -- [id: Sv]
      BEGIN
      hasReg: BOOLEAN ← HasRegistry [id, registry];
      IF hasReg # include THEN RETURN;
      aborted ← StreamDefs.userAbort;
      IF aborted THEN RETURN;
      IF ctMembers # 0 THEN WriteString[", "L];
      WriteString[id];
      ctMembers ← ctMembers + 1;
      END;  -- of ListUser
    
    IF Storage.EmptyString [registry] THEN {
      WriteLine["Empty registry name!"L]; RETURN};
    tree ← ExpandFileOrGroup[group ! BadFile => GOTO return];
    IF aborted THEN {
      BinaryTree.DestroyTree[tree, z]; WriteLine["Aborted"L]; RETURN};
    IF registry[0] = '. THEN DeleteFirstChar [registry];
      --strip off "." if present
    WriteString [IF include THEN "Intersection"L ELSE "Subtraction"L];
    WriteString[" of group "L];
    WriteString[group];
    WriteString[IF include THEN " and "L ELSE " minus "L];
    WriteString["registry "L];
    WriteString[registry];
    WriteString [" ("L];
    PutLevel [level];
    WriteString["): "L];
    BinaryTree.EnumerateTree[tree, ListUser];
    IF aborted
      THEN {WriteCR[]; WriteLine ["Aborted"L]}
      ELSE TerminateDataSink [ctMembers];
    BinaryTree.DestroyTree [tree, z];
    EXITS return => {};
    END;
  
  HasRegistry: PROC [name, registry: STRING] RETURNS [BOOLEAN] =
    BEGIN
    ssName, ssReg: String.SubStringDescriptor;
    IF name.length < registry.length THEN RETURN [FALSE];
    ssName ← [name, name.length-registry.length, registry.length];
    ssReg ← [registry, 0, registry.length];
    RETURN [String.EquivalentSubStrings [@ssName, @ssReg]];
    END;
    
  --ExpandFileOrGroup should be called only at the top level of an expansion (since a file can't be nested in either a group or another file).  It destroys the "alreadySeen" tree, built up by ExpandFile/ExpandGroup, when it is done.
  
  ExpandFileOrGroup: PROC [name: STRING] RETURNS [tree: Tree ← NIL] =
    BEGIN
    IF name[0] = '@ THEN
      BEGIN
      DeleteFirstChar [name];
      tree ← ExpandFile[name
        ! BadFile => alreadySeen ← DestroyTreeNil [alreadySeen, z]];
      END
    ELSE
      tree ← ExpandGroup[name];
    alreadySeen ← DestroyTreeNil [alreadySeen, z];
    END;
    
  BadFile: ERROR = CODE;  --raised whenever ExpandFile runs into any trouble

  ExpandFile: PROC [fileName: STRING] RETURNS [tree: Tree ← NIL] =
    BEGIN
    stream: StreamDefs.StreamHandle;
    where: {inList, endOfList, endOfInput} ← inList;
    
    getChar: PROC RETURNS [ch: CHARACTER] =
      BEGIN
      IF where = endOfList THEN
        {where ← endOfInput;
	RETURN[MailParseDefs.endOfInput]};
      ch ← stream.get[stream
        ! StreamDefs.StreamError => 
	  {where ← endOfList;
	  ch ← MailParseDefs.endOfList;
	  CONTINUE}];
      END;  -- of getChar
    
    --Errors used to abort parsing
    UnqualifiedName: ERROR [badName: STRING] = CODE;
    NestedFileIllegal: ERROR [badName: STRING] = CODE;
    UserAbort: ERROR = CODE;
  
    process: PROC [name, registry: STRING, isFile, isNested: BOOLEAN] RETURNS [write: BOOLEAN ← FALSE] =
      BEGIN
      fullName: STRING;
      fullNameLength: CARDINAL;
      IF StreamDefs.userAbort THEN ERROR UserAbort;
      IF isFile THEN ERROR NestedFileIllegal [name];
      IF registry.length = 0 THEN ERROR UnqualifiedName[name];
      fullNameLength ← name.length + 1 --"."-- + registry.length;
      fullName ← z.NEW [StringBody[fullNameLength]];
      String.AppendString[to: fullName, from: name];
      String.AppendChar[fullName, '.];
      String.AppendString[to: fullName, from: registry];
      tree ← AddNameOrGroup[fullName, tree];
      z.FREE [@fullName];
      END;  -- of process
    
    error: BOOLEAN ← FALSE;
    parse: MailParseDefs.ParseHandle;
    exD.DisplayExceptionStringOnLine["Expanding "L, 1];
    exD.AppendStringToExceptionLine[fileName, 1
      ! exD.ExceptionLineOverflow => CONTINUE];
    stream ← StreamDefs.NewByteStream [fileName, StreamDefs.Read !
      StreamDefs.FileNameError =>
        BEGIN
	WriteString ["Cannot acquire file "L];
        WriteLine [fileName];
        ERROR BadFile;
        END]; 
	
    parse ← MailParseDefs.InitializeParse [getChar];
    
    MailParseDefs.ParseNameList [parse, process !
      UnqualifiedName =>
        BEGIN
        WriteString ["Unqualified name not allowed: """];
        WriteString [badName];
        WriteString [""" in file "L];
        WriteLine[fileName];
        error ← TRUE;
        CONTINUE;
        END;
      NestedFileIllegal =>
        BEGIN
        WriteString ["Nested file not allowed: "];
        WriteString [badName];
        WriteString [""" in file "L];
        WriteLine[fileName];
        error ← TRUE;
        CONTINUE;
        END;
      MailParseDefs.ParseError =>
        BEGIN
        WriteString ["Syntax error in file "L];
        WriteLine[fileName];
        error ← TRUE;
        CONTINUE;
        END;
      UserAbort => {aborted ← TRUE; CONTINUE}];
    MailParseDefs.FinalizeParse [parse];
    stream.destroy [stream];
    IF error THEN {BinaryTree.DestroyTree[tree, z]; ERROR BadFile};
    END;  -- of ExpandFile
    
  ExpandGroup: PROC [groupName: BodyDefs.RName] RETURNS [tree: Tree ← NIL] =
    BEGIN
    members: NameInfoDefs.RListHandle;
    enumproc: PROC [name: BodyDefs.RName] RETURNS [done: BOOLEAN ← FALSE] =
      BEGIN
      IF StreamDefs.userAbort THEN {aborted ← TRUE; RETURN[done: TRUE]};
      tree ← AddNameOrGroup[name, tree];
      IF aborted THEN RETURN[done: TRUE];  --return from all recursions
      END;  -- of enumproc

    exD.DisplayExceptionStringOnLine["Expanding "L, 1];
    exD.AppendStringToExceptionLine[groupName, 1
      ! exD.ExceptionLineOverflow => CONTINUE];
    members ← GetMembers[groupName ! NotAGroup => GOTO return];
    alreadySeen ← BinaryTree.Enter [alreadySeen, groupName, z];
    NameInfoDefs.Enumerate[members, enumproc];
    NameInfoDefs.Close[members];
    EXITS return => {};
    END;

  AddNameOrGroup: PROC [name: STRING, tree: Tree] RETURNS [mergedTree: Tree] =
    BEGIN
    IF ShouldExpand [name] THEN
      BEGIN
      newTree: Tree;
      IF BinaryTree.Present [alreadySeen, name] THEN RETURN [tree];
      newTree ← ExpandGroup[name];
      mergedTree ← BinaryTree.MergeTrees[tree, newTree, z];
      --consumes "tree" and "newTree"
      END
    ELSE mergedTree ← BinaryTree.Enter[tree, name, z];
    END;  -- of AddNameOrGroup

  NotAGroup: ERROR = CODE;

  GetMembers: PROCEDURE [
    name: BodyDefs.RName, oldStamp: BodyDefs.Timestamp ← BodyDefs.oldestTime]
    RETURNS [members: NameInfoDefs.RListHandle] =
    BEGIN
    info: NameInfoDefs.MemberInfo ← NameInfoDefs.GetMembers[name, oldStamp];
    WITH i: info SELECT FROM
      group => RETURN[i.members];
      ENDCASE => ERROR NotAGroup;
    END;

  GetRName: PROC [prompt, rName: STRING, individualOk: BOOLEAN] RETURNS [punt, individual: BOOLEAN] =
    BEGIN
    type: NameInfoDefs.NameType;
    WriteString [prompt];
    IODefs.ReadID [rName ! IODefs.LineOverflow, IODefs.Rubout =>
      {WriteLine [" XXX"L]; GOTO Leave;}];
    IF rName.length = 0 THEN {WriteLine [" XXX"L]; GOTO Leave};
    IF rName[0] = '@ THEN RETURN[FALSE, FALSE];  --file name
    type ← NameInfoDefs.CheckStamp [rName];
    SELECT type FROM
      individual =>
        IF NOT individualOk
          THEN {WriteLine ["	not a valid group"L]; GOTO Leave;}
          ELSE individual ← TRUE;
      group => individual ← FALSE;
      ENDCASE =>
        {WriteLine ["	not a valid individual or group"L]; GOTO Leave;};
    punt ← FALSE;
    EXITS Leave => punt ← TRUE;
    END; -- of GetRName

  ShouldExpand: PROC [name: BodyDefs.RName] RETURNS [yes: BOOLEAN] =
    BEGIN
    SELECT level FROM
      direct => RETURN [FALSE];
      upArrow => RETURN [HasUpArrow [name]];
      ENDCASE => RETURN [NameInfoDefs.CheckStamp [name] = group];
    END;  -- of ShouldExpand
    
  HasUpArrow: PROC [name: BodyDefs.RName]
    RETURNS [yes: BOOLEAN ← FALSE] =
    BEGIN
    FOR i: CARDINAL IN [0..name.length) DO
      SELECT name[i] FROM '↑ => RETURN[TRUE]; '. => RETURN[FALSE]; ENDCASE;
      ENDLOOP;
    END;

  DeleteFirstChar: PROC [s: STRING] =
    BEGIN
    ss: String.SubStringDescriptor ← [s, 0, 1];
    String.DeleteSubString[@ss]
    END;
    
  IsMemberCommand: PROC =
    BEGIN
    name: STRING ← [maxRNameLength];
    group: STRING ← [maxRNameLength];
    pass: BOOLEAN;
    [pass,] ← GetRName["Membership check of: "L, name, TRUE];
    IF pass THEN RETURN;
    [pass,] ← GetRName[" in group: "L, group, FALSE];
    IF pass THEN RETURN;
    WriteCR[];
    IF name[0] = '@ OR group[0] = '@ THEN
      {WriteString ["Membership check not implemented for file name arguments"L]; RETURN};
    WriteString[name];
    WriteString[" is"L];
    IF NameInfoDefs.IsInList [group, name, level, self, members] # yes
      THEN WriteString["n't"L];
    WriteString[" in group "L];
    WriteString[group];
    WriteString[" ("L];
    PutLevel [level];
    WriteLine [")"L];
    NameInfoSpecialDefs.CleanUp[];
    END;  -- of IsMemberCommand

  ExpandCommand: PROC =
    BEGIN
    group: STRING ← [maxRNameLength];
    ctMembers: CARDINAL ← 0;
    pass: BOOLEAN;
    tree: Tree;

    ListUser: BinaryTree.UserProc =  -- [id: Sv]
      BEGIN
      aborted ← StreamDefs.userAbort;
      IF aborted THEN RETURN;
      IF ctMembers # 0 THEN WriteString[", "L];
      WriteString[id];
      ctMembers ← ctMembers + 1;
      END;  -- of ListUser

    [pass, ] ← GetRName ["Expand group: "L, group, FALSE];
    IF pass THEN RETURN;
    WriteCR[];
    tree ← ExpandFileOrGroup[group ! BadFile => GOTO return];
    IF aborted THEN {
      BinaryTree.DestroyTree[tree, z]; WriteLine["Aborted"L]; RETURN};
    IF tree = NIL THEN {WriteLine [" is empty"L]; RETURN;};
    WriteString["Expansion of group "L];
    WriteString[group];
    WriteString [" ("L];
    PutLevel [level];
    WriteString["): "L];
    BinaryTree.EnumerateTree[tree, ListUser];
    IF aborted
      THEN {WriteCR[]; WriteLine ["Aborted"L]}
      ELSE TerminateDataSink [ctMembers];
    BinaryTree.DestroyTree[tree, z];
    EXITS return => {};
    END;  -- of ExpandCommand

  DoubleGroupCommand: PROC [command: Command] =
    BEGIN
    group1: STRING ← [maxRNameLength];
    group2: STRING ← [maxRNameLength];
    pass: BOOLEAN;
    what: STRING ← SELECT command FROM
      subtract => "Subtracting group: "L,
      union => "Uniting groups: "L,
      ENDCASE => "Intersecting groups: "L;
    [pass, ] ← GetRName [what, group1, FALSE];
    IF pass THEN RETURN;
    what ← IF command = subtract THEN " minus group: "L ELSE " and: "L;
    [pass, ] ← GetRName [what, group2, FALSE];
    IF pass THEN RETURN;
    WriteCR[];
    CompareGroups[group1, group2, command];
    END;  -- of DoubleGroupCommand

  RegistryFilterCommand: PROC = 
    BEGIN
    include: BOOLEAN ← TRUE;
    group: STRING ← [maxRNameLength];
    reg: STRING ← [maxRNameLength];
    pass: BOOLEAN;
    [pass, ] ← GetRName ["Registry filter of group: "L, group, FALSE];
    IF pass THEN RETURN;
    WriteString [" against registry: "L];
    IODefs.ReadID[reg ! IODefs.LineOverflow, IODefs.Rubout =>
      {WriteLine [" XXX"L]; GOTO out}];
    IF reg.length = 0 THEN {WriteLine [" XXX"L]; GOTO out};
    IF reg[0] = '~ THEN 
       {include ← FALSE;
       DeleteFirstChar [reg];
       IF reg.length = 0 THEN {WriteLine [" XXX"L]; GOTO out}};
    WriteCR[];
    RegistryFilter [group, reg, include];
    EXITS out => {};
    END;

  ChangeLevel: PROC = 
    BEGIN
    char: CHARACTER;
    WriteString ["Change membership level (Closure, Direct, UpArrow): "L];
    PutLevel [level];
    WriteString [" ← "L];
    SELECT (char ← String.LowerCase [IODefs.ReadChar []]) FROM
      'c => level ← closure;
      'd => level ← direct;
      'u => level ← upArrow;
      ENDCASE => {WriteLine [" ??? "L]; RETURN};
    PutLevel [level];
    WriteCR[];
    END;  -- of ChangeLevel
    
Salutations: PROC [version: STRING] =
BEGIN OPEN Time;
time: STRING ← [20];
WriteCR[];
WriteString [version];
WriteString [", of "L];
Append [time, Unpack [Runtime.GetBcdTime []]];
time.length ← time.length - 3;		-- remove the seconds
WriteLine [time];
time.length ← 0;
Append [time, Unpack []];
WriteLine [time];
WriteCR[];
LaurelExecDefs.MakeMenuCommandCallable[user];
LaurelExecDefs.MakeMenuCommandCallable[newMail];
LaurelExecDefs.MakeMenuCommandCallable[mailFile];
LaurelExecDefs.MakeMenuCommandCallable[display];
LaurelExecDefs.MakeMenuCommandCallable[delete];
LaurelExecDefs.MakeMenuCommandCallable[undelete];
LaurelExecDefs.MakeMenuCommandCallable[moveTo];
LaurelExecDefs.MakeMenuCommandCallable[copy];
END;		-- of proc Salutations

--TTY stuff

Confirm: PROC [prompt: STRING, outputCR: BOOLEAN] RETURNS [confirmed: BOOLEAN] =
BEGIN
DO
	WriteString [prompt];
	WriteString [" [confirm] "L];
	SELECT String.LowerCase [IODefs.ReadChar []] FROM
		'y, CR, ESC, SP => confirmed ← TRUE;
		'n, DEL => confirmed ← FALSE;
		ENDCASE =>
			BEGIN
			WriteLine ["Yes (or CR) or No (or DEL)?"L];
			LOOP;
			END;
	EXIT;
ENDLOOP;
WriteString [IF confirmed THEN "Yes"L ELSE "No"L];
IF outputCR THEN WriteCR[];
END;		-- of proc Confirm

MainLoop: PROC = 
BEGIN
char: CHARACTER;
Version: STRING = "Groups 6.1"L;
Salutations [Version];
DO
	aborted ← StreamDefs.userAbort ← FALSE;
	WriteString ["=> "L];
	SELECT (char ← String.LowerCase [IODefs.ReadChar []]) FROM
		'e => ExpandCommand [];
		'i => DoubleGroupCommand [intersect];
		'l => ChangeLevel[];
		'm => IsMemberCommand[];
		'q => IF Confirm ["Quit"L, TRUE] THEN EXIT;
		'r => RegistryFilterCommand[];
		's => DoubleGroupCommand [subtract];
		'u => DoubleGroupCommand [union];
		ENDCASE =>
			BEGIN
			WriteLine ["? - Commands:
	E (Expansion),
	I (Intersection),
	L (change expansion Level)
	M (Membership check),
	Q (Quit),
	R (Registry filter),
	S (Subtraction), or
	U (Union)"L];
			LOOP;
			END;
exD.ClearExceptionsRegion[];
ENDLOOP;
WriteCR[];
[] ← Storage.Prune [];
END;		-- of proc MainLoop

-- GroupsImpl's mainline code:
MainLoop[];
END.