/* Buffered Portable C-Library interface I/O. */!!

inherit(File, #TextFile,
#(buffer,     /* The buffer */
bufDirty,   /* The number of dirty bytes in the buffer */
bufIdx,     /* The current index in the buffer */
bufLen,     /* The buffer length */
bufPos,     /* The current buffer position in the file */
position,   /* The current position in the file */
delimiter,  /* The file record delimiter */
readable,   /* Open for reading? */
writeable   /* Open for writing? */), 2, nil)!!

setClassVars(TextFile, #($BufSize /* Size of buffer for a new TextFile */))!!

now(class(TextFile))!!

/* Set the class variable $BufSize to the given value. */
Def setBufSize(self, val)
{ ^$BufSize := val;
}!!

now(TextFile)!!

/* Read a line delimited by the string in the
  instance variable delimiter. Returns a null
  String if there is an error or the read
  starts at the end of the file. */
Def skipLine(self | str, len, sPos, ePos, part, del0)
{ if not(readable) cor (position < 0) cor atEnd(self)
  then ^0;
  endif;
  if ePos := find(buffer, delimiter, sPos := bufIdx)
  then position := bufPos +
      asLong(bufIdx := ePos + size(delimiter));
    ^1;
  endif;
  len := length(self);
  loop
    if len - bufPos < asLong(bufLen)  /* EOF case */
    then 
      bufIdx := asInt((position := len) - bufPos);
      ^1;
    endif;
    if part := indexOf(buffer, del0 := delimiter[0], max(sPos, bufLen - size(delimiter)))
    then ePos := part;
      part := nil;
      loop
        if find(buffer, copyFrom(delimiter, 0, bufLen - ePos), ePos)
        then part := ePos := bufLen - ePos;
        endif;
      while not(part) cand ePos := indexOf(buffer, del0, ePos + 1)
      begin
      endLoop;
    endif;
    writeBuffer(self);
    bufPos := bufPos + bufLen;
    readBuffer(self);
    if part
    then
      if find(delimiter, copyFrom(buffer, 0, size(delimiter) - ePos), ePos)
      then position := bufPos + asLong(bufIdx := size(delimiter) - ePos);
        ^1;
      endif;
    endif;
  while not(ePos := find(buffer, delimiter, sPos := 0))
  begin
  endLoop;
  position := bufPos + asLong(bufIdx := ePos + size(delimiter));
  ^1;
}!!

/* Skip lines delimited by the string in the
  instance variable delimiter. Returns the number
  of lines skipped (could be less than linesToSkip
  at the end of the file). */
Def skipLines(self linesToSkip | linesSkipped, skip)
{
  linesSkipped := 0;
  loop while linesSkipped < linesToSkip begin
    if (skip := skipLine(self)) = 0
      ^linesSkipped;
    endif;
    linesSkipped := linesSkipped + skip;
  endLoop;
  ^linesSkipped;
 }!!

/* Returns nil if place indexes outside the file, otherwise
  the file position is changed to place and the character
  at that file position is returned. */
Def at(self, place)
{ if handle cand readable cand (place >= 0)
  then lseek(self, place, 0);
    if place < length(self)
    then ^at(buffer, bufIdx);
    endif;
  endif;
  ^nil;
}!!

/* Returns false if the current file position is
  less than the file length, true otherwise. */
Def atEnd(self)
{ ^(position >= length(self));
}!!

/* Returns the number of bytes read from the file into
  buf.  nBytes is the number requested.  nBytes is
  ASSUMED to be less than or equal to the size of the
  buffer object.  See File:bRead for more information. */
Def bRead(self, buf, nBytes | bytes)
{ if readable cand positionAsFile(self)
  then bytes := bRead(self:ancestor, buf, nBytes);
    setPos(self, position + bytes);
    ^bytes;
  endif;
  ^0;
}!!

/* Return the current buffer size (The bufLen instance
  variable). */
Def bufferSize(self)
{ ^bufLen;
}!!

/* Returns the number of bytes written to the file
  from buf.  nBytes is the number requested.  nBytes
  is ASSUMED to be less than or equal to the size of
  the buffer object.  See File:bWrite for more
  information. */
Def bWrite(self, buf, nBytes | bytes)
{ if writeable cand positionAsFile(self)
  then bytes := bWrite(self:ancestor, buf, nBytes);
    setPos(self, position + bytes);
    ^bytes;
  endif;
  ^0;
}!!

/* Flushes the file buffer and returns the result of
  sending a close(self:ancestor) message. */
Def close(self)
{ if handle
  then writeBuffer(self);
  endif;
  readable := writeable := nil;
  ^close(self:ancestor);
}!!

/* Copies the specified number of bytes from the receiver to
  the destination file.  Both files must be open.  Returns
  the number of bytes copied. This assumes that "other" is
  properly positioned. */
Def copy(self, other, nBytes | result)
{ if readable cand positionAsFile(self)
  then result := copy(self:ancestor, other, nBytes);
    setPos(self, position(self:ancestor));
    ^result;
  endif;
  ^0;
}!!

/* Return a string of characters from the file starting
  at index in the file until the characters match the
  "delim" parameter. "delim" may be a Char or a String. */
Def copyUpTo(self, delim, index | oldDelim, str)
{ lseek(self, index, 0);
  oldDelim := delimiter(self);
  setDelimiter(self, asString(delim));
  str := readLine(self);
  setDelimiter(self, oldDelim);
  ^str;
}!!

/* Sends a create(self:ancestor) message. If the
  create(self:ancestor) is successful, the file
  buffer is initialized and self is returned,
  otherwise nil is returned. */
Def create(self)
{ if not(create(self:ancestor))
  then ^readable := writeable := nil;
  endif;
  initBuffer(self, 2);
}!!

/* Return the current file delimiter string (The
  delimiter instance variable). */
Def delimiter(self)
{ ^delimiter;
}!!

/* PRIVATE method.  Initializes the instance variables
  delimiter and bufLen after sending an
  init(self:ancestor) message. All other instance
  variables are initialized in File:open or File:create.
  This method is used by File:new. */
Def init(self)
{ if not(handle)
  then init(self:ancestor);
    bufDirty := bufIdx := 0;
    position := bufPos := 0L;
    readable := writeable := nil;
    delimiter := CR_LF;
    bufLen := $BufSize;
  endif;
}!!

/* PRIVATE method.  Initializes the file buffer
  mechanism.  Returns self. */
Def initBuffer(self, mode)
{ bufDirty := bufIdx := 0;
  position := bufPos := 0L;
  buffer := new(String, bufLen);
  readBuffer(self);
  readable := (mode <> 1);
  writeable := (mode <> 0);
}!!

/* Returns the file length. */
Def length(self)
{ ^max(bufPos + bufDirty, length(self:ancestor));
}!!

/* If mode is 0, 1, or 2, the file position is set to
  offset bytes from the beginning of the file, the
  current file position, or the end of the file respectively.
  Any other mode is illegal. Note that offset can be a negative
  number. Returns the new file position. NOTE: In the case of
  an error the position is not changed before it is returned. */
Def lseek(self, offset, mode | newPos)
{ if handle
  then
    if mode > 2 cor mode < 0
    then errorCode := 1;
      ^0L;
    endif;
    if (position := asLong(offset +
      (((mode = 0) cand 0) cor
      ((mode = 1) cand position) cor
      ((mode = 2) cand length(self))))) < 0
    then bufIdx := bufPos := -1;
    else
      if (newPos := (position / bufLen) * bufLen) <> bufPos  /* Not in the current buffer */
      then writeBuffer(self);
        bufPos := newPos;
        readBuffer(self);
      endif;
      bufIdx := asInt(position - bufPos);
    endif;
  endif;
  ^position;
}!!

/* Sends an open(self:ancestor) message. If
  open(self:ancestor) is successful, the file
  buffer is initialized and self is returned,
  otherwise nil is returned. */
Def open(self, mode)
{ if not(open(self:ancestor, mode))
  then ^readable := writeable := nil;
  endif;
  initBuffer(self, mode);
}!!

/* PRIVATE method.  Return nil if the file state
  cannot be made to match what it would be for a File
  object, true (self) otherwise.  The buffer is
  flushed and the true file position is made to match
  the instance variable. */
Def positionAsFile(self)
{ if not(handle) cor position < 0
  then ^nil;
  endif;
  writeBuffer(self);
  lseek(self:ancestor, position, 0);
}!!

/* Returns nil if place indexes outside the file, otherwise
  the file position is changed to place and ch is put into
  the file at that file position and true is returned. */
Def put(self, ch, place)
{ if handle cand writeable cand (position >= 0) cand (place >= 0)
  then lseek(self, place, 0);
    put(buffer, ch, bufIdx);
    bufDirty := max(bufDirty, bufIdx + 1);
    ^ch;
  endif;
  ^nil;
}!!

/* Decrements the file position by one and puts ch into the
  file at the new file position.  Returns self. */
Def putBackChar(self, ch)
{ ^put(self, ch, position - 1L);
}!!

/* Returns a string read from the file.  The length of
  the string will be equal to bytes unless the end of
  the file is encountered. */
Def read(self, bytes | buf)
{ if readable cand positionAsFile(self)
  then buf := read(self:ancestor, bytes);
    setPos(self, position + size(buf));
    ^buf;
  endif;
  ^"";
}!!

/* PRIVATE method.  Reads a buffer at the current file
  position.  The buffer is padded with nulls to be
  equal in length to the value in the ivar bufLen.
  Returns self.  Assumes that the file is readable. */
Def readBuffer(self)
{ lseek(self:ancestor, bufPos, 0);
  bRead(self:ancestor, fill(buffer, asChar(0)), bufLen);
}!!

/* Nil is returned if at End-Of-File or there was an error,
  otherwise the character at the current file position is
  returned and the file position is incremented by one. */
Def readChar(self | ret)
{ if ret := at(self, position)
  then skipOne(self);
  endif;
  ^ret;
}!!

/* Read a line delimited by the string in the
  instance variable delimiter. Returns a null
  String if there is an error or the read
  starts at the end of the file. */
Def readLine(self | str, len, sPos, ePos, part, del0)
{ if not(readable) cor (position < 0) cor atEnd(self)
  then ^"";
  endif;
  if ePos := find(buffer, delimiter, sPos := bufIdx)
  then position := bufPos +
      asLong(bufIdx := ePos + size(delimiter));
    ^copyFrom(buffer, sPos, ePos);
  endif;
  str := "";
  len := length(self);
  loop
    if len - bufPos < asLong(bufLen)  /* EOF case */
    then ^str + copyFrom(buffer, sPos,
      bufIdx := asInt((position := len) - bufPos));
    endif;
    if part := indexOf(buffer, del0 := delimiter[0], max(sPos, bufLen - size(delimiter)))
    then ePos := part;
      part := nil;
      loop
        if find(buffer, copyFrom(delimiter, 0, bufLen - ePos), ePos)
        then part := ePos := bufLen - ePos;
        endif;
      while not(part) cand ePos := indexOf(buffer, del0, ePos + 1)
      begin
      endLoop;
    endif;
    str := str + copyFrom(buffer, sPos, bufLen);
    writeBuffer(self);
    bufPos := bufPos + bufLen;
    readBuffer(self);
    if part
    then
      if find(delimiter, copyFrom(buffer, 0, size(delimiter) - ePos), ePos)
      then position := bufPos + asLong(bufIdx := size(delimiter) - ePos);
        ^copyFrom(str, 0, size(str) - ePos);
      endif;
    endif;
  while not(ePos := find(buffer, delimiter, sPos := 0))
  begin
  endLoop;
  position := bufPos + asLong(bufIdx := ePos + size(delimiter));
  ^str + copyFrom(buffer, sPos, ePos);
}!!

/* Return the current size of the file. */
Def readSize(self)
{ if readable
  then ^length(self);
  endif;
  ^0;
}!!

/* Delete target range from the file and replace it with
  source range from source.  Returns self. */
Def replace(self, source, srcB, srcE, dstB, dstE | str, tFile)
{ if handle cand writeable cand
    (size(str := copyFrom(source, srcB, srcE)) = srcE - srcB)
  then
    if dstE - dstB = size(str)
    then lseek(self, dstB, 0);
      write(self, str);
    else
      if readable
      then writeBuffer(self);
        tFile := create(setName(new(File):File, "temp.rpl"));
        copy(self:ancestor, tFile,
          length(self:ancestor) - lseek(self:ancestor, dstE, 0));
        lseek(self:ancestor, dstB, 0);
        write(self:ancestor, str);
        lseek(tFile:File, 0, 0);
        copy(tFile:File, self, length(tFile:File));
        truncate(self:ancestor);
        close(tFile:File);
        delete(tFile:File);
        if dstB < position
        then setPos(self,
          if position < dstE
          then min(position, length(self:ancestor));
          else position + dstB - dstE + size(str);
          endif);
        else readBuffer(self);
        endif;
      endif;
    endif;
  endif;
}!!

/* Set the file buffer size to be used by initBuffer()
  to allocate the file buffer. */
Def setBufferSize(self, newSize)
{ if handle
  then ^nil;
  endif;
  bufLen := asInt(newSize);
}!!

/* Set the file delimiter string (The delimiter
  instance variable). */
Def setDelimiter(self, d)
{ delimiter := d;
}!!

/* PRIVATE method.  Set bufPos and bufIdx based on the
  value of position. */
Def setPos(self, pos)
{ position := pos;
  bufPos := (position / bufLen) * bufLen;
  bufIdx := asInt(position - bufPos);
  readBuffer(self);
}!!

/* PRIVATE method.  Increments the file position by
  one, flushing and re-filling the buffer if necessary. */
Def skipOne(self)
{ bufIdx := bufIdx + 1;
  position := position + 1L;
  if bufIdx >= bufLen
  then writeBuffer(self);
    bufPos := position;
    readBuffer(self);
    bufIdx := 0;
  endif;
}!!

/* Returns a Stream initialized to operate on self. */
Def streamOver(self)
{ ^on(Stream, self);
}!!

/* Returns the number of characters from aStr
  successfully written to the file. */
Def write(self, aStr | num)
{ if writeable cand positionAsFile(self)
  then num := write(self:ancestor, aStr);
    setPos(self, position + num);
    ^num;
  endif;
  ^0;
}!!

/* PRIVATE method.  Writes the modified part of a
  buffer, if any, at the current file position.  The
  number of characters from the start of the buffer
  that have been modified is the value of the ivar
  bufDirty.  Returns self.  Assumes that the file is
  writeable. */
Def writeBuffer(self)
{ if bufDirty > 0
  then lseek(self:ancestor, bufPos, 0);
    bWrite(self:ancestor, buffer, bufDirty);
    bufDirty := 0;
  endif;
}!!

/* Nil is returned if there is an error, otherwise ch is put
  into the file at the current file position and the file
  position is incremented by one. */
Def writeChar(self, ch | ret)
{ if ret := put(self, ch, position)
  then skipOne(self);
  endif;
  ^ret;
}!!

/* Write a line delimited by the string in the
  instance variable delimiter.  Returns an Int
  which is the number of bytes actually written
  to the file including the length of the
  delimiter string. */
Def writeLine(self, s)
{ ^write(self, s + delimiter);
}!!

/* Return the maximum size of the file. */
Def writeSize(self)
{ if writeable
  then ^33554432L;
  endif;
  ^0;
}!!

/* TextFile class initialization */
setBufSize(TextFile, 512);
