/* CLASS: VirtFileObject. 
   This Virtual File Object class is a specialized VirtDataObject to hold
   a text file data.  It implements the VirtDataObject protocol for files
   using fixed-size chunks of the file-size.

   REQUIRE: VIRTDATA.CLS, CHUNKMAN.CLS
*/!!

inherit(VirtDataObject, #VirtFileObject, #(aFile  /* a TextFile */
), 2, nil)!!

now(class(VirtFileObject))!!

/* 06/20/92 - PUBLIC 
  Open a new Virtual File Object representing the specified file of
  the moduleInfo.
  Ex: 
    open(VirtFileObject, newModuleInfo, addrSize).
*/
Def open(self, aModuleInfo, ignoreArg | aVirtualFileObject)
{ 
  aVirtualFileObject := new(self:ancestor) ;
  ^init(aVirtualFileObject, aModuleInfo);
}!!

now(VirtFileObject)!!

/* 06/20/92 - PUBLIC 
   Return number of lines in chunk.
*/
Def getChunkSize(self, chunkNumber)
{ 
  ^linesPerChunk;
}
!!

/* 06/20/92 - PUBLIC 
   Calculate starting line number of chunk.
   The 1st chunks's line numbers range from 1 to linesPerChunk, and so on...
*/
Def getChunkStartLine(self, chunkNumber)
{ 
  ^(((chunkNumber - 1) * linesPerChunk) + 1);
}
!!

/* 06/20/92 - PRIVATE 
   Build a chunk from a known file location. 
   Return: #(lineNumber, TextCollection).
*/
Def buildKnownChunk(self, filePos, chunkNumber | textCol )
{  
  /* Seek to the proper file position */
  moveTo(aFile, filePos);
  checkError(aFile);
  
  /* Get the text from file */
  if (textCol := getFileText(self, chunkNumber)) then
    /* return the chunk */
    ^tuple(firstLineOfChunk(self, chunkNumber), textCol);
  endif;
  ^nil;  
}
!!

/* 06/20/92 - PRIVATE 
   Get a non-memoized chunk. Memoize chunks for later (in rememberedLocations).
   If find EOF, set totalLinesExact. If beyond EOF, return nil.
*/
Def buildRandomChunk(self, chunkWanted | chunkNum, filePointer, textCol, line)
{ 
  /* Seek the filePointer as far as we know how */
  if chunkNum := highestChunkSeen then
    if not(filePointer := at(rememberedLocations, chunkNum)) then
      ^nil; /* ERROR - bail out */
    endif ;
    moveTo(aFile, filePointer);
  else
    /* Begin with chunk #1 and filePointer = 0 - Move to location */
    chunkNum := 1;
    moveTo(aFile, 0L);
    add(rememberedLocations, chunkNum, position(aFile));
  endif;
  
  /* Find starting line, memorizing along the way */
  loop
  while (chunkNum < chunkWanted) and not(atEnd(aFile))
  begin
    line := 0; 
    if checkAbort(self) then
      ^nil;
    endif;
    checkError(aFile);
    loop  /* read a chunk; throw lines away */
    while (line < linesPerChunk) cand not(atEnd(aFile))
    begin 
      readLine(aFile); 
      line := line + 1;
    endLoop;
    /* Check termination conditions */
    if atEnd(aFile) then
      if not(totalLinesExact) then
        setTotalLinesExact(self, (line-1 + firstLineOfChunk(self,chunkNum)));
      endif ;
      ^nil ; /* ran out of file */
    else
      chunkNum := chunkNum + 1 ; /* must read through next unwanted chunk */
      add(rememberedLocations, chunkNum, position(aFile)); /* remember */
      highestChunkSeen := chunkNum;
    endif ;
  endLoop;
 
  /* Get the text from file */
  if not(textCol := getFileText(self, chunkWanted)) then
    ^nil;
  endif;  
  line := size(textCol);
  
  if atEnd(aFile) and not(totalLinesExact) then
    /* SetTotalLineExact by callBack to browser to set numLines */
    setTotalLinesExact(self, (line-1 + firstLineOfChunk(self, chunkNum)));
  endif ;
 
  /* return the chunk */
  ^tuple( firstLineOfChunk(self, chunkWanted), textCol);
}
!!

/* 06/23/92 - PUBLIC
   Given a (virtual) line number, return the chunk number which contains it.
*/
Def chunkNumberFromLineNumber(self, lineNumber)
{ 
  /* Chunks have the same number of lines, so we can calculate which 
  ** chunk this should be in.  Note that the 1st chunk number is 1. 
  */

  ^(1 + (lineNumber / linesPerChunk))  /* NOTES: integer division (div) */
}!!

/* 06/20/92 - PUBLIC */
Def close(self)
{
  closed? := #true;
  ^close(aFile);
}
!!

/* 06/20/92 - PRIVATE 
   Calculate starting line number of chunk.
   The 1st chunks's line numbers range from 1 to linesPerChunk, and so on...
*/
Def firstLineOfChunk(self, chunkNumber)
{ 
  ^(((chunkNumber - 1) * linesPerChunk) + 1);
}
!!

/* 06/20/92 - PUBLIC (to it descendant)
   If aFile has been reOpen()'ed, cached info may be stale.  The user
   can call this function to flush cached info, or not call it to keep
   cached info.
*/
Def flushCachedMemory(self)
{
  highestChunkSeen := totalLinesExact  := nil;
  if rememberedLocations then
    clear(rememberedLocations);
  else  
    /* NOTES: rememberedLocations = [chunk#->filePos,...] */
    rememberedLocations := new(Dictionary, 2); 
  endif; 
}
!!

/* 06/22/92 - PUBLIC
   Given a chunkNumber >= 1, return a tuple: 
   #(startingLineNum, textCollection).
   If chunk number is beyond the actual data, return nil.
     NOTES: When we reach EOF (End Of File), set totalLinesExact.
*/
Def getChunk(self, chunkNumber | filePos, chunk)
{ 
  /* Check if we already know the file position */
  if checkAbort(self) then
    ^nil;
  endif;
  if filePos := at(rememberedLocations, chunkNumber ) then
    chunk := buildKnownChunk(self, filePos, chunkNumber);
  else
    chunk := buildRandomChunk(self, chunkNumber);
  endif;
  ^chunk;
}!!

/* 7/26/1992 16:28 - PRIVATE
  Read text string from the current file position.
    Return a textCollection.
*/
Def getFileText(self, chunkWanted | textCol, line, str, sourceLine)
{ 
  /* Allocate a new TextCollection */
  textCol := new(TextCollection, linesPerChunk);
  line := 0; 
  sourceLine := firstLineOfChunk(self, chunkWanted);
  if checkError(aFile) <> 0 then
    ^nil;
  endif;  
  loop
  while not(atEnd(aFile)) cand (line < linesPerChunk)
  begin 
    str  := readLine(aFile);
    /* Format string with line Number and add string to the Text Collection */
    add(textCol, format("[%6.6u] ", sourceLine) + str);    
    line := line + 1; 
    sourceLine := sourceLine + 1;
  endLoop;
  ^textCol;
}
!!

/* 06/20/92 - PRIVATE 
  initialization; openFile(VirtFileObject, fileName) does most of the work.
*/
Def init(self, aModuleInfo)
{ 
  init(self:ancestor);
  /* Child process first then call parent */
  ^openFile(self, aModuleInfo);
}!!

/* 06/20/92 - PUBLIC
   Return the number if text lines in self. Guess if don't know.
*/
Def numLinesIn(self)
{ 
  /* if we know how many lines exist, say so--else guess */
  if totalLinesExact then 
    ^totalLinesExact ;
  else 
    ^totalLinesGuess ;
  endif ;
}
!!

/* 11/30/1992 16:54 - PUBLIC
  Open a virtualFileObject.
*/
Def openFile(self, moduleInfo)
{ 
  if sourceHighestChunk(moduleInfo) then
    ^openFileWithInfo(self, moduleInfo);
  endif;    
  ^openFileFirstTime(self, moduleInfo);  
}
!!

/* 06/20/92 - PRIVATE 
   Open a file for a VirtFileObject - Return the new object or nil if error.
    NOTES: require TheSourcePresenter.
*/
Def openFileFirstTime(self, aModuleInfo | aFileName, fileLength, 
  error, line, errMsg) { 
  if not(aFileName := filePath(aModuleInfo)) then
    ^nil;
  endif;
  /* process native or unix file */
  if unixFile?(aFileName)
    if not(openUnixFileFirstTime(self, aFileName)) then
      ^nil;
    endif;  
  else
    if not(openNativeFileFirstTime(self, aFileName)) then
      ^nil;
    endif;
  endif;
  /* NOTES: rememberedLocations = [chunk#->filePos,...] */
  rememberedLocations := new( Dictionary, 2 ) ;   
  /* 
  ** Guess how many lines the file has (numberOfChars div 10 ), 
  ** or do the right thing by read it in to get the exact number 
  */
  fileLength := max(0, length(aFile));
  if (fileLength = 0) then
    displayFormattedError(ErrorTextLibClass$Inst, 
       ER_CANT_GET_SIZE, FORCE_POPUP, nil, nil, nil);
    totalLinesGuess := totalLinesExact := line;
  else
    /* If file size is less than 1K read it and get the exact number
    ** of line.
    */
    if fileLength < 1024 then
      moveTo(aFile, 0L); /* seek to beginning of file */
      line := 0;
      loop
      while not(atEnd(aFile))
      begin 
        skipLine(aFile);
        checkError(aFile);
        line := line + 1; 
      endLoop;
      totalLinesGuess := totalLinesExact := line;
    else  
      totalLinesGuess := (fileLength / 10);
    endif;  
  endif;
  
  /* Save the file path of opened module */
  setModulePath(self, aFileName);
}
!!

/* 12/1/1992 14:42 - PRIVATE
  Opened a virtFileObject with saved information from the moduleInfo object.
  This will speed up file I/O for the source browser.
    NOTES: require TheSourcePresenter.
*/
Def openFileWithInfo(self, moduleInfo | fPath)
{ 
  if not(fPath := filePath(moduleInfo)) then
    ^nil;
  endif;
  /* separate processing for unix file names */
  if unixFile?(fPath)  then
    if not(openUnixFileFirstTime(self, fPath)) then
      ^nil;
    endif;    
  else
    if not(openNativeFileFirstTime(self, fPath)) then
      ^nil;
    endif;
  endif;  
  /* NOTES: rememberedLocations = [chunk#->filePos,...] */
  rememberedLocations := sourceLocations(moduleInfo);  
  totalLinesGuess  := totalSourceLinesGuess(moduleInfo);
  totalLinesExact  := totalSourceLinesExact(moduleInfo); 
  highestChunkSeen := sourceHighestChunk(moduleInfo);
  setModulePath(self, fPath);
}
!!

/* 06/20/92 - PRIVATE 
   Do the native file processing for openFileFirstTime.
   Returns nil for error, otherwise returns GOOD.
*/
Def openNativeFileFirstTime(self, aFileName)
{ 
  if not(aFile := new(TextFile)) then
    displayFormattedError(ErrorTextLibClass$Inst, 
       ER_CANT_OPEN_SOURCE_FILE, FORCE_POPUP, ": " + aFileName, nil, nil);
    ^nil 
  endif;  
  if TheSourcePresenter then
    setDelimiter(aFile, sourceDelimiter(TheSourcePresenter));
  else  
    setDelimiter(aFile, CR_LF);
  endif;  
  setName(aFile, aFileName);
  /* detect error on open file */
  if not(open(aFile, 0)) cor (getError(aFile) <> 0) then 
    displayFormattedError(ErrorTextLibClass$Inst, 
       ER_CANT_OPEN_SOURCE_FILE, FORCE_POPUP, ": " + aFileName, nil, nil);
    ^nil 
  endif; 
  /* if delimiter setting is changed, reset the setting for the Source window */
  if (TheSourcePresenter cand 
      (delimiter(aFile) <> sourceDelimiter(TheSourcePresenter))) then
    resetSourceDelimiter(TheSourcePresenter, delimiter(aFile)); 
  endif;  
  ^GOOD;
}
!!

/* 06/20/92 - PRIVATE 
   Do the UNIX file processing for openFileFirstTime.
   Returns nil for error, otherwise return GOOD
   (alternate paths are not supported by the UNIX open capability).
*/
Def openUnixFileFirstTime(self, aFileName)
{
  if not(aFile := unixOpenFile(CLIULibraryClass$Inst, aFileName)) then
    /* display error */
    beep();   
    displayFormattedError(ErrorTextLibClass$Inst, 
       ER_CANT_OPEN_UNIX_FILE, FORCE_POPUP, ": " + aFileName, nil, nil);
    ^nil;
  endif;
  
  /* successful */
  ^GOOD;
}
!!

/* 06/20/92 - PUBLIC 
   Reopen a previously opened file.
*/
Def reOpen(self, ignoreArg | success)
{
  if closed? then
    /* handle unix files differently */
    if unixFile?(aFile.fileName)
      aFile := unixOpenFile(CLIULibraryClass$Inst, aFile.fileName);
      success := #true;
    else
      open(aFile, 0);  /* Mode = 0 for read-only */
      /* NOTES: if error, aFile's handle will be nil */
      success := (checkError(aFile) = 0);
    endif;
    
    /* process error conditions */
    if success
      closed? := nil;
    else
      displayFormattedError(ErrorTextLibClass$Inst, 
         ER_CANT_REOPEN_FILE, FORCE_POPUP, nil, nil, nil);
      ^nil
    endif;    
  endif;
    
  ^self;
}
!!

/* 06/20/92 - PUBLIC 
  Set resizeCallBack of the dataObject.
*/
Def setResizeCallback(self, aResizeBlock)
{ 
  resizeCallback := aResizeBlock ;
}
!!

/* 06/20/92 - PRIVATE 
  Set the totalLineExact to numLines and resize it.
*/
Def setTotalLinesExact(self, numLines)
{ 
  totalLinesExact := numLines;
  setModuleTotalLines(self, numLines);  
  if resizeCallback then
    eval( resizeCallback, numLines );
  endif;
}
!!
