-- File: AltoFileOpsB.mesa
-- Last edited by Levin:  30-Apr-81 14:24:14

DIRECTORY
  AltoDefs USING [PageSize],
  AltoFile USING [
    AllocateDiskPage, ByteNumber, CFP, CloseDirectory, DeleteFP, DirHandle,
    DiskFull, FileAlreadyExists, FP, FreeDiskPage, IllegalFileName,
    InvalidFP, LDPtr, MapFileNameToFP, NewSN, NoSuchFile, OpenDirectory, PageNumber,
    VersionOption],
  AltoFileDefs USING [FA, TIME],
  AltoFilePrivate USING [
    AltoPageNumber, DoDiskRequest, FileHandle, FileObject, FindEOF, GetvDAForPage,
    infinityPage, initialRuns, InsertFile, LastPageBytes, minUsefulRuns,
    openSeal, PurgeFile, ReleaseFile, RunIndex, RunTable, TruncatevDATable, VdaRun],
  DiskIODefs USING [
    CompletionStatus, DiskError, DiskRequest, eofvDA, FID, fillInvDA,
    PageCount, RequestID, vDA, XferSpec],
  FileDefs USING [
    ComparePositions, defaultTime, FileTime, FSInstance, OpenOptions, Position],
  MiscDefs USING [Zero],
  Mopcodes USING [zEXCH],
  StringDefs USING [MesaToBcplString],
  VMDefs USING [CantOpen, Error],
  VMStorage USING [AllocatePage, FreePage, shortTerm];

AltoFileOpsB: MONITOR LOCKS file.LOCK USING file: AltoFilePrivate.FileHandle
  IMPORTS
    AltoFile, AltoFilePrivate, DiskIODefs, FileDefs, MiscDefs, StringDefs,
    VMDefs, VMStorage
  EXPORTS AltoFile, AltoFilePrivate, FileDefs =

  BEGIN OPEN AltoFile, AltoFilePrivate, DiskIODefs, FileDefs;


  -- Miscellaneous Declarations --

  unknownLengthFA: AltoFileDefs.FA = [da: eofvDA, page: 0, byte: 0];

  IllegalExtend: ERROR = CODE;
  IllegalTruncate: ERROR = CODE;
  InvalidFile: ERROR = CODE;


  -- Types Exported to FileDefs --

  FileObject: PUBLIC TYPE = AltoFilePrivate.FileObject;


  -- Operations (exported to AltoFilePrivate on behalf of FileDefs) --

  Open: PUBLIC PROCEDURE [
    instance: FileDefs.FSInstance, name: STRING,
    options: FileDefs.OpenOptions ← oldReadOnly]
    RETURNS [FileHandle] =
    BEGIN
    DoOpen: PROCEDURE [file: FileHandle, newlyOpened: BOOLEAN]
      RETURNS [worked: BOOLEAN] =
      {RETURN[OpenFromHandle[file, newlyOpened, options ~= oldReadOnly]]};

    fv: VersionOption =
      SELECT options FROM oldReadOnly, old => old, new => new, ENDCASE => oldOrNew;
    fp: FP ← MapFileNameToFP[name, fv
		! DiskFull => ERROR VMDefs.Error[resources];
		  NoSuchFile => ERROR VMDefs.CantOpen[notFound];
		  IllegalFileName => ERROR VMDefs.CantOpen[illegalFileName];
		  FileAlreadyExists => ERROR VMDefs.CantOpen[alreadyExists]];
    RETURN[InsertFile[fp, DoOpen ! DiskError => ERROR VMDefs.Error[io]]]
    END;

  Close, CloseFile: PUBLIC PROCEDURE [file: FileHandle] =
    BEGIN
    DoClose: PROCEDURE [file: FileHandle] =
      BEGIN OPEN VMStorage;
      UpdateLengthHint[file ! DiskError => ERROR VMDefs.Error[io]];
      shortTerm.FREE[@file.runTable];
      END;

    ValidateFile[file];
    ReleaseFile[file, DoClose];
    END;

  Abandon: PUBLIC PROCEDURE [file: FileHandle] =
    BEGIN
    DoAbandon: PROCEDURE [file: FileHandle] = {VMStorage.shortTerm.FREE[@file.runTable]};

    ValidateFile[file];
    ReleaseFile[file, DoAbandon];
    END;

  Destroy: PUBLIC PROCEDURE [file: FileHandle] =
    BEGIN
    DoDestroy: PROCEDURE [file: FileHandle] =
      BEGIN
      dirFP: FP;
      fileFP: FP ← FP[serial: file.fileID.serial, leaderDA: file.leadervDA];

      ThrowAwayFilePages: PROCEDURE =
	BEGIN
	request: DiskRequest;
	leader: LDPtr ← ReadLeaderPage[file, @request];
	dirFP ← FP[serial: leader.dirFP.serial, leaderDA: leader.dirFP.leaderDA];
	VMStorage.FreePage[leader];
	EnsureKnownVDAs[file, 0]; -- discover any unknown disk addresses
	FreeFileTail[file, 0];
	END;

      ThrowAwayFilePages[ ! DiskError => ERROR VMDefs.Error[io]];
      VMStorage.shortTerm.FREE[@file.runTable];
      BEGIN OPEN AltoFile;
      dir: DirHandle = OpenDirectory[dirFP ! InvalidFP => GO TO SkipDirectoryDelete];
      [] ← DeleteFP[dir, @fileFP];
      CloseDirectory[dir];
      EXITS SkipDirectoryDelete => NULL;
      END;
      END;

    ValidateFile[file];
    PurgeFile[file, DoDestroy];
    END;

  GetLength: PUBLIC ENTRY PROCEDURE [file: FileHandle] RETURNS [Position] =
    BEGIN
    page: AltoPageNumber;
    bytes: LastPageBytes;
    ValidateFile[file];
    IF ~file.lengthKnown THEN FindEOF[file ! UNWIND => NULL];
    page ← file.lastPage;
    bytes ← file.bytes;
    RETURN[MakeFileLength[page, bytes]]
    END;

  SetLength: PUBLIC PROCEDURE [file: FileHandle, length: Position] =
    BEGIN
    oldLength: Position;
    ValidateFile[file];
    oldLength ← GetLength[file];
    SELECT ComparePositions[oldLength, length] FROM
      less =>
	BEGIN
	buffer: POINTER = VMStorage.AllocatePage[];
	BEGIN
	ENABLE DiskError => {VMStorage.FreePage[buffer]; ERROR VMDefs.Error[io]};
	tempLength: Position ← [page: oldLength.page, byte: 0];
	IF oldLength.byte ~= 0 THEN -- read partial last page into buffer
	  BEGIN
	  xferSpec: ARRAY [0..1) OF XferSpec ←
	    [[buffer, GetvDAForPage[file, file.lastPage], 0]];
	  request: DiskRequest ←
	    [firstPage: file.lastPage, fileID:, firstPagevDA:, pagesToSkip: 0,
	      nonXferID:, proc:, xfers: DESCRIPTOR[@xferSpec, 1],
	      noRestore: FALSE, command: ReadD[]];
	  [] ← DoDiskRequest[@request, file];
	  END;
	-- not the most efficient algorithm...
	UNTIL tempLength.page = length.page DO
	  tempLength.page ← tempLength.page + 1;
	  Extend[file, tempLength, buffer];
	  ENDLOOP;
	IF length.byte ~= 0 THEN Extend[file, length, buffer];
	END;
	VMStorage.FreePage[buffer];
	END;
      equal => NULL;
      greater => Truncate[file, length];
      ENDCASE;
    END;

  Extend: PUBLIC ENTRY PROCEDURE [
    file: FileHandle, length: Position, buffer: POINTER] =
    BEGIN
    xferSpec: ARRAY [0..2) OF XferSpec;
    request: DiskRequest;
    lastvDA: vDA;
    oldLength: Position;
    needANewPage: BOOLEAN = (length.byte = 0);
    ValidateFile[file];
    IF ~file.lengthKnown THEN FindEOF[file ! UNWIND => NULL];
    oldLength ← MakeFileLength[file.lastPage, file.bytes];
    IF ComparePositions[length, oldLength] ~= greater OR
      ~(length.page = oldLength.page OR
	 (length.page = oldLength.page + 1 AND length.byte = 0)) THEN
      ERROR IllegalExtend;
    xferSpec[0] ← [buffer, (lastvDA ← GetvDAForPage[file, file.lastPage]), 0];
    request ← DiskRequest[
      firstPage: file.lastPage, fileID:, firstPagevDA:, pagesToSkip: 0,
      nonXferID:, proc:, xfers: DESCRIPTOR[@xferSpec, 1], noRestore: FALSE,
      command: WriteLD[
      next: eofvDA, prev: GetvDAForPage[file, file.lastPage - 1],
      lastByteCount: AltoByteCount[length.byte] - 1]];
    IF needANewPage THEN
      BEGIN -- the following hack writes the client's data in the shadow page.
      xferSpec[1] ← [buffer, AllocateDiskPage[lastvDA ! UNWIND => NULL], 0];
      request.xfers ← DESCRIPTOR[@xferSpec, 2];
      END;
    [] ← DoDiskRequest[@request, file];
    IF needANewPage THEN {file.lastPage ← file.lastPage + 1; file.bytes ← 0}
    ELSE file.bytes ← AltoByteCount[length.byte] - 1;
    file.lengthChanged ← TRUE;
    END;

  Truncate: PUBLIC ENTRY PROCEDURE [file: FileHandle, length: Position] =
    BEGIN
    newLastPage: AltoPageNumber = AltoFromFilePageNumber[length.page];
    pagesToFree: BOOLEAN;

    WriteNewLastPage: PROCEDURE =
      -- truncates the file at 'newLastPage', setting the appropriate byte count.
      BEGIN
      buffer: POINTER ← VMStorage.AllocatePage[];
      xferSpec: ARRAY [0..1) OF XferSpec ←
	[[buffer, GetvDAForPage[file, newLastPage], 0]];
      request: DiskRequest ←
	[firstPage: newLastPage, fileID: file.fileID, firstPagevDA:,
	  pagesToSkip: 0, nonXferID:, proc:, xfers: DESCRIPTOR[@xferSpec, 1],
	  noRestore: FALSE, command: ReadD[]];
      [] ← DoDiskRequest[@request, file ! DiskError =>
		{VMStorage.FreePage[buffer]; ERROR VMDefs.Error[io]}];
      request.command ← WriteLD[
	next: eofvDA, prev: GetvDAForPage[file, newLastPage - 1],
	lastByteCount: AltoByteCount[length.byte] - 1];
      [] ← DoDiskRequest[@request, file ! DiskError =>
		{VMStorage.FreePage[buffer]; ERROR VMDefs.Error[io]}];
      VMStorage.FreePage[buffer];
      END;

    ValidateFile[file];
    IF ~file.lengthKnown THEN FindEOF[file ! UNWIND => NULL];
    SELECT ComparePositions[length, MakeFileLength[file.lastPage, file.bytes]] FROM
      less =>
	BEGIN
	IF (pagesToFree ← file.lastPage > newLastPage) THEN
	  EnsureKnownVDAs[file, newLastPage - 1];
	WriteNewLastPage[];
	IF pagesToFree THEN
	  {FreeFileTail[file, newLastPage + 1]; TruncatevDATable[file, newLastPage]};
	file.lastPage ← newLastPage;
	file.bytes ← AltoByteCount[length.byte] - 1;
	file.lengthChanged ← TRUE;
	END;
      equal => NULL;
      greater => ERROR IllegalTruncate;
      ENDCASE;
    END;

  GetTimes: PUBLIC PROCEDURE [file: FileHandle]
    RETURNS [read, write, create: FileTime] = GetFileTimes;

  SetCreationTime: PUBLIC PROCEDURE [
    file: FileHandle, create: FileTime ← defaultTime] =
    BEGIN
    request: DiskRequest;
    leader: LDPtr = ReadLeaderPage[file, @request];
    IF create = defaultTime THEN create ← LeaderToMesaTime[DayTime[]];
    leader.created ← MesaToLeaderTime[create];
    RewriteLeaderPage[file, @request, leader];
    END;


  -- Procedures Exported to AltoFile --

  OpenFromFP: PUBLIC PROCEDURE [fp: FP, markWritten: BOOLEAN] RETURNS [FileHandle] =
    BEGIN

    DoOpen: PROCEDURE [file: FileHandle, newlyOpened: BOOLEAN]
      RETURNS [worked: BOOLEAN] =
      {RETURN[OpenFromHandle[file, newlyOpened, markWritten]]};

    RETURN[InsertFile[fp, DoOpen ! DiskError => ERROR VMDefs.Error[io]]]
    END;

  CreateFile: PUBLIC PROCEDURE [
    name: STRING, directory: FP, leadervDA: vDA ← fillInvDA] RETURNS [FP] =
    BEGIN
    leader: LDPtr;
    fp: FP;
    vDAtoTry: vDA ← IF leadervDA = fillInvDA THEN vDA[0] ELSE vDA[leadervDA - 1];
    lastPagevDA: vDA;
    xferSpec: ARRAY [0..2) OF XferSpec;
    request: DiskRequest;
    xferSpec[0].diskAddress ← vDAtoTry ← AllocateDiskPage[vDAtoTry];
    xferSpec[1].diskAddress ← lastPagevDA ← AllocateDiskPage[vDAtoTry];
    xferSpec[0].buffer ← xferSpec[1].buffer ← leader ← VMStorage.AllocatePage[];
    fp ← FP[NewSN[], vDAtoTry];
    MiscDefs.Zero[leader, AltoDefs.PageSize];
    leader.created ← DayTime[];
    StringDefs.MesaToBcplString[name, LOOPHOLE[@leader.name]];
    leader.propBegin ← @leader.props[0] - leader;
    leader.propLength ← LENGTH[leader.props];
    leader.dirFP ← CFP[directory.serial, 1, 0, directory.leaderDA];
    leader.eofFA ← AltoFileDefs.FA[da: lastPagevDA, page: 1, byte: 0];
    request ← DiskRequest[
      firstPage: 0, fileID: FID[1, fp.serial], firstPagevDA:, pagesToSkip: 0,
      nonXferID:, proc:, xfers: DESCRIPTOR[@xferSpec, 2], noRestore: FALSE,
      command: WriteLD[next: eofvDA, prev: eofvDA, lastByteCount: 0]];
    [] ← DoDiskRequest[@request ! DiskError => VMStorage.FreePage[leader]];
    VMStorage.FreePage[leader];
    RETURN[fp]
    END;

  GetFileTimes: PUBLIC PROCEDURE [file: FileHandle]
    RETURNS [read, write, create: FileTime] =
    BEGIN
    request: DiskRequest;
    leader: LDPtr = ReadLeaderPage[file, @request];
    read ← LeaderToMesaTime[leader.read];
    write ← LeaderToMesaTime[leader.written];
    create ← LeaderToMesaTime[leader.created];
    VMStorage.FreePage[leader];
    END;

  SetFileTimes: PUBLIC PROCEDURE [file: FileHandle,
    read, write, create: FileTime ← defaultTime] =
    BEGIN
    now: FileTime = LeaderToMesaTime[DayTime[]];
    request: DiskRequest;
    leader: LDPtr = ReadLeaderPage[file, @request];
    IF read = defaultTime THEN read ← now;
    IF write = defaultTime THEN write ← now;
    IF create = defaultTime THEN create ← now;
    leader.read ← MesaToLeaderTime[read];
    leader.written ← MesaToLeaderTime[write];
    leader.created ← MesaToLeaderTime[create];
    RewriteLeaderPage[file, @request, leader];
    END;

  ReadLeaderPage: PUBLIC PROCEDURE [file: FileHandle, request: POINTER TO DiskRequest]
    RETURNS [leader: LDPtr] =
    -- reads the leader page of 'file' into core, returning a pointer to it and leaving
    -- request↑ suitable for calling RewriteLeaderPage.
    BEGIN
    xferSpec: ARRAY [0..1) OF XferSpec;
    leader ← VMStorage.AllocatePage[];
    xferSpec[0] ← [leader, file.leadervDA, 0];
    request↑ ←
      [firstPage: 0, fileID:, firstPagevDA:, pagesToSkip: 0, nonXferID:, proc:,
	xfers: DESCRIPTOR[@xferSpec, 1], noRestore: FALSE, command: ReadD[]];
    [] ← DoDiskRequest[request, file ! DiskError => VMStorage.FreePage[leader]];
    END;

  RewriteLeaderPage: PUBLIC PROCEDURE [
    file: FileHandle, request: POINTER TO DiskRequest, leader: LDPtr] =
    -- writes the leader page 'leader' onto 'file'.  The storage associated with
    -- 'leader' is then released.
    BEGIN
    xferSpec: ARRAY [0..1) OF XferSpec ← [[leader, file.leadervDA, 0]];
    request.xfers ← DESCRIPTOR[@xferSpec, 1];
    request.command ← WriteD[];
    [] ← DoDiskRequest[request, file ! DiskError => VMStorage.FreePage[leader]];
    VMStorage.FreePage[leader];
    END;



  -- Internal Procedures --

  -- Open --

  OpenFromHandle: PROCEDURE [file: FileHandle, checkLength, markWritten: BOOLEAN]
    RETURNS [worked: BOOLEAN] =
    BEGIN
    leader: LDPtr = VMStorage.AllocatePage[];
    request: DiskRequest;
    xferSpec: ARRAY [0..1) OF XferSpec ← [[leader, file.leadervDA, 0]];
    status: CompletionStatus;
    worked ← FALSE;
    request ←
      [firstPage: 0, fileID:, firstPagevDA:, pagesToSkip: 0, nonXferID:, proc:,
      xfers: DESCRIPTOR[@xferSpec, 1], noRestore: FALSE, command: ReadD[]];
    file.runTable ← VMStorage.shortTerm.NEW[RunTable[initialRuns]];
    file.nRuns ← minUsefulRuns;
    file.runTable[0] ← VdaRun[page: 0, vda: file.leadervDA];
    file.runTable[1] ← VdaRun[page: 1, vda: fillInvDA];
    file.runTable[2] ← VdaRun[page: infinityPage, vda: eofvDA];
    [status, , ] ← DoDiskRequest[@request, file, FALSE];   -- disk error => InvalidFP
    IF status = ok THEN
      BEGIN
      leader.read ← DayTime[];
      IF markWritten THEN leader.written ← leader.created ← leader.read;
      request.command ← WriteD[];
      [] ← DoDiskRequest[@request, file ! DiskError => VMStorage.FreePage[leader]];
      IF checkLength THEN  -- validate length hint
	BEGIN
	eofFA: AltoFileDefs.FA = leader.eofFA;
	file.lengthKnown ← FALSE;
	IF eofFA.da ~= unknownLengthFA.da THEN
	  BEGIN -- length hint is present - validate it.
	  next: vDA;
	  bytes: LastPageBytes;
	  request.firstPage ← eofFA.page;
	  xferSpec[0].diskAddress ← eofFA.da;
	  request.command ← ReadD[];
	  request.noRestore ← TRUE;  -- anticipate possible check error
	  [status, next, bytes] ← DoDiskRequest[@request, file , FALSE];
	  IF status = ok AND next = eofvDA THEN
	    BEGIN
	    file.lastPage ← request.firstPage;
	    file.lengthChanged ← (file.bytes ← bytes) ~= eofFA.byte;
	    file.lengthKnown ← TRUE;
	    END;
	  END;
	END;
      worked ← TRUE;
      END;
    VMStorage.FreePage[leader];
    END;

  -- Truncation Utilities --

  EnsureKnownVDAs: PROCEDURE [file: FileHandle, tail: AltoPageNumber] =
    -- ensures that the vdas for all (Alto) pages >= 'tail' are known.
    BEGIN
    table: POINTER TO RunTable = file.runTable;
    IF file.lengthKnown THEN
      FOR i: RunIndex DECREASING IN [0..file.nRuns - 3] DO
	IF table[i].vda = fillInvDA THEN EXIT;
	IF table[i].page <= tail THEN RETURN;
	ENDLOOP;
    TruncatevDATable[file, tail];
    FindEOF[file];
    END;

  FreeFileTail: PROCEDURE [file: FileHandle, tail: AltoPageNumber] =
    -- releases all disk pages of 'file' beginning with 'tail'.  It is assumed that
    -- EnsureKnownVDAs has previously been called to fill the vda table appropriately.
    BEGIN
    FOR page: AltoPageNumber IN [tail..file.lastPage] DO
      FreeDiskPage[GetvDAForPage[file, page] ! DiskError => CONTINUE]; ENDLOOP;
    END;

  UpdateLengthHint: PROCEDURE [file: FileHandle] =
    -- rewrites the length hint into the leader page of the specified file.  Note:  it
    -- is assumed that appropriate mutual exclusion on the file object has been
    -- performed by the caller.
    BEGIN
    leader: LDPtr;
    request: DiskRequest;
    lastPagevDA: vDA;
    IF ~(file.lengthKnown AND file.lengthChanged) THEN RETURN;
    leader ← ReadLeaderPage[file, @request];
    lastPagevDA ← GetvDAForPage[file, file.lastPage];
    leader.eofFA ←
      IF lastPagevDA = fillInvDA THEN unknownLengthFA
      ELSE AltoFileDefs.FA[da: lastPagevDA, page: file.lastPage, byte: file.bytes];
    RewriteLeaderPage[file, @request, leader];
    END;

  -- Miscellaneous Procedures --

  ValidateFile: PROCEDURE [file: FileHandle] =
    {IF file.seal ~= openSeal THEN ERROR InvalidFile};

  AltoFromFilePageNumber: PROCEDURE [fp: PageNumber] RETURNS [AltoPageNumber] = INLINE
    {RETURN[fp + 1]};

  FileFromAltoPageNumber: PROCEDURE [ap: AltoPageNumber] RETURNS [PageNumber] = INLINE
    {RETURN[ap - 1]};

  AltoByteCount: PROCEDURE [fb: ByteNumber] RETURNS [LastPageBytes] = INLINE
    -- ignore possibility that fb = LAST[ByteNumber]
    {RETURN[fb + 1]};

  FileByteNumber: PROCEDURE [ab: LastPageBytes] RETURNS [ByteNumber] = INLINE
    {RETURN[ab - 1]};

  MakeFileLength: PROCEDURE [page: AltoPageNumber, byte: LastPageBytes]
    RETURNS [Position] = INLINE
    {RETURN[[FileFromAltoPageNumber[page], FileByteNumber[byte] + 1]]};

  DayTime: PROCEDURE RETURNS [AltoFileDefs.TIME] = INLINE
    BEGIN
    SecondsClock: POINTER TO AltoFileDefs.TIME = LOOPHOLE[572B];
    RETURN[SecondsClock↑]
    END;

  LeaderToMesaTime: PROCEDURE [AltoFileDefs.TIME] RETURNS [FileTime] =
    MACHINE CODE BEGIN Mopcodes.zEXCH END;

  MesaToLeaderTime: PROCEDURE [FileTime] RETURNS [AltoFileDefs.TIME] =
    MACHINE CODE BEGIN Mopcodes.zEXCH END;

  END.