-- File: WalnutStreamImpl.mesa
-- Contents:
-- procedures for reading & writing Walnut log-Style files
-- This module is NOT a MONITOR; its assumes that its caller has a lock on the
-- stream if that is appropriate

-- Created by: Willie-Sue on April 27, 1983
-- Last edited by:
-- Willie-Sue on December 5, 1983 11:27 am

DIRECTORY
AlpineFS USING [OpenFileFromStream],
BasicTime USING [Now],
FS USING [OpenFile, OpenFileFromStream, SetByteCountAndCreatedTime],
IO,
 GVBasics USING [ItemHeader, ItemType],
 GVRetrieve USING [Failed, GetItem, Handle, NextItem],
 Rope,
 RopeIO USING [GetRope, PutRope],
 RuntimeError USING [BoundsFault],
 WalnutLog USING [LogEntryType, MsgRec, MessageRecObject, minPrefixLength,
     categoriesRope, copyBuffer, msgIDRope,
     AddMessageToLog, ConstructMsgID, ParseMsgID],
 WalnutRetrieve USING [ParseMsgIntoFields],
 WalnutStream,
 WalnutWindow USING [logIsAlpineFile, Report, ReportRope];

WalnutStreamImpl: CEDAR PROGRAM
IMPORTS AlpineFS, BasicTime, FS, IO, GVRetrieve, Rope, RopeIO, RuntimeError,
 WalnutLog, WalnutRetrieve, WalnutWindow
EXPORTS WalnutStream =
  
BEGIN OPEN WalnutLog;

ROPE: TYPE = Rope.ROPE;
TiogaCTRL: GVBasics.ItemType;

-- ********************************************************

ReadPrefixInfo: PUBLIC PROC[strm: IO.STREAM, headersPos: INT]
  RETURNS[msgID: ROPE, categories: ROPE, outOfSynch: BOOL] =
BEGIN
 curPos: INT;
 outOfSynch← FALSE;

UNTIL (curPos← strm.GetIndex[]) = headersPos DO
  tag, value: ROPE;
IF curPos > headersPos THEN {outOfSynch← TRUE; RETURN};
  [tag, value]← TagAndValue[strm: strm, inc: 2];
IF Rope.Equal[tag, msgIDRope, FALSE] THEN msgID← value
ELSE IF Rope.Equal[tag, categoriesRope, FALSE] THEN categories← value;
ENDLOOP;
END;

TagAndValue: PUBLIC PROC[strm: IO.STREAM, inc: INT] RETURNS[tag, value: ROPE] =
BEGIN
line: ROPE← strm.GetLineRope[];
pos: INT← line.Find[":"];
IF pos < 0 THEN RETURN;
tag← line.Substr[0, pos];
value← line.Substr[pos+inc, Rope.MaxLen ! RuntimeError.BoundsFault =>
  {tag← NIL; CONTINUE}];
END;

FindStartOfEntry: PUBLIC PROC[strm: IO.STREAM, doReport: BOOL]
RETURNS[startPos, prefixLength, entryLength: INT, entryChar: CHAR] =
BEGIN ENABLE IO.EndOfStream => GOTO eoS;
line: ROPE;
relPos: INT← 0;
startFound: BOOL;
prefixLength← entryLength← 0;
entryChar← 'X;  -- not a valid entryType char

IF strm.EndOf[] THEN RETURN;
startPos← strm.GetIndex[];
line← strm.GetLineRope[];
[startFound, relPos]← IsStart[line];
IF NOT startFound THEN
BEGIN
IF doReport THEN
 WalnutWindow.Report[
IO.PutFR["*start* not found at pos %g. Skipping to next entry.", IO.int[startPos]]];
UNTIL startFound DO
IF strm.EndOf[] THEN RETURN;
  startPos← strm.GetIndex[];
  line← strm.GetLineRope[];
  [startFound, relPos]← IsStart[line];
ENDLOOP;
END;

-- Read entry info line from log, e.g.: 00101 00029 US+
startPos← startPos + relPos;
entryLength← strm.GetInt[];
prefixLength← strm.GetInt[];
line← strm.GetLineRope[];
entryChar← line.Fetch[line.Length[]-1];

EXITS
eoS => {entryLength← -1; RETURN};
END;

starStartStar: ROPE = "*start*";
IsStart: PUBLIC PROC[line: ROPE] RETURNS[startFound: BOOL, relPos: INT] =
BEGIN
 relPos← 0;
IF line.Length = 0 THEN RETURN[FALSE, relPos];
IF line.Equal[starStartStar] THEN RETURN[TRUE, relPos];
IF (relPos← line.Length[] - starStartStar.Length[]) <=0 THEN RETURN[FALSE, relPos];
IF starStartStar.Equal[line.Substr[relPos]] THEN RETURN[TRUE, relPos];
RETURN[FALSE, relPos];
END;

RopeFromStream: PUBLIC PROC[strm: IO.STREAM, startPos, len: INT] RETURNS[ROPE] =
-- reads arbitrary length ROPE from a stream
BEGIN ENABLE IO.EndOfStream => GOTO TooShort;

 strm.SetIndex[startPos];
RETURN[RopeIO.GetRope[strm, len]];

EXITS TooShort =>
RETURN[IO.PutFR["[message body lost!!! (%g,%g)]\n", IO.int[startPos], IO.int[len]]];
END;

FlushAndSetCreateDate: PUBLIC PROC[strm: IO.STREAM] =
-- flushes the stream and sets the create date to BasicTime.Now
BEGIN
 of: FS.OpenFile;
 strm.Flush[];
 of← IF WalnutWindow.logIsAlpineFile THEN AlpineFS.OpenFileFromStream[strm]
  ELSE FS.OpenFileFromStream[strm];
FS.SetByteCountAndCreatedTime[of, -1, BasicTime.Now[]];
END;

-- ********************************************************

MakeLogEntry: PUBLIC PROC [
  strm: IO.STREAM, entryType: LogEntryType, entryText: ROPE, msgID: ROPENIL,
  doFlush: BOOLTRUE]
RETURNS [endStrmPos, prefixLength, entryLength: INT] =
-- Puts entryText out in Laurel format on the given stream.
-- There are four kinds of entries:
-- (1) messages: entryText is the header and body of the message
-- (2) insertions: entryText is of form (insertion of new relship):
-- Relation: relation
-- attr1: xxx
-- attr2: yyy
-- or of the form (for insertion of new entity):
-- Domain: domain
-- name: xxx
-- (3) deletions: entryText same form as above, but represents deletion of that relship or entity
-- (4) hasbeenread: entryText is the messageID

-- Returns integer position in file of end of log entry.
BEGIN OPEN IO;
typeChar: CHAR;
isMessage: BOOL← (entryType=message) OR (entryType=newMessage);

prefixLength← minPrefixLength + msgID.Length[];   -- magic number
entryLength← entryText.Length[] + prefixLength;

SELECT entryType FROM
message => typeChar← ' ;
newMessage => typeChar← '?;
insertion => typeChar← '+;
deletion => typeChar← '-;
hasbeenread => typeChar← '←
ENDCASE;

-- can't fool with msg if it has formatting, so don't ever add CR at end
strm.SetIndex[strm.GetLength[]];
strm.PutF["*start*\n%05d %05d US%g\n",
    int[entryLength], int[prefixLength], char[typeChar]];

IF msgID.Length[]#0 THEN strm.PutRope[msgID];
RopeIO.PutRope[strm, entryText];
IF doFlush THEN FlushAndSetCreateDate[strm];
RETURN[strm.GetIndex[], prefixLength, entryLength]
END;

GVLogEntry: PUBLIC PROC [strm: IO.STREAM, gvH: GVRetrieve.Handle, prefix: ROPE]
  RETURNS [lastIndex: INT, ok: BOOL] =
BEGIN
 startPos: INT← strm.GetLength[];  -- where this message begins
 strm.SetIndex[startPos];

BEGIN ENABLE GVRetrieve.Failed => GOTO gvFailed;

 prefixLen: INT← minPrefixLength + prefix.Length[];

 msgLen, lenWritten: INT← 0;
 prefixWritten, ctrlInfo: BOOLFALSE;

WritePrefix: PROC =
BEGIN
  strm.PutF["*start*\n%05d %05d US%g\n",
    IO.int[prefixLen+msgLen], IO.int[prefixLen], IO.char['?]];
  strm.PutRope[prefix];
  lenWritten← msgLen;
  prefixWritten← TRUE;
END;

CopyItemToLog: PROC =
BEGIN
-- 
strm.SetIndex[strm.GetLength[]]; --  -- make sure at end
  is: IO.STREAM← GVRetrieve.GetItem[gvH];
  DO

  bytes: INT;
  IF (bytes← is.GetBlock[copyBuffer, 0, 512]) = 0 THEN EXIT;
--  copyBuffer.length← bytes;--  -- grapevineUser bug
  strm.PutBlock[copyBuffer];
ENDLOOP;
END;

DO
item: GVBasics.ItemHeader ← GVRetrieve.NextItem[gvH];
SELECT item.type FROM
PostMark, Sender, ReturnTo => ERROR;
Audio => NULL;  -- we may use this soon
Recipients, Capability, updateItem, reMail => NULL;
Text =>
{ IF ctrlInfo THEN ERROR;  -- items in wrong order!!!
msgLen← msgLen + LOOPHOLE[item.length, INT];
IF ~prefixWritten THEN WritePrefix[];
CopyItemToLog[];
};
TiogaCTRL =>
{ ctrlInfo← TRUE;
msgLen← msgLen + LOOPHOLE[item.length, INT];
CopyItemToLog[];
};
LastItem => EXIT;
ENDCASE => LOOP;
ENDLOOP;

IF ~ctrlInfo THEN
  { ch: CHAR;
  strm.SetIndex[strm.GetLength[]-1];
IF (ch← strm.GetChar[]) # '\n THEN {strm.PutChar['\n]; msgLen← msgLen + 1};
  };
IF msgLen # lenWritten THEN { strm.SetIndex[startPos]; WritePrefix[]};
-- strm.Flush[];--  -- flush after write
RETURN[strm.GetLength[], TRUE];

EXITS
gvFailed => {strm.SetIndex[startPos]; strm.SetLength[startPos]; RETURN[startPos, FALSE]};
END;
END;

-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
MsgRecFromStream: PUBLIC PROC [strm: IO.STREAM, prefixLength, msgLength: INT]
  RETURNS[msgRec: MsgRec] =
BEGIN ENABLE IO.EndOfStream =>
  {WalnutWindow.Report["\nUnexpected EOF, ignoring msg entry"]; GOTO badMessage};

 prefixPos: INT← strm.GetIndex[] - minPrefixLength;
 headersPos: INT← prefixPos + prefixLength;
 msgID, categories: ROPENIL;
 outOfSynch: BOOL;
 categoriesList: LIST OF ROPE;

 [msgID, categories, outOfSynch]← ReadPrefixInfo[strm, headersPos];

IF outOfSynch THEN GOTO badMessage;
IF categories # NIL THEN
BEGIN OPEN IO;
  h: STREAM← RIS[categories];
UNTIL h.EndOf[] DO
  categoriesList← CONS[h.GetTokenRope[breakProc: IO.IDProc].token, categoriesList] ENDLOOP;
  h.Close[];
END;

 msgRec← NEW[MessageRecObject];
 []← WalnutRetrieve.ParseMsgIntoFields[msgRec, strm, msgLength];
IF (msgRec.gvID← msgID) = NIL THEN
  []← ConstructMsgID[msgRec] ELSE ParseMsgID[msgRec];

 msgRec.prefixPos← prefixPos;
 msgRec.headersPos← headersPos;
 msgRec.msgLength← msgLength;
 msgRec.categoriesList← categoriesList;

EXITS
  badMessage => RETURN[NIL];
END;

-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
-- works for Laurel & Hardy files, as well as archiveLogs

ReadOldMailFile: PUBLIC PROC[strm: IO.STREAM, defaultPrefix: ROPE]
  RETURNS [ok: BOOL] =
BEGIN ENABLE IO.EndOfStream => GOTO eos;
DO
beginMsgPos, msgLength, entryLength, prefixLength: INT;
msgID, msgText, prefix, categories: ROPE;
outOfSynch: BOOLFALSE;
pos: INT← 0;

[beginMsgPos, prefixLength, entryLength, ]← FindStartOfEntry[strm, TRUE];
IF entryLength = -1 THEN RETURN[FALSE];
IF (entryLength=0) OR (entryLength=prefixLength) THEN EXIT; -- Hardy's null message at end
IF IO.PeekChar[strm] = '@ THEN strm.SetIndex[beginMsgPos + prefixLength] -- hardy file
ELSE [msgID, categories, outOfSynch]← ReadPrefixInfo[strm, beginMsgPos+prefixLength];
IF outOfSynch THEN
  { WalnutWindow.Report["Can't find prefix info; skipping to next entry"];
  strm.SetIndex[beginMsgPos + entryLength];
LOOP
};

 prefix←
IF categories#NIL THEN Rope.Cat[categoriesRope, ": ", categories, "\n"] ELSE defaultPrefix;

IF msgID # NIL THEN prefix← Rope.Cat[msgIDRope, ": ", msgID, "\n", prefix];

 msgLength← entryLength-prefixLength;
 msgText← RopeIO.GetRope[strm, msgLength];
DO  -- replace \240's with spaces
  pos← msgText.SkipTo[pos, "\240\000"];
IF pos = msgLength THEN EXIT;
IF msgText.Fetch[pos] = '\000 THEN
  { IF msgText.Fetch[pos+1] = '\000 THEN EXIT ELSE pos← pos + 1}
ELSE msgText← msgText.Replace[start: pos, len: 1, with: " "];
ENDLOOP;

 AddMessageToLog[entryText: msgText, prefix: prefix];
 WalnutWindow.ReportRope["."];
ENDLOOP;
RETURN[TRUE];

EXITS eos => RETURN[FALSE]
END;

TRUSTED { TiogaCTRL← LOOPHOLE[1013B] };

END.