-- File: WalnutUpdateImpl.mesa
-- Contents:
-- reads the logStream, starting at startPos, & makes the appropriate database updates

-- Created by: Willie-Sue on April 27, 1983
-- Last edited by:
-- Willie-Sue on May 31, 1983 3:55 pm

DIRECTORY
DB,
 FileIO,
IO,
 Rope,
 RopeIO,
 Runtime,
 WalnutDB,
 WalnutDBLog,
 WalnutLog,
 WalnutRetrieve,
 WalnutStream,
 WalnutWindow;

WalnutUpdateImpl: CEDAR PROGRAM

IMPORTS DB, FileIO, IO, Rope, RopeIO,
 WalnutDB, WalnutDBLog, WalnutLog, WalnutRetrieve,
 WalnutStream, WalnutWindow

EXPORTS WalnutStream =

BEGIN OPEN DB, WalnutDB, WalnutDBLog, WalnutLog, WalnutStream, WalnutWindow;

skipRope: ROPE← "Unexpected EOF; trying for next entry";

-- ********************************************************
UpdateFromStream: PUBLIC PROC [strm: IO.STREAM, startPos: INT] RETURNS[success: BOOL] =
BEGIN
doReport: BOOL← (startPos = 0);  -- report if scavenging from beginning
BadFormat: PROC[s: ROPE] =
{ IF doReport THEN
  Report[IO.PutFR["Bad log file format at %g: %g", IO.int[strm.GetIndex[]], IO.rope[s]]]
};

BEGIN ENABLE IO.EndOfStream => {BadFormat["Unexpected EOF"]; GOTO GiveUp};
entryLength, prefixLength, beginMsgPos: INT;
msg: Msg;
msgSet: MsgSet;
entryChar: CHAR;

DomainUpdate: PROC RETURNS[BOOL] =
BEGIN
  msName, tag: ROPE;
  [tag, msName]← TagAndValue[strm, 2];
IF NOT tag.Equal["name"] THEN {BadFormat["expected name"]; RETURN[FALSE]};
  ReportRope[Rope.FromChar[entryChar]];
SELECT entryChar FROM
'- =>
{msgSet← DeclareMsgSet[msName, OldOnly].msgSet;
IF msgSet = NIL THEN RETURN[TRUE];
[]← DestroyMsgSet[msgSet]
};
'+ => []← DeclareMsgSet[msName];
ENDCASE => ERROR;
RETURN[TRUE];
END;

RelationUpdate: PROC RETURNS[BOOL] =
BEGIN
mName, categoryName, tag: ROPE;
[tag, mName]← TagAndValue[strm, 2];
IF NOT tag.Equal["of"] THEN {BadFormat["of?"]; RETURN[FALSE]};
msg← DeclareMsg[mName, OldOnly].msg;
IF msg=NIL THEN {BadFormat[Rope.Cat[mName, " doesn\'t exist!"]]; RETURN[FALSE]};
[tag, categoryName]← TagAndValue[strm, 2];
IF NOT tag.Equal["is"] THEN {BadFormat["is?"]; RETURN[FALSE]};
ReportRope[Rope.FromChar[entryChar]];
SELECT entryChar FROM
'- =>
{ msgSet← DeclareMsgSet[categoryName, OldOnly].msgSet; -- check if msgSet exists
IF msgSet # NIL THEN []← RemoveMsgFromMsgSet[msg, msgSet]
};
'+ => -- create msgSet if it doesn't exist
 []← AddMsgToMsgSet[msg, DeclareMsgSet[categoryName].msgSet]
ENDCASE => ERROR;
RETURN[TRUE];
END;

ProcessHasBeenRead
: PROC =
BEGIN
  len: INT;
  mName: ROPE← RopeIO.GetRope[strm, prefixLength-minPrefixLength ! IO.EndOfStream =>
  { Report["\nUnexpected EOF, ignoring HasBeenRead entry"]; GOTO eof}];
IF mName.Fetch[len← mName.Length[]-1] = '\n THEN mName← mName.Substr[0, len];
  msg← DeclareMsg[mName, OldOnly].msg;
IF msg = NIL THEN RETURN; -- ignore
  SetMsgHasBeenRead[msg];

EXITS eof => RETURN;
END;

strm.SetIndex[startPos];
DO
-- Read *start* from log
[beginMsgPos, prefixLength, entryLength, entryChar]← FindStartOfEntry[strm, doReport];
IF entryLength = -1 THEN RETURN[TRUE];
IF (entryLength=0) OR (entryLength=prefixLength) AND
NOT (entryChar = '←) THEN RETURN[TRUE];
-- Do delete, create, or message update to database
SELECT entryChar FROM
'← => ProcessHasBeenRead[]; -- mark message as read

'+, '- => -- add or remove relship
BEGIN
foo, domainOrRelation: ROPE;
ok: BOOL;
strm.SetIndex[beginMsgPos+prefixLength];  -- ignore prefix info
[domainOrRelation, foo] ← TagAndValue[strm, 2];

IF domainOrRelation.Equal["Domain"] THEN
{ IF (ok← foo.Equal["MsgSet"]) THEN ok← DomainUpdate[] -- Add or delete MsgSet
ELSE BadFormat[IO.PutFR["%g is not a valid domain", IO.rope[foo]]];
}
ELSE IF domainOrRelation.Equal["Relation"] THEN
{ IF (ok← foo.Equal["mCategory"]) THEN ok← RelationUpdate[] --AddTo or RemoveFrom
ELSE BadFormat["expected mCategory"]
}
   ELSE -- domainOrRelation not "Domain" or "Relation"
{BadFormat["not domain or relation"]; ok← FALSE};

IF ~ok THEN strm.SetIndex[beginMsgPos+entryLength] -- next entry
ELSE IF strm.GetChar[]#'\n THEN BadFormat["Missing CR"];-- Skip over the trailing CR
END;

ENDCASE => -- regular message entry
BEGIN
msgRec: MsgRec←
 WalnutStream.MsgRecFromStream[strm, prefixLength, entryLength-prefixLength];
IF entryChar # '? THEN msgRec.hasBeenRead← TRUE;
[]← MsgRecToMsg[msgRec];
ReportRope["."];
strm.SetIndex[beginMsgPos+entryLength];
END;
ENDLOOP;

EXITS GiveUp => RETURN[TRUE];  -- try to press on with what we have
END;
END;

ExpungeFromStream: PUBLIC PROC[strm, tempLog: IO.STREAM, doUpdates, tailRewrite: BOOL]
  RETURNS[startExpungePos: INT, ok: BOOL] =
-- Dumping is driven from the log file & preserves the bits that came from Grapevine
-- Dumps all the Msgs in the database to a tempLog, setting all
-- their body pointers to reference the NEW log, then copies tempLog to logStream
BEGIN OPEN IO;
newLen: INT;
BEGIN ENABLE IO.EndOfStream => GOTO badLogFile;
msg: Msg;

toBeDestroyedList: LIST OF Msg;
count: INT← 0;
expungeFilePos, startPos, prefixLength, entryLength: INT;
entryChar: CHAR;
numBad: INTEGER← 0;
DoCount: PROC =
{IF (count← count + 1) MOD 10 = 0 THEN
 ReportRope[IF count MOD 100 = 0 THEN "!" ELSE "~"]
};
WriteBadLogEntry: PROC[startPos: INT] RETURNS[nextStartPos: INT] =
 { errorStream: STREAM← FileIO.Open["WalnutExpunge.errlog", write];
  line: ROPE;
IF (numBad← numBad+1) = 1 THEN errorStream.SetIndex[0]
ELSE errorStream.SetIndex[errorStream.GetLength[]];
  strm.SetIndex[startPos];
  errorStream.PutRope[
   IO.PutFR["\n Bad entry from pos %g of log file, written at %g\n", int[startPos], time[]]];
  errorStream.PutRope[strm.GetSequence[]]; -- *start* line
  errorStream.PutChar[strm.GetChar[]];  -- CR
  DO

  nextStartPos← strm.GetIndex[];
  line← strm.GetSequence[];
IF line.Equal["*start*"] THEN {nextStartPos← strm.GetIndex[]-8; EXIT};
  errorStream.PutRope[line];
  errorStream.PutChar[strm.GetChar[]];
ENDLOOP;
  errorStream.SetLength[errorStream.GetIndex[]];
  errorStream.Close[];
};
CopyEntryIfNecessary: PROC =
 { thisTag, thisValue: ROPE← NIL;
  thisMsg: Msg;
  doCopy: BOOLTRUE;
  prefixPos: INT;
  IF entryChar = '← THEN  -- hasbeenread entry
   { -- strm.SetIndex[startPos+minPrefixLength]; where strm is pos'd
   thisValue← RopeIO.GetRope[strm, prefixLength-minPrefixLength-1];
   []← strm.GetChar[ ! IO.EndOfStream => CONTINUE]
   }
   ELSE
  { strm.SetIndex[startPos+prefixLength];
   [thisTag, thisValue]← TagAndValue[strm, 2];
   IF thisTag.Equal["Relation"] AND thisValue.Equal["mCategory"] THEN
   { [thisTag, thisValue]← TagAndValue[strm, 2];
   IF ~thisTag.Equal["of"] THEN thisValue← NIL;
   };
   };
  IF thisValue # NIL THEN
   { thisMsg← DeclareMsg[thisValue, OldOnly].msg;
   IF ~Null[thisMsg] THEN
   IF (prefixPos← V2I[GetP[thisMsg, mPrefixPos]]) >= startExpungePos THEN
     doCopy← FALSE;
   };
  IF doCopy THEN
   { numToDo: INT;
   strm.SetIndex[startPos];
FOR numToDo← entryLength, numToDo-512 UNTIL numToDo<512 DO
[]← tempLog.GetBlock[copyBuffer];
strm.PutBlock[copyBuffer];
ENDLOOP;
IF numToDo # 0 THEN
{ copyBuffer.length← numToDo;
[]← tempLog.GetBlock[copyBuffer];
strm.PutBlock[copyBuffer];
copyBuffer.length← 512;
};
   };
 };

tempLog.SetIndex[0];
startExpungePos←
IF tailRewrite THEN V2I[GetF[walnutInfoRelship, wStartExpungePos]] ELSE 0;
strm.SetIndex[startExpungePos];

DO   -- loop for dumping messages & making other log entries
msgID: ROPENIL;
fullText: ROPENIL;
headersPos, curPos: INT;
badEntry: BOOLFALSE;

[startPos, prefixLength, entryLength, entryChar]← FindStartOfEntry[strm, TRUE];
IF entryLength = 0 THEN EXIT;
IF (entryChar # ' ) AND (entryChar # '?) THEN-- perhaps copy other entry to tempLog
{ IF startExpungePos # 0 THEN CopyEntryIfNecessary[]
ELSE strm.SetIndex[startPos+entryLength];
LOOP
};

headersPos← startPos+prefixLength;
UNTIL (curPos← strm.GetIndex[]) = headersPos DO
-- look for a msgID: field
  tag, value: ROPE;

[tag, value]← TagAndValue[strm: strm, inc: 2];
IF Rope.Equal[tag, msgIDRope, FALSE] THEN msgID← value;
IF curPos > headersPos THEN -- bad entry we don't understand
 { nextStartPos: INT← WriteBadLogEntry[startPos];
  cMsg: ROPE← " Confirm to push on, deny to stop the Expunge";
ReportRope[PutFR["\nBad message entry at log pos %g;", int[startPos]]];
  Report[" entry written on WalnutExpunge.errlog"];
  Report[cMsg];
IF (ok← UserConfirmed[])
    THEN {strm.SetIndex[nextStartPos]; badEntry← TRUE; EXIT};
RETURN[startExpungePos, FALSE]};
ENDLOOP;
IF badEntry THEN LOOP;

IF msgID = NIL THEN
BEGIN  -- have to read fullText to fashion msgID
mr: MsgRec← NEW[MessageRecObject];
[]← WalnutRetrieve.ParseMsgIntoFields[mr, strm, entryLength-prefixLength !
IO.EndOfStream => {Report[skipRope]; strm.SetIndex[startPos+entryLength]; LOOP}];
msgID← ConstructMsgID[mr];
strm.SetIndex[startPos+prefixLength];  -- back up stream
END;

msg← DeclareMsg[msgID, OldOnly].msg;
IF (msg=NIL) OR Null[msg] THEN {strm.SetIndex[startPos+entryLength]; LOOP}
ELSE
BEGIN
curPrefixPos: INT;
thisMsgSet: MsgSet;
nameOfCatsList: ROPENIL;

-- if is only in deletedMsgSet, don't put on tempLog
FOR mL: LIST OF DB.Value← GetPList[msg, mCategoryIs], mL.rest UNTIL mL=NIL DO
IF ~Eq[thisMsgSet← V2E[mL.first], deletedMsgSet] THEN
  nameOfCatsList← Rope.Cat[nameOfCatsList, " ", DB.GetName[thisMsgSet]];
ENDLOOP;

IF nameOfCatsList.Length[] = 0 THEN
  { toBeDestroyedList← CONS[msg, toBeDestroyedList]; GOTO doneWithOne};
  msgID← Rope.Cat[msgID, "\nCategories:", nameOfCatsList];

IF fullText = NIL THEN fullText←
RopeIO.GetRope[strm, entryLength-prefixLength ! IO.EndOfStream =>
{Report[skipRope]; GOTO doneWithOne}];

DoCount[];
expungeFilePos← MakeLogEntry[
   tempLog,
IF V2B[GetP[msg, mHasBeenReadIs]] THEN message ELSE newMessage,
 fullText, Rope.Cat[msgIDRope, ": ", msgID, "\n"]];

-- set header & body pos pointers to posn in NEW log if doing updates:
IF doUpdates THEN
  { thisPrefixPos: INT← startExpungePos + expungeFilePos - entryLength;
  curPrefixPos← V2I[GetP[msg, mPrefixPos]];
  IF curPrefixPos # thisPrefixPos THEN
  { []← SetP[msg, mHeadersPos, I2V[thisPrefixPos+prefixLength]];
   []← SetP[msg, mPrefixPos, I2V[thisPrefixPos]];
  };
  };


EXITS
  doneWithOne => strm.SetIndex[startPos+entryLength];
END;  -- put message on log file
ENDLOOP;  -- loop for dumping messages

Report[PutFR["\nDumped %g messages from Log File... ", int[count]]];
tempLog.SetLength[newLen← tempLog.GetIndex[]];
tempLog.Flush[];  -- make sure tempLog is in good shape
Report[PutFR["Old log length was %g bytes", int[strm.GetLength[]]]];
IF tailRewrite THEN
{ Report[PutFR
  ["Previous end of log was %g bytes", int[strm.GetLength[]-startExpungePos]]];
Report[PutFR["New end of log file is %g bytes (%g messages)", int[newLen], int[count]]];
Report[PutFR["New log length is %g bytes", int[startExpungePos+newLen]]];
}
ELSE
{ Report[PutFR["Old log length was %g bytes", int[strm.GetLength[]]]];
Report[PutFR["New log length is %g bytes (%g messages)", int[newLen], int[count]]]
};

IF doUpdates THEN
 { ReportRope[" Destroying Msgs in Deleted MsgSet ..."];
-- may see a msg more than once in the log file, hence must check if already destroyed
  count← 0;
FOR mL: LIST OF Msg← toBeDestroyedList, mL.rest UNTIL mL = NIL DO
  IF ~Null[mL.first] THEN
   { DestroyEntity[mL.first];  -- destroy the message
   DoCount[];
   };
ENDLOOP;
  Report[PutFR["\n%g Msgs destroyed",int[count]]];
BEGIN   -- fix up num in deleted (should be 0 but who knows)
  num: INT← V2I[GetP[deletedMsgSet, msNumInSetIs]] - count;
  []← SetP[deletedMsgSet, msNumInSetIs, I2V[num]];
END;
};  -- end doUpdates

RETURN[startExpungePos, TRUE];

EXITS
badLogFile =>
{ tempLog.SetLength[tempLog.GetIndex[]];
tempLog.Close[];
RETURN[startExpungePos, FALSE]
};
END;
END;

END.