-- File: CmFilesA.mesa - last edit:
  -- Smokey, Oct 17, 1979 1:02 PM
  -- Evans, Aug 19, 1980 10:44 AM
  -- Tom, Aug 28, 1979 6:01 PM
  -- Mark, September 5, 1980  1:15 AM
  -- Karlton, September 5, 1980  1:13 AM
  -- Levin,  8-Mar-82 17:12:37
  
DIRECTORY
  Ascii USING [ControlZ, CR, NUL, SP, TAB],
  CmFile USING [ErrorCode, Lop],
  Compatibility USING [SHandle],
  Segments,
  Streams,
  String USING [
    AppendChar, EquivalentString, EquivalentSubString, 
    StringBoundsFault, StringToDecimal, SubString, SubStringDescriptor],
  Storage USING [CopyString, FreeStringNil, String];
  
CmFilesA: PROGRAM
  IMPORTS CmFile, Segments, Streams, String, Storage
  EXPORTS CmFile =
  BEGIN
  
-- Global Data

  userDotCmName: STRING = "User.cm";
  lineSize: CARDINAL = 250;
  
  CmFileInfo: TYPE = RECORD [
    keepOpen: BOOLEAN ← FALSE,
    fh: Segments.FHandle ← NIL,
    sh: Streams.Handle ← NIL,
    fileName: STRING ← NIL];
  CmFileInfoHandle: TYPE = POINTER TO CmFileInfo;
  
  userDotCmInfo: CmFileInfo ← [fileName: userDotCmName];
  otherCmInfo: CmFileInfo ← [];
  lastOpenedCFI: CmFileInfoHandle ← NIL;  -- Current file open info
  
  quote: CHARACTER = '";
  
-- SIGNALs

  Error: PUBLIC SIGNAL [code: CmFile.ErrorCode] = CODE;
  
-- Client collusion

--  UserDotCmSetFHHint: PUBLIC PROCEDURE [file: Segments.FHandle] = {
--    Segments.LockFile[file]};
    
-- Simple Interface Procedures

  Open: PUBLIC PROCEDURE [fileName: STRING] =
    BEGIN FileOpen[fileName]; lastOpenedCFI.keepOpen ← TRUE; END;
    
  Close: PUBLIC PROCEDURE [fileName: STRING] =
    BEGIN FileClose[fileName]; lastOpenedCFI.keepOpen ← FALSE; END;
    
  Line: PUBLIC PROCEDURE [fileName, title, name: STRING]
    RETURNS [s: STRING] =
    BEGIN
    localLine: STRING ← [lineSize];
    card: CARDINAL;
    IF ~OpenAndMatch[localLine, fileName, title, name] THEN s ← NIL
    ELSE
      BEGIN
      s ← Storage.String[localLine.length-name.length];
      card ← name.length+2;
      WHILE card<localLine.length AND localLine[card]=Ascii.SP DO
	card ← card + 1;
	ENDLOOP;
      IF localLine[card]=quote THEN
	BEGIN
	FOR card IN (card..localLine.length) DO
	  IF localLine[card]=quote THEN EXIT;
	  String.AppendChar[s, localLine[card]];
	ENDLOOP;
	END
      ELSE
	BEGIN
	FOR card IN [card..localLine.length) DO
	  String.AppendChar[s, localLine[card]];
	ENDLOOP;
	END;
      END;
    IF ~lastOpenedCFI.keepOpen THEN Close[fileName];
    END;
    
  GetNextToken: PUBLIC PROCEDURE [source: String.SubString, token: STRING] 
    RETURNS [valid: BOOLEAN] =
    BEGIN valid ← RealGetNextToken[source, token, FALSE]; END;
    
  GetNextTokenAsANumber: PUBLIC PROCEDURE [source: String.SubString] 
    RETURNS [i: INTEGER] =
    BEGIN
    token: STRING ← [20];
    IF RealGetNextToken[source, token, TRUE] THEN
      RETURN[String.StringToDecimal[token]]
    ELSE RETURN[0];
    END;
    
  ReadLineOrToken: PUBLIC PROCEDURE [
    sh: Compatibility.SHandle, buffer: STRING, terminator: CHARACTER]
    RETURNS[c: CHARACTER] =
    BEGIN
    started: BOOLEAN ← FALSE;
    buffer.length ← 0;
    c ← Ascii.NUL;
    DO SELECT (c ← Streams.GetByte[sh ! Streams.End[] => EXIT]) FROM
	terminator, Ascii.ControlZ, Ascii.CR => EXIT;
	Ascii.SP, Ascii.TAB => NULL;
	ENDCASE => started ← TRUE;
      IF started THEN String.AppendChar[buffer, c
	  ! String.StringBoundsFault => GOTO TooMany];
      ENDLOOP;
    IF c=Ascii.ControlZ THEN
      WHILE c # Ascii.CR DO
	  c ← Streams.GetByte[sh ! Streams.End[] => EXIT] ENDLOOP;
    EXITS
      TooMany => NULL;
    END;
    
  TitleMatch: PUBLIC PROCEDURE [buffer, title: STRING]
    RETURNS [BOOLEAN] =
    BEGIN
    llSubString, titleSubString: String.SubStringDescriptor;
    IF buffer.length < title.length+2 THEN RETURN[FALSE];
    IF buffer[0] # '[ THEN RETURN[FALSE];
    IF buffer[title.length+1] # '] THEN RETURN[FALSE];
    llSubString ← [buffer, 1, title.length];
    titleSubString ← [title, 0, title.length];
    RETURN[String.EquivalentSubString[@llSubString, @titleSubString]]
    END;
    
-- More efficient ones 

  OpenSection: PUBLIC PROCEDURE [fileName, title: STRING]
    RETURNS [BOOLEAN] =
    BEGIN
    Open[fileName ! Segments.FileNameProblem[] => GOTO badFile];
    IF FindSection[title] THEN RETURN[TRUE];
    RETURN[FALSE];
    EXITS
      badFile => RETURN[FALSE];
    END;
    
  NextItem: PUBLIC PROCEDURE RETURNS[name, args: STRING] =
    BEGIN
    terminator: CHARACTER;
    localLine: STRING ← [lineSize];
    name ← args ← NIL;
    terminator ← ReadLineOrToken[lastOpenedCFI.sh, localLine, ':];
    IF terminator # ': THEN
      BEGIN
      [] ← ReadLineOrToken[lastOpenedCFI.sh, localLine, Ascii.CR];
      RETURN[NIL,NIL];
      END;
    name ← Storage.CopyString[localLine];
    [] ← ReadLineOrToken[lastOpenedCFI.sh, localLine, Ascii.CR];
    args ← IF localLine.length = 0 THEN NIL ELSE Storage.CopyString[localLine];
    RETURN[name, args];
    END;
    
-- Use "User.cm" as fileName in above

  UserDotCmOpen: PUBLIC PROCEDURE = BEGIN Open[userDotCmName]; END;
  
  UserDotCmClose: PUBLIC PROCEDURE = BEGIN Close[userDotCmName]; END;
  
  UserDotCmLine: PUBLIC PROCEDURE [title, name: STRING] RETURNS [STRING] =
    BEGIN
    RETURN[Line[fileName: userDotCmName, title: title, name: name]];
    END;
    
  UserDotCmOpenSection: PUBLIC PROCEDURE [title: STRING] RETURNS [BOOLEAN] =
    BEGIN RETURN[OpenSection[userDotCmName, title]]; END;
    
  UserDotCmNextItem: PUBLIC PROCEDURE RETURNS[name, args: STRING] =
    BEGIN
    -- Support pulling entries out of two files at the same time since it is
    -- not too hard and some clown will probably try it.
    saveCFI: CmFileInfoHandle ← lastOpenedCFI;
    lastOpenedCFI ← @userDotCmInfo;
    [name, args] ← NextItem[];
    lastOpenedCFI ← saveCFI;
    RETURN[name, args];
    END;
    
-- Private Procedures

  FileClose: PRIVATE PROCEDURE [fileName: STRING] =
    BEGIN
    IF lastOpenedCFI = NIL THEN
      {SIGNAL Error[noneOpen]; RETURN};
    IF ~String.EquivalentString[fileName, lastOpenedCFI.fileName] THEN
      SIGNAL Error[notOpen];
    IF lastOpenedCFI.sh # NIL THEN Streams.Destroy[lastOpenedCFI.sh];
    -- There are other pointers to lastOpenedCFI↑, so NIL the freed pointers!
    lastOpenedCFI.fh ← NIL;
    lastOpenedCFI.sh ← NIL;
    END;
    
  FileOpen: PRIVATE PROCEDURE [fileName: STRING] =
    BEGIN
    isUserDotCm: BOOLEAN = IsUserDotCm[fileName];
    IF isUserDotCm THEN lastOpenedCFI ← @userDotCmInfo
    ELSE
      BEGIN
      lastOpenedCFI ← @otherCmInfo;
      IF ~String.EquivalentString[fileName, lastOpenedCFI.fileName] THEN
	BEGIN
	IF lastOpenedCFI.keepOpen THEN
	  BEGIN
	  SIGNAL Error[multipleOpens];
	  FileClose[lastOpenedCFI.fileName];
	  lastOpenedCFI.keepOpen ← FALSE;
	  END;
	lastOpenedCFI.fileName ←
	  Storage.FreeStringNil[lastOpenedCFI.fileName];
	lastOpenedCFI.fh ← NIL;  -- Different file => bad FHandle
	END;
      END;
    IF lastOpenedCFI.keepOpen THEN
      {Streams.SetIndex[lastOpenedCFI.sh, 0]; RETURN};
    IF lastOpenedCFI.fh = NIL THEN
      lastOpenedCFI.fh ← Segments.NewFile[fileName, Segments.Read];
    lastOpenedCFI.sh ← Streams.CreateStream[lastOpenedCFI.fh, Segments.Read
      ! Segments.FileUnknown[] =>
	BEGIN
	Segments.ReleaseFile[lastOpenedCFI.fh];
	lastOpenedCFI.fh ← Segments.NewFile[fileName, Segments.Read];
	RETRY;
	END];
    IF lastOpenedCFI.fileName = NIL THEN
      lastOpenedCFI.fileName ← Storage.CopyString[fileName];
    END;
    
  FindLineGivenSection: PRIVATE PROCEDURE [localLine, name: STRING]
    RETURNS [BOOLEAN] =
    BEGIN
    DO
      [] ← ReadLineOrToken[lastOpenedCFI.sh, localLine, Ascii.CR];
      IF localLine.length = 0 THEN RETURN[FALSE];
      IF localLine[0]='[ THEN RETURN[FALSE];
      IF NameMatch[localLine, name] THEN RETURN[TRUE];
      ENDLOOP;
    END;
    
  FindSection: PRIVATE PROCEDURE [title: STRING] RETURNS [BOOLEAN] =
    BEGIN
    temp: STRING ← [lineSize];
    DO
      IF ReadLineOrToken[lastOpenedCFI.sh, temp, Ascii.CR] = Ascii.NUL THEN
	RETURN[FALSE];
      IF temp.length # 0 AND TitleMatch[temp, title] THEN RETURN[TRUE];
      ENDLOOP;
    END;
    
  IsUserDotCm: PRIVATE PROCEDURE [fileName: STRING] RETURNS [b: BOOLEAN] =
    BEGIN
    oldLength: CARDINAL = fileName.length;
    IF fileName[fileName.length-1] = '. THEN
      fileName.length ← fileName.length - 1;
    b ← String.EquivalentString[fileName, userDotCmName];
    fileName.length ← oldLength;
    END;
    
  NameMatch: PRIVATE PROCEDURE [localLine, name: STRING]
    RETURNS [BOOLEAN] =
    BEGIN
    llSubString, nameSubString: String.SubStringDescriptor;
    IF localLine.length < name.length+2 THEN RETURN[FALSE];
    IF localLine[name.length] # ': THEN RETURN[FALSE];
    IF localLine[name.length+1] # Ascii.SP THEN RETURN[FALSE];
    llSubString ← [localLine, 0, name.length];
    nameSubString ← [name, 0, name.length];
    RETURN[String.EquivalentSubString[@llSubString, @nameSubString]]
    END;
    
  OpenAndMatch: PRIVATE PROCEDURE [localLine, fileName, title, name: STRING]
    RETURNS [BOOLEAN] = INLINE
    BEGIN
    FileOpen[fileName !
      Segments.FileNameError => GOTO badFile;
      Error => RESUME];
    RETURN[(FindSection[title] AND FindLineGivenSection[localLine, name])];
    EXITS
      badFile => RETURN[FALSE];
    END;
    
  RealGetNextToken: PRIVATE PROCEDURE [
    source: String.SubString, token: STRING, number: BOOLEAN] 
    RETURNS [valid: BOOLEAN] =
    BEGIN
    started: BOOLEAN ← FALSE;
    c: CHARACTER;
    token.length ← 0;
    DO
      IF source.length = 0 THEN EXIT;
      SELECT c ← CmFile.Lop[source] FROM
	Ascii.SP, Ascii.TAB => IF started THEN EXIT;
	Ascii.CR, ': => EXIT; 
	'-, IN ['0..'9], 'B, 'D =>
	  BEGIN
	  IF token.length < token.maxlength THEN
	    {token[token.length] ← c; token.length ← token.length + 1};
	  started ← TRUE;
	  END;
	ENDCASE => 
	  BEGIN
	  IF number THEN EXIT;
	  IF token.length < token.maxlength THEN
	    {token[token.length] ← c; token.length ← token.length + 1};
	  started ← TRUE;
	  END;
      ENDLOOP;
    RETURN[token.length # 0];
    END;
    
    
END.  -- of CmFilesA