-- UniqueImpl.mesa
-- edited by Schmidt (February 3, 1983 6:03 pm)
-- based on version of SHayes (15-Sep-81 19:47:34)


DIRECTORY
  Ascii: TYPE USING [ControlZ],
  Buttons: TYPE USING [Button, ButtonProc, Create],
  Containers: TYPE USING [ChildXBound, ChildYBound, Container, Create],
  FileIO: TYPE USING[Open, OpenFailed],
  IO: TYPE USING[Close, CreateInputStreamFromRope, EndOfStream, GetChar, Handle,
     Put, PutChar, PutF, PutRope, rope, string],
  Labels: TYPE USING [Create, Label, Set], 
  List: TYPE USING[CompareProc, DReverse, UniqueSort],
  Menus: TYPE USING [CreateEntry, CreateMenu, InsertMenuEntry, Menu, MenuProc], 
  Process: TYPE USING[Detach],
  Rope: TYPE USING[Cat, Compare, Equal, Fetch, Flatten, FromChar, Length, ROPE],
  Rules: TYPE USING [Create, Rule],
  TypeScript: TYPE USING[TS, Create],
  UECP: TYPE USING[Argv, Parse],
  UserExec: TYPE USING[CommandProc, RegisterCommand],
  ViewerClasses: TYPE USING [Viewer], 
  ViewerOps: TYPE USING [PaintViewer, SetMenu, SetOpenHeight], 
  ViewerIO: TYPE USING[CreateViewerStreams],
  ViewerTools: TYPE USING [GetContents, GetSelectionContents,
     MakeNewTextViewer, SetSelection];

UniqueImpl: CEDAR PROGRAM
  IMPORTS Buttons, Containers, FileIO, IO, Labels, List, Menus,
     Process, Rope, Rules, TypeScript, UECP, UserExec,
     ViewerIO, ViewerOps, ViewerTools  =  {

  Uppercase: TYPE = CHAR ['A..'Z];
  Lowercase: TYPE = CHAR ['a..'z];
  Digits: TYPE = CHAR ['0..'9];
  Mode: TYPE = {files, extensions, words, lines, numbers};
  
  Global: TYPE = REF GlobalRecord;
  GlobalRecord: TYPE = RECORD[
     container: Containers.Container ← NIL, 
	  ttyTypeScript: TypeScript.TS ← NIL,
	  modeButton: Buttons.Button ← NIL,
	  modeLabel: Labels.Label ← NIL,
	  fileNameButton: Buttons.Button ← NIL,
	  fileNameViewer: ViewerClasses.Viewer ← NIL,
	  in: IO.Handle ← NIL,
	  out: IO.Handle ← NIL,
	  ch: CHAR ← ' ,
	  eof: BOOL ← FALSE,
	  list: LIST OF Rope.ROPE ← NIL,
	  mode: Mode ← files
	  ];
  
  entryHeight: CARDINAL = 15; 
  entryVSpace: CARDINAL = 10; 
  entryHSpace: CARDINAL = 10; 
  
  -- mds usage
  oneGlobal: Global;
  modeString: ARRAY Mode OF Rope.ROPE
  	← ["Files", "Extensions", "Words", "Lines", "Numbers"];
 -- end of mds usage
     
  BuildOuter: PROC RETURNS[g: Global] = 
   {
   menu: Menus.Menu ← Menus.CreateMenu[];
   g ← NEW[GlobalRecord ← []];
   g.container ← Containers.Create[info: [name: "UniqueWindow", iconic: FALSE,
   	scrollable: FALSE]];
   -- first row of menu items
   Menus.InsertMenuEntry[menu, Menus.CreateEntry["Go", GoProc, g]]; 
   --
   ViewerOps.SetMenu[g.container, menu];
   BuildUserInput[g];
   ViewerOps.PaintViewer[g.container, all];
   [out: g.out] ← ViewerIO.CreateViewerStreams[viewer: g.ttyTypeScript,
   		name: NIL];
   };
  
  BuildUserInput: PROC[g: Global] = 
   {
   heightSoFar: CARDINAL ← 0; 
   l: ViewerClasses.Viewer;
   rule: Rules.Rule;
   
		CreateButton: PROC[bname, lname: Rope.ROPE, newLine: BOOL, 
		  drawRule: BOOL ← FALSE] 
         RETURNS[button: Buttons.Button, label: Labels.Label] = 
         {
         x: CARDINAL;
         IF newLine THEN {
            IF l = NIL THEN 
               heightSoFar ← entryVSpace/2
            ELSE
               heightSoFar ← heightSoFar + entryVSpace + l.wh;
	    		IF drawRule THEN {
	    			rule ← Rules.Create[info: [parent: g.container, wx: 0, wy: heightSoFar,
	    				ww: 0, wh: 1]];
      			Containers.ChildXBound[g.container, rule];
      			heightSoFar ← heightSoFar + entryVSpace;
      			};
      		x ← 0;
	    		}
	 		ELSE 
	    		x ← l.wx + l.ww + entryHSpace;
	 		l ← button ← Buttons.Create[info: [name: bname, parent: g.container,
	    		border: FALSE, wx: x, wy: heightSoFar], proc: PushButton, clientData: g];
	 		IF lname ~= NIL THEN
	    		l ← label ← Labels.Create[info: [name: lname, parent: g.container,
   	       	wx: button.wx + button.ww + entryHSpace, wy: heightSoFar, border: TRUE]];
   	 	};

   [g.modeButton, g.modeLabel] ← CreateButton["Mode:", "AppendWords", TRUE];
   IF g.mode = files THEN Labels.Set[g.modeLabel, "Files"];
   [g.fileNameButton,] ← CreateButton["FileName:", NIL, FALSE];
   l ← g.fileNameViewer ← ViewerTools.MakeNewTextViewer[info: [parent: g.container,
      wx: l.wx+l.ww+entryHSpace, wy: heightSoFar, ww: 100, wh: entryHeight, 
      data: NIL, scrollable: FALSE, border: FALSE], paint: FALSE];
   Containers.ChildXBound[g.container, g.fileNameViewer];
   heightSoFar ← heightSoFar+entryVSpace+l.wh;
   --  now the line above the typescript
   rule ← Rules.Create[info: [parent: g.container, wx: 0, wy: heightSoFar, ww: 0, wh: 1]];
   Containers.ChildXBound[g.container, rule];
   heightSoFar ← heightSoFar+entryVSpace;
   -- now the typescript
   g.ttyTypeScript ← TypeScript.Create[info: [parent: g.container,
      wx: 0, wy: heightSoFar, ww: 0, wh: 800, border: FALSE]];	-- 800 due to viewers bug
   Containers.ChildXBound[g.container, g.ttyTypeScript];
   Containers.ChildYBound[g.container, g.ttyTypeScript];
   ViewerOps.SetOpenHeight[g.container, heightSoFar + 50];
   };

PushButton: Buttons.ButtonProc =  
   {
   g: Global ← NARROW[clientData];
   SELECT NARROW[parent, ViewerClasses.Viewer] FROM
   g.fileNameButton => 
      ViewerTools.SetSelection[g.fileNameViewer, NIL];
   g.modeButton => {
      g.mode ← IF g.mode = numbers THEN files ELSE SUCC[g.mode];
      Labels.Set[g.modeLabel, modeString[g.mode]];
      };
   ENDCASE => ERROR;
   };
	
GoProc: Menus.MenuProc =  
  {
  g: Global ← NARROW[clientData];
  ProcessFileName[g, ViewerTools.GetContents[g.fileNameViewer]];
  IF g.in ~= NIL THEN {
  	p: PROCESS ← FORK DriveUnique[g, g.mode];
  	TRUSTED {Process.Detach[p];}
  	};
  };
  
-- this procedure may be forked
DriveUnique: PROC[g: Global, mode: Mode] = {
  ENABLE ABORTED => GOTO out;
  g.list ← NIL;
  SELECT mode FROM
  words => Things[g, MatchId];
  lines => Things[g, MatchLine];
  numbers => Things[g, MatchNumber];
  extensions => FileNames[g, compareExts];
  files => FileNames[g, compare];
  ENDCASE => ERROR;
  g.out.Put[IO.string["\n\n-------------------------\n\n"L]];
  g.in.Close[];
  g.in ← NIL;
  EXITS
  out => g.out.PutF["Aborted.\n"];
  };
  
ProcessFileName: PROC[g: Global, fileName: Rope.ROPE] = {
	g.eof ← FALSE;
	g.in ← NIL;
	IF fileName.Length[] = 0 THEN {
		r: Rope.ROPE ← ViewerTools.GetSelectionContents[];
		IF r.Length[] = 0 THEN {
			g.out.PutF["Error - No filename or selection given.\n\n"];
        	GOTO NoInput
			};
		g.in ← IO.CreateInputStreamFromRope[r];
		}
	ELSE {
		g.in ← FileIO.Open[fileName, read
      	 ! FileIO.OpenFailed => {
        			g.out.PutF["Error - Can't open %s\n\n", IO.rope[fileName]];
        			GOTO NoInput
        			}];
       };
   InCh[g];
   EXITS 
   NoInput => NULL;
   };
	
InCh: PROC[g: Global] = {
	IF ~g.eof AND g.in#NIL THEN
       g.ch ← g.in.GetChar[
         ! IO.EndOfStream => {
         	g.eof ← TRUE; 
         	g.ch ← '\000; 
         	CONTINUE
         	}];
    };
 
Trash: PROC[g: Global] =
    { 
    DO
      SELECT g.ch FROM ' , '\t, '\n => InCh[g]; Ascii.ControlZ => Z[g]; ENDCASE => EXIT;
      ENDLOOP;
    };

Z: PROC[g: Global] = {
   IF g.ch = Ascii.ControlZ THEN 
   	WHILE g.ch # '\n DO 
   		InCh[g] 
   		ENDLOOP; 
   };

compareExts: List.CompareProc = {
    name1: Rope.ROPE ← NARROW[ref1];
    name2: Rope.ROPE ← NARROW[ref2];
    ext1: Rope.ROPE;
    ext2: Rope.ROPE;
    i: INT;
    FOR i IN [0..name1.Length[]) UNTIL name1.Fetch[i] = '. DO 
    	ENDLOOP;
    ext1 ← Rope.Flatten[name1, i];
    FOR i IN [0..name2.Length[]) UNTIL name2.Fetch[i] = '. DO 
    	ENDLOOP;
    ext2 ← Rope.Flatten[name2, i];
    RETURN[
      SELECT Rope.Compare[ext1, ext2, FALSE] FROM
        less => less,
        greater => greater,
        ENDCASE =>
          SELECT Rope.Compare[name1, name2, FALSE] FROM
            less => less,
            greater  => greater,
            ENDCASE => equal];
    };

  compare: List.CompareProc = {
    name1: Rope.ROPE ← NARROW[ref1];
    name2: Rope.ROPE ← NARROW[ref2];
    RETURN[
      SELECT Rope.Compare[name1, name2, FALSE] FROM
        less => less,
        greater  => greater,
        ENDCASE => equal]
    };

  MatchId: PROC [g: Global] RETURNS[s: Rope.ROPE] = {
    s ← NIL;
    WHILE (g.ch = '$) 
      OR (g.ch = '-) 
      OR (g.ch IN Uppercase) 
      OR (g.ch IN Lowercase)
      OR (g.ch IN Digits) DO 
      	s ← Rope.Cat[s, Rope.FromChar[g.ch]];
      	InCh[g] 
      	ENDLOOP;
    };

  MatchNumber: PROC [g: Global] RETURNS[s: Rope.ROPE] = {
    s ← NIL;
    WHILE (g.ch IN Digits) DO 
    	s ← Rope.Cat[s, Rope.FromChar[g.ch]];
      InCh[g] 
      ENDLOOP;
    };

  MatchLine: PROC [g: Global] RETURNS[s: Rope.ROPE] = {
    s ← NIL;
    UNTIL (g.ch = '\n) OR g.eof DO 
      s ← Rope.Cat[s, Rope.FromChar[g.ch]];
      InCh[g];
      Z[g];
      ENDLOOP;
    IF g.ch = '\n THEN {
    	s ← Rope.Cat[s, Rope.FromChar[g.ch]];
      InCh[g] 
      };
    };

  Things: PROC [g: Global, p: PROC[g: Global] RETURNS[Rope.ROPE]] = {
    s: Rope.ROPE;
    WHILE ~g.eof DO
      UNTIL (s ← p[g]).Length[] ~= 0 OR g.eof DO 
      	InCh[g]; 
      	Trash[g] 
      	ENDLOOP;
      IF s.Length[] # 0 THEN 
      	g.list ← CONS[s, g.list];
      ENDLOOP;
    ProcessAndPrint[g, compare];
    };

ProcessAndPrint: PROC[g: Global, cProc: List.CompareProc] = TRUSTED {
	g.list ← LOOPHOLE[List.DReverse[LOOPHOLE[g.list]]];
   g.list ← LOOPHOLE[List.UniqueSort[LOOPHOLE[g.list], cProc]];
   PrintList[g];
   };
    
FileNames: PROC [g: Global, cProc: List.CompareProc] =
    BEGIN
    s, t: Rope.ROPE;
    WHILE ~g.eof DO
      UNTIL ((s←MatchId[g]).Length[] ~= 0 AND g.ch = '.) OR g.eof DO 
      	InCh[g]; 
      	Trash[g] 
      	ENDLOOP;
      InCh[g];
      IF (t←MatchId[g]).Length[] ~= 0 THEN {
        s ← Rope.Cat[s, ".", t];
        g.list ← CONS[s, g.list];
     	 };
     	 ENDLOOP;
    ProcessAndPrint[g, cProc];
    END;

PrintList: PROC[g: Global] = {
	FOR l: LIST OF Rope.ROPE ← g.list, l.rest UNTIL l = NIL DO
		SmartOutString[g, l.first];
		ENDLOOP;
	};
	
SmartOutString: PROC [g: Global, s: Rope.ROPE] = {
     g.out.PutRope[s];
     IF s.Fetch[s.Length[] - 1] # '\n THEN g.out.PutChar[' ]; 
     };

UniqueExecProc: UserExec.CommandProc = {
	argv: UECP.Argv ← UECP.Parse[event.commandLine];
	filename: Rope.ROPE ← NIL;
	mode: Mode ← files;
	IF argv.argc > 1 THEN {
		mode ← SELECT TRUE FROM
			Rope.Equal[argv[1], modeString[files], FALSE] => files,
			Rope.Equal[argv[1], modeString[extensions], FALSE] => extensions,
			Rope.Equal[argv[1], modeString[words], FALSE] => words,
			Rope.Equal[argv[1], modeString[lines], FALSE] => lines,
			Rope.Equal[argv[1], modeString[numbers], FALSE] => numbers,
			ENDCASE => files;
		};
	IF argv.argc > 2 THEN {
		filename ← argv[2];
		};
	ProcessFileName[oneGlobal, filename];
	IF oneGlobal.in ~= NIL THEN 
		DriveUnique[oneGlobal, mode];
	};
	
{
-- start code
oneGlobal ← BuildOuter[];
UserExec.RegisterCommand["Unique", UniqueExecProc];
}}.