-- File: WalnutLogImpl.mesa
-- Contents:
-- types and procedures for writing on the log file

-- Created by: Willie-Sue on July 16, 1982
-- Last edited by:
-- Willie-Sue Hoo-Hah on June 6, 1983 1:31 pm

DIRECTORY

DB,
 DateAndTime USING [Parse, Unintelligible],
 GVBasics USING[Timestamp],
 GVRetrieve USING [Handle],
 FileIO USING[ commitAndReopenTransOnFlush, finishTransOnClose, Open],
 IO,
 Rope,
 UserCredentials USING [GetUserCredentials],
 ViewerTools USING [TiogaContentsRec],
 WalnutDB USING [InitializeDBVars],
 WalnutDBLog,
 WalnutLog,
 WalnutSendMail,
 WalnutStream,
 WalnutWindow;

WalnutLogImpl: CEDAR MONITOR
IMPORTS
  DateAndTime, FileIO, IO, Rope, UserCredentials,
  DB, WalnutDB, WalnutDBLog, WalnutSendMail,
  WalnutStream, WalnutWindow
EXPORTS WalnutLog, WalnutDBLog =
  
BEGIN OPEN DB, WalnutLog, WalnutStream, WalnutWindow;

SchemaVersionTime:
  PUBLIC GreenwichMeanTime← DateAndTime.Parse["March 24, 1983 9:24 am"].dt;

msgIDRope: PUBLIC ROPE← "gvMsgID";
categoriesRope: PUBLIC ROPE← "Categories";

bufferSize: CARDINAL = 512;
copyBuffer: PUBLIC REF TEXTNEW[TEXT[bufferSize]];  -- one to share

-- ********************************************************
logStream: IO.STREAMNIL;

lastMsgReadFromLog: INT; -- pos in log after last msg entered in database
lastMsgWrittenToLog: INT;  -- pos in log at end of last msg retrieved
skipRope: ROPE← "\nUnexpected EOF, skipping ahead.";
currentLogName: ROPE;  -- set by call to InitializeLog

-- ********************************************************
-- schemaVersion is time found in database
SchemaMismatch: PUBLIC SIGNAL[schemaVersion: GreenwichMeanTime] = CODE;

walnutLogInfo: PUBLIC Relation; -- used to keep current info about the log file
wExpectedLength: PUBLIC Attribute; -- int (how long the log file is)
wExpectedDBPos: PUBLIC Attribute; -- int (how much of the log has been parsed into Msgs)
wStartExpungePos: PUBLIC Attribute; -- int (where in log file the last/current expunge started)
wCopyInProgress: PUBLIC Attribute; -- bool (TRUE while Copying onto tail of log file)
wSchemaVersion: PUBLIC Attribute; -- time (for keeping track of changes in walnut's schema)
walnutInfoRelship: PUBLIC Relship;  -- need fetch & check only once

-- ********************************************************
-- see WalnutDB for parallel operation on Walnut's database

-- GVMessageToLog more or less corresponds to DeclareMsg

GVMessageToLog: PUBLIC ENTRY PROC[
 gvH: GVRetrieve.Handle, timeStamp: GVBasics.Timestamp, gvSender: RName]
  RETURNS[ok: BOOL] =
-- reads message items from Grapevine & makes a log entry for this message
{ ENABLE UNWIND => NULL;
prefix: ROPE
Rope.Cat[msgIDRope, ": ", gvSender, " $ ", RopeFromTimestamp[timeStamp],
  "\nCategories: Active\n"];
[lastMsgWrittenToLog, ok]← GVLogEntry[logStream, gvH, prefix];
};

msgSetEntry: ROPE = "Domain: MsgSet\nname: %g\n\n";
msgEntry: ROPE ="Relation: mCategory\nof: %g\nis: %g\n\n";

LogCreateMsgSet: PUBLIC ENTRY PROC[msName: ROPE] =
-- writes an entry for creating a msgSet
{ ENABLE UNWIND => NULL;
[]← MakeLogEntry[logStream, insertion, IO.PutFR[msgSetEntry, IO.rope[msName]]]
};

LogDestroyMsgSet: PUBLIC ENTRY PROC[msName: ROPE] =
-- writes an entry for destroying a msgSet
{ ENABLE UNWIND => NULL;
[]← MakeLogEntry[logStream, deletion, IO.PutFR[msgSetEntry, IO.rope[msName]]];
};

LogAddMsg: PUBLIC ENTRY PROC[mName: ROPE, msName: ROPE] =
-- writes an entry for adding a msg to a msgSet
{ ENABLE UNWIND => NULL;
[]← MakeLogEntry
  [logStream, insertion, IO.PutFR[msgEntry, IO.rope[mName], IO.rope[msName]]];
};

LogRemoveMsg: PUBLIC ENTRY PROC[mName: ROPE, msName: ROPE] =
-- writes an entry for removing a msg from a msgSet
{ ENABLE UNWIND => NULL;
[]← MakeLogEntry
  [logStream, deletion, IO.PutFR[msgEntry, IO.rope[mName], IO.rope[msName]]];
};

LogMsgHasBeenRead: PUBLIC ENTRY PROC[mName: ROPE] =
-- writes an entry for reading a msg (displaying for the first time)
{ ENABLE UNWIND => NULL;
[]← MakeLogEntry[logStream, hasbeenread, NIL, mName.Concat["\n"]]
};

-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
-- like GVMessageToLog, but is used by OldMailReader

AddMessageToLog: PUBLIC ENTRY PROC [entryText, prefix: ROPE] =
-- makes a message LogEntryType on log (used by old mail reader)
{ ENABLE UNWIND => NULL;
  lastMsgWrittenToLog← MakeLogEntry[logStream, message, entryText, prefix]
  };

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

InitializeLogVars: INTERNAL PROC =
BEGIN OPEN WalnutDBLog;
 walnutLogInfo← DeclareRelation["walnutLogInfo", $Walnut, OldOnly];
IF walnutLogInfo#NIL THEN DO  -- check schema version numbers
  { rs: RelshipSet;
  curVersion: GreenwichMeanTime;
  wSchemaVersion← DeclareAttribute[walnutLogInfo, "wSchemaVersion", TimeType];
  rs← RelationSubset[walnutLogInfo];
  walnutInfoRelship← NextRelship[rs];
  -- should only be one relship in this relation
  IF NextRelship[rs] # NIL THEN ERROR;
  IF walnutInfoRelship = NIL THEN {ReleaseRelshipSet[rs]; EXIT};
  ReleaseRelshipSet[rs];
  curVersion← V2T[GetF[walnutInfoRelship, wSchemaVersion]];
IF curVersion#SchemaVersionTime THEN SIGNAL SchemaMismatch[curVersion];
EXIT;
  };
ENDLOOP
ELSE
  { walnutLogInfo← DeclareRelation["walnutLogInfo", $Walnut];
  wSchemaVersion← DeclareAttribute[walnutLogInfo, "wSchemaVersion", TimeType];
  };

  wExpectedLength← DeclareAttribute[walnutLogInfo, "wExpectedLength", IntType];
  wExpectedDBPos← DeclareAttribute[walnutLogInfo, "wExpectedDBPos", IntType];
  wStartExpungePos← DeclareAttribute[walnutLogInfo, "wStartExpungePos", IntType];
  wCopyInProgress← DeclareAttribute[walnutLogInfo, "wCopyInProgress", BoolType];

BEGIN
  rs: RelshipSet← RelationSubset[walnutLogInfo];
  walnutInfoRelship← NextRelship[rs];
  -- should only be one relship in this relation
IF NextRelship[rs] # NIL THEN ERROR;
IF walnutInfoRelship = NIL THEN walnutInfoRelship← CreateRelship[walnutLogInfo];
  ReleaseRelshipSet[rs];
END;
  []← SetF[walnutInfoRelship, wSchemaVersion, T2V[SchemaVersionTime]];

END;

GetExpectedLogLength: PUBLIC ENTRY PROC RETURNS[INT] =
-- Retrieves last stored log length from database (used for recovery from crash)
{ ENABLE UNWIND => NULL;
RETURN
[V2I[GetF[walnutInfoRelship, wExpectedLength]]]
};

SetExpectedLogLength: PUBLIC ENTRY PROC[len: INT] =
-- sets the log length in the database
{ ENABLE UNWIND => NULL;
SetF[walnutInfoRelship, wExpectedLength, I2V[len]];
SetWalnutUpdatesPending[TRUE]
};

GetExpectedDBLogPos: PUBLIC ENTRY PROC RETURNS[INT] =
-- Retrieves last stored DB log pos from database (used for recovery from crash)
{ ENABLE UNWIND => NULL;
RETURN[V2I[GetF[walnutInfoRelship, wExpectedDBPos]]]
};

SetExpectedDBLogPos: PUBLIC ENTRY PROC[len: INT] =
-- sets the last stored DB log position
{ ENABLE UNWIND => NULL;
SetF[walnutInfoRelship, wExpectedDBPos, I2V[len]];
SetWalnutUpdatesPending[TRUE]
};

GetStartExpungePos: PUBLIC ENTRY PROC RETURNS[INT] =
-- Retrieves the time at which new mail was last retrieved
{ ENABLE UNWIND => NULL;
RETURN[V2I[GetF[walnutInfoRelship, wStartExpungePos]]]
};

SetStartExpungePos: PUBLIC ENTRY PROC[len: INT] =
 -- set the starting position of the next expunge
{ ENABLE UNWIND => NULL;
SetF[walnutInfoRelship, wStartExpungePos, I2V[len]];
SetWalnutUpdatesPending[TRUE]
};

GetCopyInProgress: PUBLIC ENTRY PROC RETURNS [BOOL] =
-- returns TRUE if a copy onto tail of log file is in progress
{ ENABLE UNWIND => NULL;
RETURN[V2B[GetF[walnutInfoRelship, wCopyInProgress]]]
};

SetCopyInProgress
: PUBLIC ENTRY PROC[doingCopy: BOOL] =
 -- set the copy in progress bit in the DB
{ ENABLE UNWIND => NULL;
SetF[walnutInfoRelship, wCopyInProgress, B2V[doingCopy]];
SetWalnutUpdatesPending[TRUE];
};

-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
-- utility routines & ones that read the log

LogLength: PUBLIC ENTRY PROC[doFlush: BOOL] RETURNS[curLength: INT] =
-- returns length of log file
{ ENABLE UNWIND => NULL;
IF logStream = NIL THEN RETURN[0];
IF doFlush THEN logStream.Flush[]; RETURN[logStream.GetLength[]];
};

-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
-- Expunge writes on or changes the current log

ExpungeMsgs: PUBLIC ENTRY PROC [tempLog: IO.STREAM, doUpdates, tailRewrite: BOOL]
  RETURNS[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
-- IF doUpdates it sets all msgs' body pointers to reference the NEW log and
-- copies tempLog to logStream
BEGIN ENABLE UNWIND => NULL;
 startExpungePos: INT;
 [startExpungePos, ok]← ExpungeFromStream[logStream, tempLog, doUpdates, tailRewrite];
IF ~ok THEN RETURN;
IF ~doUpdates THEN RETURN;

 Report["Copying tempLog to log file"];
 SetF[walnutInfoRelship, wCopyInProgress, B2V[TRUE]];
 SetF[walnutInfoRelship, wStartExpungePos, I2V[startExpungePos]];
 MarkTransaction[TransactionOf[$Walnut]];
 CopyTempLogToLog[tempLog, startExpungePos];
END;

-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
InitializeLog: PUBLIC ENTRY PROC[fileName: ROPE] RETURNS [curLength: INT] =
-- initializes fileName as the current log stream
BEGIN ENABLE UNWIND => NULL;
IF logStream # NIL THEN RETURN[logStream.GetLength[]];  -- already open
 currentLogName← fileName;
RETURN[DoInitializeLog[]];
END;

CloseLogStream: PUBLIC ENTRY PROC =
{ ENABLE UNWIND => NULL;
IF
logStream#NIL THEN logStream.Close[];
logStream← NIL
};

FinishExpunge: PUBLIC ENTRY PROC =
-- called if wCopyInProgress is TRUE during startup
BEGIN ENABLE UNWIND => NULL;
 startPos: INT← V2I[GetF[walnutInfoRelship, wStartExpungePos]];
 tempLog: IO.Handle← FileIO.Open["Walnut.TempLog"];
 CopyTempLogToLog[tempLog, startPos];
 MarkTransaction[TransactionOf[$Walnut]];
END;

ResetLogToExpectedLength: PUBLIC ENTRY PROC =
BEGIN ENABLE UNWIND => NULL;
 endPos: INT← V2I[GetF[walnutInfoRelship, wExpectedLength]];
 logStream.SetIndex[endPos];
 logStream.SetLength[endPos];
 logStream.Flush[];
END;

-- must know when the state of Walnut's segment is changing

OpenWalnutTransaction
: PUBLIC ENTRY PROC[trans: Transaction, noLog: BOOL] =
BEGIN ENABLE UNWIND => NULL;
 fileNotFound, transOpen: BOOLFALSE;
IF walnut#NIL THEN ReportRope[" Opening Walnut transaction ..."];
 OpenTransaction[
segment: $Walnut,
userName: WalnutSendMail.userRName,
password: UserCredentials.GetUserCredentials[].password,
useTrans: trans,
noLog: noLog ! Error =>
IF
code=FileNotFound THEN {fileNotFound← TRUE; CONTINUE}
ELSE IF code=TransactionAlreadyOpen THEN {transOpen← TRUE; CONTINUE}];

IF fileNotFound OR transOpen THEN
{ CloseTransaction[TransactionOf[$Walnut]];
IF fileNotFound THEN DeclareSegment[walnutSegmentFile, $Walnut,,, NewOnly];
OpenTransaction[
 segment: $Walnut,
 userName: WalnutSendMail.userRName,
 password: UserCredentials.GetUserCredentials[].password,
 useTrans: trans,
 noLog: noLog];
IF fileNotFound THEN
MarkTransaction[TransactionOf[$Walnut]]; -- make sure segment initializated
};

 InitializeLogVars[];
 WalnutDB.InitializeDBVars[];
 lastMsgReadFromLog← V2I[GetF[walnutInfoRelship, wExpectedDBPos]];
END;

CloseWalnutTransaction: PUBLIC ENTRY PROC =
BEGIN ENABLE UNWIND => NULL;
 len: INT;
 ReportRope[" Closing Walnut transaction ..."];
IF logStream#NIL THEN
  { len← logStream.GetLength[];
  logStream.Flush[];
  IF lastMsgReadFromLog >= lastMsgWrittenToLog THEN lastMsgReadFromLog← len;
  SetF[walnutInfoRelship, wExpectedLength, I2V[len]];
  SetF[walnutInfoRelship, wExpectedDBPos, I2V[lastMsgReadFromLog]];
  };

-- funny case of aborting during initialization, log not open
IF TransactionOf[$Walnut]#NIL THEN CloseTransaction[TransactionOf[$Walnut]];

 SetWalnutUpdatesPending[FALSE];
 Report[" ... done"];
END;

MarkWalnutTransaction: PUBLIC ENTRY PROC =
BEGIN ENABLE UNWIND => NULL;
 len: INT← logStream.GetLength[];

IF lastMsgReadFromLog >= lastMsgWrittenToLog THEN lastMsgReadFromLog← len;
 ReportRope[" Saving Walnut updates ..."];
 SetF[walnutInfoRelship, wExpectedLength, I2V[len]];
 SetF[walnutInfoRelship, wExpectedDBPos, I2V[lastMsgReadFromLog]];
 MarkTransaction[TransactionOf[$Walnut]];
 SetWalnutUpdatesPending[FALSE];
 Report[" ... done"];

END;

TiogaTextFromLog: PUBLIC ENTRY PROC[startPos, length: INT]
  RETURNS[contents: TiogaContents] =
BEGIN ENABLE UNWIND => NULL;
 contents←
  WalnutSendMail.TiogaTextFromStrm[logStream, startPos-1, length+1];  -- hack for now

IF contents # NIL THEN
  { IF contents.formatting=NIL THEN
   contents.contents← Rope.Substr[contents.contents, 1];
RETURN;
  };
 contents← NEW[ViewerTools.TiogaContentsRec];
 contents.contents←
  IO.PutFR["[message body lost!!! (%g,%g)]\n", IO.int[startPos], IO.int[length]];
END;

RopeFromLog
: PUBLIC ENTRY PROC[startPos, length: INT] RETURNS[ROPE] =
-- used by ArchiveMsgSet
BEGIN ENABLE UNWIND => NULL;
RETURN[RopeFromStream[logStream, startPos, length]];
END;

-- ********************************************************
-- operations that read the log

UpdateFromLog: PUBLIC ENTRY PROC[startPos: INT] RETURNS[success: BOOL] =
-- rebuild database
BEGIN ENABLE UNWIND => NULL;
 logWasntOpen: BOOL← logStream = NIL;
IF logWasntOpen THEN []← DoInitializeLog[];

IF success← UpdateFromStream[logStream, startPos] THEN
{ IF startPos = 0 THEN Report[" WalnutScavenge done"];
lastMsgReadFromLog← lastMsgWrittenToLog← logStream.GetLength[];
SetF[walnutInfoRelship, wExpectedLength, I2V[lastMsgWrittenToLog]];
SetF[walnutInfoRelship, wExpectedDBPos, I2V[lastMsgReadFromLog]]}
ELSE Report["\nThere were problems reading the log file."];

IF logWasntOpen THEN {logStream.Close[]; logStream← NIL};
END;

NextMsgRecFromLog: PUBLIC ENTRY PROC[startOfNextMessage: INT]
  RETURNS[endPos: INT, anymore: BOOL, msgRec: MsgRec] =
BEGIN ENABLE UNWIND => NULL;
startPos, entryLength, prefixLength: INT;
entryChar: CHAR;

 logStream.SetIndex[startOfNextMessage];
-- Read *start* from log
 [startPos, prefixLength, entryLength, entryChar]← FindStartOfEntry[logStream, TRUE];
IF entryLength <= 0 THEN RETURN[lastMsgReadFromLog← logStream.GetIndex[], FALSE, NIL];

-- ignore all log entries except those with entryChar = '? or SP
-- any other entries have been processed
SELECT entryChar FROM
  '?, ' => -- msg entry
  { msgRec← MsgRecFromStream[logStream, prefixLength, entryLength-prefixLength];
   IF entryChar # '? THEN msgRec.hasBeenRead← TRUE;
   lastMsgReadFromLog← startPos+entryLength;
  };
ENDCASE => NULL;
RETURN[lastMsgReadFromLog, TRUE, msgRec];
END;

ConstructMsgID: PUBLIC PROC[mr: MsgRec] RETURNS[ROPE] =
-- uses gv address of Cabernet as default
-- if date is Unintelligible => use Time.Current
BEGIN
 ts: GVBasics.Timestamp← [net: 3, host: 14, time: 0];
IF mr.gvSender = NIL THEN
  { IF mr.inMsgSender # NIL THEN mr.gvSender← mr.inMsgSender
ELSE IF mr.from = NIL THEN mr.gvSender← "UnknownSender"
ELSE mr.gvSender← mr.from};
IF mr.date = NIL THEN mr.date← IO.PutFR[NIL, IO.time[]];  -- current time
BEGIN
  ts.time← DateAndTime.Parse[mr.date ! DateAndTime.Unintelligible => GOTO badTime].dt;
TRUSTED {mr.dateCode← LOOPHOLE[ts.time, GreenwichMeanTime]};
EXITS
  badTime => TRUSTED {mr.dateCode← Time.Current[]};
END;

-- mr.gvID← Rope.Cat[mr.gvSender, " $ ", ~~GVBasics.RopeFromTimestamp[ts]];

 mr.gvID← Rope.Cat[mr.gvSender, " $ ", RopeFromTimestamp[ts] ];
RETURN[mr.gvID];
END;

ParseMsgID
: PUBLIC PROC[mr: MsgRec] =
-- parses gvSender out of mr.gvID, compute dateCode
BEGIN
 local: ROPE← mr.gvID;
 mr.gvSender← local.Substr[0, local.Find[" $ "]];
 mr.dateCode← DateAndTime.Parse[mr.date ! DateAndTime.Unintelligible => GOTO badTime].dt;
EXITS
 badTime => TRUSTED {mr.dateCode← Time.Current[]};
END;

RopeFromTimestamp: PUBLIC PROC[ts: GVBasics.Timestamp] RETURNS [ROPE] =
BEGIN OPEN IO;
RETURN[PutFR["%g#%g@%g",
 int[ts.net], int[ts.host], time[LOOPHOLE[ts.time, GreenwichMeanTime]] ]];
END;

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

DoInitializeLog
: INTERNAL PROC RETURNS [curLength: INT] =
BEGIN
 logStream← FileIO.Open[fileName: currentLogName,
  accessOptions: write,
  closeOptions: FileIO.commitAndReopenTransOnFlush+FileIO.finishTransOnClose];
RETURN[lastMsgWrittenToLog← logStream.GetLength[]]
END;

CopyTempLogToLog: INTERNAL PROC[tempLog: IO.Handle, logStreamStartPos: INT] =
-- copy tempLog to logStream
BEGIN
bytes: NAT;
logStream.SetIndex[logStreamStartPos]; tempLog.SetIndex[0];
DO
IF (bytes← tempLog.GetBlock[copyBuffer]) = 0 THEN EXIT;
logStream.PutBlock[copyBuffer];
ENDLOOP;
lastMsgReadFromLog← lastMsgWrittenToLog← logStream.GetIndex[];
logStream.SetLength[lastMsgReadFromLog]; -- set the log length to current length
logStream.Flush[];

tempLog.Close[]; -- we don't need this guy any more, we've copied him to log.

SetF[walnutInfoRelship, wCopyInProgress, B2V[FALSE]];
SetF[walnutInfoRelship, wStartExpungePos, I2V[lastMsgReadFromLog]];
END
;

END.