-- File: ClientFileTool.mesa
-- Last edit by Russ Atkinson: May 13, 1982 3:14 pm
--		Schmidt: 25-Jul-81 12:09:21
-- NOTE: change the create date message when changing this beast

DIRECTORY
  Ascii USING [CR],
  Directory USING
    [CreateFile, DeleteFile, Error, GetDefaultContext, GetNext, GetProps, GetWD,
     Handle, Lookup, ModifyContext],
  DirectoryExtras USING [ForgetVolumes],
  Event USING[Notify, Reason],
  File USING [
    Capability, GetAttributes,
    GetSize, grow, nullCapability, PageCount,
    Permissions, read, SetSize, shrink, write],
  FormSW USING [
    AllocateItemDescriptor, ClientItemsProcType, CommandItem,
    line0, line1, line2, nextPlace, ProcType, StringItem],
  HeapString USING [AppendChar, AppendString],
  Inline USING [LowHalf],
  Put USING [Char, Blanks, Date, LongDecimal, Text],
  Space USING [
    CopyOut, Create, Delete, Handle, Map, Unmap, virtualMemory],
  Storage USING [FreeString, String],
  String USING [AppendLongDecimal, AppendString, EquivalentString],
  Time USING [Current, Packed],
  Tool USING [Create, MakeFormSW, MakeFileSW, MakeSWsProc],
  ToolDriver USING [Address, NoteSWs],
  Volume USING [
    Close, GetAttributes, GetLabelString, GetNext, GetType, ID,
    maxNameLength, nullID, PageCount, Type, TypeSet, systemID],
  VolumeExtras USING [OpenVolume],
  Window USING [Handle],
  TTY USING [ResetUserAbort, UserAbort];
  
ClientFileTool: PROGRAM 
  IMPORTS
    Directory, DirectoryExtras, Event, File, FormSW, HeapString, Inline,
    Put, Space, Storage, String, Time, Tool, ToolDriver,
    TTY, Volume, VolumeExtras
   = BEGIN OPEN HeapString;

  Bool: TYPE = BOOLEAN;
  Char: TYPE = CHARACTER;
  
  CommandDesc: TYPE = RECORD [cpName, localName: STRING ← NIL];

  CommandType: TYPE = {get, list, delete, name, getboth};
  
  copier: CommandDesc ← CommandDesc[NIL, NIL];
  clientVolume: Volume.ID ← Volume.nullID;
  volumeOpened: BOOLEAN ← FALSE;  -- some slight protection here
  window, cpMsgSW, cpFormSW: Window.Handle ← NIL;

  -- Executive Commands Invoked from Menu

  GetFileFromClientVolume: FormSW.ProcType = {
    DoCommand[get]};
  
  DeleteFilesFromClientVolume: FormSW.ProcType = {
    DoCommand[delete]};

  ListFilesOnClientVolume: FormSW.ProcType = {
    DoCommand[list]};

  NameFilesOnClientVolume: FormSW.ProcType = {
    DoCommand[name]};

  GetBothFilesOnClientVolume: FormSW.ProcType = {
    DoCommand[getboth]};


  DoCommand: PROC [command: CommandType] = {
    OPEN HeapString;
    volumeLabel: STRING ← "Client"L;
    fileName, pathName, localName, workingDir, scratch: STRING ← NIL;
    oldContext: Directory.Handle ← Directory.GetDefaultContext[];
    fileCount, pageCount: LONG CARDINAL ← 0;
    useLocal: Bool ← FALSE;
    IF volumeOpened THEN
       {DisplayMsg["Client volume already opened!"L]; RETURN};
    IF clientVolume = Volume.nullID THEN
       clientVolume ← MapClientVolumeNameToID[volumeLabel];
    IF clientVolume = Volume.nullID THEN
       {DisplayMsg["Client volume not found!"L]; RETURN};

    fileName ← Storage.String[60];
    pathName ← Storage.String[60];
    localName ← Storage.String[60];
    workingDir ← Storage.String[60];
    scratch ← Storage.String[60];
    Directory.GetWD[oldContext, workingDir];

    AppendChar[@pathName, '<];
    AppendString[@pathName, volumeLabel];
    AppendString[@pathName, ">SysDir>"L];
    IF copier.cpName # NIL THEN AppendString[@fileName, copier.cpName];
    IF command = get AND copier.localName # NIL AND copier.localName.length > 0 THEN
       {AppendString[@localName, copier.localName];
        useLocal ← TRUE};
    {single: Bool ← SingleFilePattern[fileName];
     nextName: STRING ← [80];
     pathNameLen: NAT ← pathName.length;
     triedBcd: BOOL ← FALSE;
     triedMesa: BOOL ← FALSE;
     IF useLocal THEN
        {IF NOT single THEN
            {DisplayMsg["Sorry, client patterns require blank local name."L];
	     GO TO out};
         IF NOT SingleFilePattern[localName] THEN
            {DisplayMsg["Sorry, patterns not allowed in local name."L];
	     GO TO out}};
     VolumeExtras.OpenVolume
       [volume: clientVolume, readOnly: command # delete
        ! ANY => {DisplayMsg["Client volume could not be opened!"L]; GO TO out}];
     volumeOpened ← TRUE;
     Directory.ModifyContext[oldContext, pathName];
     DisplayMsg[""L];
     {root: File.Capability;
      volSize,freePages: Volume.PageCount;
      [volSize, freePages, root] ← Volume.GetAttributes[clientVolume];
      DisplayNums
         ["-- Client volume -- #total pages: "L, volSize,
	  ", #free pages: "L, freePages]};
     IF fileName.length = 0 THEN
        {DisplayMsg["No client file name."L]; GO TO out};
     DO
        localFile: File.Capability ← File.nullCapability;
        pages: File.PageCount ← 0;
        cap: File.Capability ← File.nullCapability;
	lagName: STRING ← [80];
	IF TTY.UserAbort[] THEN {
	   TTY.ResetUserAbort[];
	   DisplayMsg["!!Aborted!!"];
	   GO TO out;
	   };
	IF command = delete THEN String.AppendString[lagName, nextName];
	IF single
	   THEN
	     {nextName.length ← 0;
	      String.AppendString[nextName, pathName];
	      String.AppendString[nextName, fileName];
	      SELECT FALSE FROM
	         command = getboth =>
	           {};
	         triedBcd =>
	           {String.AppendString[nextName, ".bcd"];
	            triedBcd ← TRUE};
	         triedMesa =>
	           {String.AppendString[nextName, ".mesa"];
	            triedMesa ← TRUE};
                 ENDCASE => EXIT;
	      cap ← Directory.Lookup
	        [fileName: nextName, permissions: File.read
	         ! Directory.Error --[type]-- =>
	           {IF type # fileNotFound
		       THEN DisplayMsg[nextName, " has a problem!"L]
		       ELSE DisplayMsg[nextName, " could not be found!"L];
		    EXIT}]}
	   ELSE
             cap ← Directory.GetNext
                [pathName, nextName, nextName
                 ! ANY => CONTINUE];
        IF nextName.length = 0 OR cap = File.nullCapability THEN EXIT;
        IF NOT Match[fileName, 0, nextName, pathNameLen, command = getboth] THEN LOOP;
	
	pages ← File.GetSize[cap];
	SELECT command FROM
	  get, getboth =>
	    {scratch.length ← 0;
	     AppendString[@scratch, workingDir];
	     IF pages > 8191 THEN
                {DisplayMsg[nextName, " is too big!"L]; LOOP};
	     IF NOT useLocal
                THEN {-- skip over path name to get local name
	              pos: NAT ← pathNameLen;
		      localName.length ← 0;
	              FOR pos: NAT IN [pathNameLen..nextName.length) DO
		          AppendChar[@localName, nextName[pos]];
		          ENDLOOP};
	     AppendString[@scratch, localName];
             localFile ←
	       Directory.Lookup
	         [fileName: scratch,
		  permissions: File.read+File.write+File.grow+File.shrink
		  ! Directory.Error --[type]-- =>
	            {IF type # fileNotFound THEN
		        {DisplayMsg[localName, " has a problem"L]; LOOP};
		     CONTINUE}];
	     IF localFile = File.nullCapability
                THEN localFile ←
                  Directory.CreateFile
	            [fileName: scratch,
	             fileType: File.GetAttributes[cap].type,
	             size: pages
		     ! ANY =>
		        {DisplayMsg[fileName, " can't be created locally"L]; LOOP}]
                ELSE File.SetSize[localFile, pages];
             DisplayMsg["Copy "L, nextName];
             {chunk: CARDINAL ← Inline.LowHalf[pages];
	      space: Space.Handle;
	      base: File.PageCount ← 0;
	      IF chunk > 64 THEN chunk ← 64;
	      IF chunk > 0 THEN
	         space ← Space.Create
                   [chunk, Space.virtualMemory
                    ! ANY => {DisplayMsg
		                ["Could not create VM space for "L, nextName];
		              LOOP}];
              WHILE base < pages DO
	        -- loop to transfer file (avoid LARGE spaces)
	        Space.Map[space, [cap, base]];
                Space.CopyOut[space, [localFile, base]];
		Space.Unmap[space];
		base ← base + chunk;
		ENDLOOP;
              Space.Delete[space]};
             DisplayMsg["  to "L, scratch];
	     Event.Notify[Event.Reason[newFiles]];
	     };
	  delete =>
	    {Directory.DeleteFile
               [nextName
                ! ANY =>
	           {DisplayMsg[nextName, " could not be deleted!"L]; LOOP}];
	     DisplayMsg[nextName, " deleted."L];
	     nextName.length ← 0;
	     String.AppendString[nextName, lagName];
	     };
	  list =>
	    {readDate, writeDate, createDate: Time.Packed;
             byteLength: LONG CARDINAL;
	     parent: File.Capability;
	     [readDate, writeDate, createDate, byteLength, parent]
	        ← Directory.GetProps
                    [cap, lagName
                     ! ANY => {DisplayMsg[nextName, "     ???"L]; LOOP}];
             DisplayMsg[lagName, " "L];
             {size: STRING ← [12];
              lim: NAT ← lagName.length + 1;
              String.AppendLongDecimal[size, byteLength];
	      lim ← lim + size.length;
	      IF lim < 36 THEN Put.Blanks[cpMsgSW, 36-lim];
              Put.Text[cpMsgSW, size];
	      size.length ← 0;
	      String.AppendLongDecimal[size, pages];
	      IF size.length < 5 THEN Put.Blanks[cpMsgSW, 5-size.length];
	      Put.Text[cpMsgSW, size];
	      };
             Put.Text[cpMsgSW, "p  "L];
             Put.Date[cpMsgSW, createDate];
	     };
	  name =>
	    {IF fileCount = 0 THEN Put.Char[cpMsgSW, Ascii.CR];
	     Put.Char[cpMsgSW, '  ];
	     FOR i: NAT IN [pathNameLen..nextName.length) DO
	       Put.Char[cpMsgSW, nextName[i]];
	       ENDLOOP; 
	     };
	  ENDCASE => ERROR;

	fileCount ← fileCount + 1;
	pageCount ← pageCount + pages;
	IF single AND command # getboth THEN EXIT;
        ENDLOOP;

    EXITS out => {}};
   IF volumeOpened THEN {
      IF workingDir # NIL AND workingDir.length > 0 THEN {
         Directory.ModifyContext[oldContext, workingDir]};
      DirectoryExtras.ForgetVolumes[];
      Volume.Close[clientVolume];
      volumeOpened ← FALSE};
   Storage.FreeString[fileName];
   Storage.FreeString[pathName];
   Storage.FreeString[localName];
   Storage.FreeString[workingDir];
   Storage.FreeString[scratch];
   {lead: STRING ←
      SELECT command FROM
        get, getboth => "-- Copied #files: "L,
        delete => "-- Deleted #files: "L,
        list => "-- Listed #files: "L,
	name => "-- Named #files: "L,
        ENDCASE => ERROR;
    DisplayNums[lead, fileCount, ", #pages: "L, pageCount]};
   };
   
  DisplayNums: PROC [s1: STRING ← NIL, d1: LONG CARDINAL ← 0,
                     s2: STRING ← NIL, d2: LONG CARDINAL ← 0] = {
    Put.Char[cpMsgSW, Ascii.CR];
    IF s1 # NIL THEN
       {Put.Text[cpMsgSW, s1]; Put.LongDecimal[cpMsgSW, d1]};
    IF s2 # NIL THEN
       {Put.Text[cpMsgSW, s2]; Put.LongDecimal[cpMsgSW, d2]};
    };

  SingleFilePattern: PROC [pattern: STRING] RETURNS [BOOLEAN] = {
    patlen: NAT = IF pattern = NIL THEN 0 ELSE pattern.length;
    pos: NAT ← 0;
    FOR pos IN [pos..patlen) DO
        c: Char = pattern[pos];
	IF c = '* OR c = '# OR c = '  THEN RETURN [FALSE];
        ENDLOOP;
    RETURN [TRUE]};
    
  Match: PROC [pattern: STRING, patpos: NAT, object: STRING, objpos: NAT,
  	     isGetBoth: Bool]  RETURNS [Bool] = {
    -- return TRUE if the object matches the pattern,
    -- FALSE if it does not match.  The pattern may contain
    -- * characters which will match 0 or more characters
    -- in the object.  Case does not matter.
    -- If isGetBoth, then .bcd and .mesa extensions also match.
    -- Blanks in the pattern separate alternate matches
     
    submatch: PROC
        [i1: NAT, len1: INTEGER, i2: NAT, len2: INTEGER, isGetBoth: BOOLEAN]
        RETURNS [BOOLEAN] = {
      DO
        c1: Char ← IF len1 > 0 THEN Lower[pattern[i1]] ELSE ' ;
	IF c1 = '  THEN
	   {IF len2 = 0 THEN RETURN [TRUE];
	    IF NOT isGetBoth THEN RETURN [FALSE];
	    RETURN [Match[".mesa"L, 0, object, i2, FALSE]
	            OR Match[".bcd"L, 0, object, i2, FALSE]]};
        IF c1 = '* THEN {
           -- quick kill for * at end of pattern
           IF len1 = 1 THEN RETURN [TRUE];
           -- else must take all combinations
           {j1: NAT ← i1 + 1;
            nlen1: INTEGER ← len1 - 1;
            j2: NAT ← i2;
            nlen2: INTEGER ← len2;
            WHILE nlen2 >= 0 DO
               IF submatch[j1, nlen1, j2, nlen2, isGetBoth] THEN RETURN [TRUE];
               j2 ← j2 + 1;
               nlen2 ← nlen2 - 1;
               ENDLOOP};
           RETURN [FALSE];
           };
        IF len2 <= 0 THEN RETURN [FALSE];
        -- at this point demand an exact match in both strings
        {c2: Char ← Lower[object[i2]];
         IF c1 # c2 AND c1 # '# THEN
	    -- mismatch, so reject this alternative
	    RETURN [FALSE];
         };
        i1 ← i1 + 1;
        len1 ← len1 - 1;
        i2 ← i2 + 1;
        len2 ← len2 - 1;
        ENDLOOP;
      }; -- submatch
   patlen: CARDINAL = IF pattern = NIL THEN 0 ELSE pattern.length;
   objlen: CARDINAL = IF object = NIL THEN 0 ELSE object.length;
   
   DO -- cycle through all of the alternatives in the pattern
      WHILE patpos < patlen AND pattern[patpos] = '  DO
        -- skip over blanks
        patpos ← patpos + 1;
	ENDLOOP;
      IF patpos >= patlen THEN RETURN [FALSE];
      IF submatch[patpos, patlen-patpos, objpos, objlen-objpos, isGetBoth]
         THEN RETURN [TRUE];
      WHILE patpos < patlen AND pattern[patpos] # '  DO
        -- skip over non-blanks
        patpos ← patpos + 1;
	ENDLOOP;
      ENDLOOP;
   };

  Lower: PROC [c: Char] RETURNS [Char] = INLINE {
    IF c IN ['A..'Z]
       THEN RETURN[LOOPHOLE[LOOPHOLE[c,CARDINAL]+40B]]
       ELSE RETURN[c]};

  -- Error Reporting Procedures

  DisplayMsg: PROCEDURE [s1, s2: STRING ← NIL] = {
    Put.Char[cpMsgSW, Ascii.CR];
    IF s1 # NIL THEN Put.Text[cpMsgSW, s1];
    IF s2 # NIL THEN Put.Text[cpMsgSW, s2]};

  -- Initialization and Window Management

  MakeSWs: Tool.MakeSWsProc = {
    OPEN Tool;
    addresses: ARRAY [0..2) OF ToolDriver.Address;
    cpFormSW ← MakeFormSW[window: window, formProc: MakeForm];
    cpMsgSW ← MakeFileSW[window: window, name: "ClientFileTool.log"L, h: 360];
    addresses ← [["cpFormSW"L, cpFormSW], ["cpMsgSW"L, cpMsgSW]];
    ToolDriver.NoteSWs["Executive"L, DESCRIPTOR[addresses]]};

  MakeForm: FormSW.ClientItemsProcType = {
    OPEN FormSW;
    items ← AllocateItemDescriptor[7];
    items[0]
      ← CommandItem
          [tag: "Get"L, place: [20, line0],
	   proc: GetFileFromClientVolume];
    items[1]
      ← CommandItem
          [tag: "GetBoth"L, place: [-30, line0],
	   proc: GetBothFilesOnClientVolume];
    items[2]
      ← CommandItem
          [tag: "List"L, place: [-30, line0],
	   proc: ListFilesOnClientVolume];
    items[3]
      ← CommandItem
          [tag: "Names"L, place: [-30, line0],
	   proc: NameFilesOnClientVolume];
    items[4]
      ← CommandItem
          [tag: "DELETE!"L, place: [-50, line0],
	   proc: DeleteFilesFromClientVolume];
    items[5] ← StringItem[
      tag: "ClientFile"L, place: [0,line1], string: @copier.cpName, inHeap: TRUE];
    items[6] ← StringItem[
      tag: "LocalFile"L, place: [0,line2], string: @copier.localName, inHeap: TRUE];
    RETURN[items: items, freeDesc: TRUE];
    };
    
--  copied for now (will get exported from CascadeExec)

  MapClientVolumeNameToID: PROCEDURE [name: STRING] RETURNS [volumeID: Volume.ID] = {
    myType: Volume.Type = Volume.GetType[Volume.systemID];
    debuggers: Volume.TypeSet ← [];
    volumeID ← Volume.nullID;
    IF myType = FIRST[Volume.Type] THEN RETURN;
    debuggers[PRED[myType]] ← TRUE;
    UNTIL (volumeID ← Volume.GetNext[volumeID, debuggers]) = Volume.nullID DO
      volumeLabel: STRING ← [Volume.maxNameLength];
      Volume.GetLabelString[volumeID, volumeLabel];
      IF String.EquivalentString[volumeLabel, name] THEN EXIT;
      ENDLOOP};

  -- Initialization
  
 Init: PROC = {
  window ← Tool.Create
    [name: "ClientFile Tool of May 13, 1982"L,
     makeSWsProc: MakeSWs,
     initialBox: [[300,100],[400,360]]];
  Put.Text[cpMsgSW, "ClientFileTool started at "L];
  Put.Date[cpMsgSW, Time.Current[]];
  Put.Char[cpMsgSW, Ascii.CR];
  };
  
  Init[];
  END.