(********************************************************************)
(*                                                                  *)
(*  strifile.s7i  Implementation type for files stored in a string  *)
(*  Copyright (C) 2008, 2010 - 2013, 2020 - 2024  Thomas Mertes     *)
(*                                                                  *)
(*  This file is part of the Seed7 Runtime Library.                 *)
(*                                                                  *)
(*  The Seed7 Runtime Library is free software; you can             *)
(*  redistribute it and/or modify it under the terms of the GNU     *)
(*  Lesser General Public License as published by the Free Software *)
(*  Foundation; either version 2.1 of the License, or (at your      *)
(*  option) any later version.                                      *)
(*                                                                  *)
(*  The Seed7 Runtime Library is distributed in the hope that it    *)
(*  will be useful, but WITHOUT ANY WARRANTY; without even the      *)
(*  implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR *)
(*  PURPOSE.  See the GNU Lesser General Public License for more    *)
(*  details.                                                        *)
(*                                                                  *)
(*  You should have received a copy of the GNU Lesser General       *)
(*  Public License along with this program; if not, write to the    *)
(*  Free Software Foundation, Inc., 51 Franklin Street,             *)
(*  Fifth Floor, Boston, MA  02110-1301, USA.                       *)
(*                                                                  *)
(********************************************************************)


(**
 *  [[file|File]] implementation type for files stored in a [[string]].
 *  String files are seekable, therefore they
 *  support the functions [[#length(in_striFile)|length]],
 *  [[#seek(inout_striFile,in_integer)|seek]] and
 *  [[#tell(in_striFile)|tell]].
 *)
const type: striFile is sub null_file struct
    var string: content is "";
    var integer: position is 1;
  end struct;

type_implements_interface(striFile, file);


(**
 *  Open a ''striFile'' with the given string content.
 *  @return the file opened.
 *)
const func file: openStriFile (in string: content) is func
  result
    var file: newFile is STD_NULL;
  local
    var striFile: new_striFile is striFile.value;
  begin
    new_striFile.content := content;
    newFile := toInterface(new_striFile);
  end func;


(**
 *  Open a ''striFile'' with an empty string content.
 *  @return the file opened.
 *)
const func file: openStriFile is func
  result
    var file: newFile is STD_NULL;
  local
    var striFile: new_striFile is striFile.value;
  begin
    newFile := toInterface(new_striFile);
  end func;


(**
 *  Write the [[string]] ''stri'' to ''outStriFile''.
 *)
const proc: write (inout striFile: outStriFile, in string: stri) is func
  begin
    if outStriFile.position = succ(length(outStriFile.content)) then
      outStriFile.content &:= stri;
    elsif outStriFile.position <= length(outStriFile.content) then
      if pred(outStriFile.position) <= length(outStriFile.content) - length(stri) then
        outStriFile.content @:= [outStriFile.position] stri;
      else
        outStriFile.content := outStriFile.content[.. pred(outStriFile.position)];
        outStriFile.content &:= stri;
      end if;
    elsif outStriFile.position > length(outStriFile.content) then
      outStriFile.content &:=
          "\0;" mult pred(outStriFile.position - length(outStriFile.content)) &
          stri;
    end if;
    outStriFile.position +:= length(stri);
  end func;


const proc: moveLeft (inout striFile: outStriFile, in string: stri) is func
  begin
    if outStriFile.position > length(stri) then
      outStriFile.position -:= length(stri);
    else
      outStriFile.position := 1;
    end if;
  end func;


(**
 *  Read a character from ''inStriFile''.
 *  @return the character read, or [[char#EOF|EOF]] at the end of the file.
 *)
const func char: getc (inout striFile: inStriFile) is func
  result
    var char: charRead is ' ';
  begin
    if inStriFile.position <= length(inStriFile.content) then
      charRead := inStriFile.content[inStriFile.position];
      incr(inStriFile.position);
    else
      charRead := EOF;
    end if;
  end func;


(**
 *  Read a [[string]] with maximum length from ''inStriFile''.
 *  @return the string read.
 *  @exception RANGE_ERROR The parameter ''maxLength'' is negative.
 *)
const func string: gets (inout striFile: inStriFile, in integer: maxLength) is func
  result
    var string: striRead is "";
  begin
    if maxLength <= 0 then
      if maxLength <> 0 then
        raise RANGE_ERROR;
      end if;
    elsif maxLength <= succ(length(inStriFile.content) - inStriFile.position) then
      striRead := inStriFile.content[inStriFile.position fixLen maxLength];
      inStriFile.position +:= maxLength;
    else
      striRead := inStriFile.content[inStriFile.position ..];
      inStriFile.position := succ(length(inStriFile.content));
    end if;
  end func;


(**
 *  Read a [[string]] from ''inStriFile'' until the ''terminator'' character is found.
 *  If a ''terminator'' is found the string before the ''terminator'' is
 *  returned and the ''terminator'' character is assigned to inStriFile.bufferChar.
 *  The file position is advanced after the ''terminator'' character.
 *  If no ''terminator'' is found the rest of ''inStriFile'' is returned and
 *  [[char#EOF|EOF]] is assigned to the inStriFile.bufferChar. When the function
 *  is left inFile.bufferChar contains either ''terminator'' or [[char#EOF|EOF]].
 *  @param inFile File from which the string is read.
 *  @param terminator Character which terminates the string.
 *  @return the string read without the ''terminator'' or the rest of the
 *          file if no ''terminator'' is found.
 *)
const func string: getTerminatedString (inout striFile: inStriFile,
                                        in char: terminator) is func
  result
    var string: stri is "";
  local
    var integer: terminatorPos is 0;
  begin
    terminatorPos := pos(inStriFile.content, terminator, inStriFile.position);
    if terminatorPos <> 0 then
      stri := inStriFile.content[inStriFile.position .. pred(terminatorPos)];
      inStriFile.position := succ(terminatorPos);
      inStriFile.bufferChar := terminator;
    else
      stri := inStriFile.content[inStriFile.position ..];
      inStriFile.position := succ(length(inStriFile.content));
      inStriFile.bufferChar := EOF;
    end if;
  end func;


(**
 *  Read a word from ''inStriFile''.
 *  Before reading the word it skips spaces and tabs. The function
 *  accepts words ending with " ", "\t", "\n", or [[char#EOF|EOF]].
 *  The word ending characters are not copied into the string.
 *  When the function is left inStriFile.bufferChar contains ' ',
 *  '\t', '\n' or [[char#EOF|EOF]].
 *  @return the word read.
 *)
const func string: getwd (inout striFile: inStriFile) is func
  result
    var string: stri is "";
  local
    const set of char: space_or_tab is {' ', '\t'};
    const set of char: space_tab_or_nl is {' ', '\t', '\n'};
    var integer: wordStartPos is 0;
    var integer: wordEndPos is 0;
  begin
    wordStartPos := inStriFile.position;
    while wordStartPos <= length(inStriFile.content) and
        inStriFile.content[wordStartPos] in space_or_tab do
      incr(wordStartPos);
    end while;
    if wordStartPos > length(inStriFile.content) then
      inStriFile.position := wordStartPos;
      inStriFile.bufferChar := EOF;
    else
      wordEndPos := succ(wordStartPos);
      while wordEndPos <= length(inStriFile.content) and
          inStriFile.content[wordEndPos] not in space_tab_or_nl do
        incr(wordEndPos);
      end while;
      if wordEndPos <= length(inStriFile.content) then
        stri := inStriFile.content[wordStartPos .. pred(wordEndPos)];
        inStriFile.position := succ(wordEndPos);
        inStriFile.bufferChar := inStriFile.content[wordEndPos];
      else
        stri := inStriFile.content[wordStartPos ..];
        inStriFile.position := succ(length(inStriFile.content));
        inStriFile.bufferChar := EOF;
      end if;
    end if;
  end func;


(**
 *  Read a line from ''inStriFile''.
 *  A striFile works as if all lines end with '\n'.
 *  The line ending character is not copied into the string. When
 *  the function is left inStriFile.bufferChar contains '\n' or
 *  [[char#EOF|EOF]].
 *  @return the line read.
 *)
const func string: getln (inout striFile: inStriFile) is func
  result
    var string: stri is "";
  local
    var integer: newlinePos is 0;
  begin
    newlinePos := pos(inStriFile.content, '\n', inStriFile.position);
    if newlinePos <> 0 then
      stri := inStriFile.content[inStriFile.position .. pred(newlinePos)];
      inStriFile.position := succ(newlinePos);
      inStriFile.bufferChar := '\n';
    else
      stri := inStriFile.content[inStriFile.position ..];
      inStriFile.position := succ(length(inStriFile.content));
      inStriFile.bufferChar := EOF;
    end if;
  end func;


(**
 *  Determine the end-of-file indicator.
 *  The end-of-file indicator is set if at least one request to read
 *  from the file failed.
 *  @return TRUE if the end-of-file indicator is set, FALSE otherwise.
 *)
const func boolean: eof (in striFile: inStriFile) is
  return inStriFile.position > length(inStriFile.content);


(**
 *  Determine if at least one character can be read successfully.
 *  This function allows a file to be handled like an iterator.
 *  @return FALSE if ''getc'' would return [[char#EOF|EOF]],
 *          TRUE otherwise.
 *)
const func boolean: hasNext (in striFile: inStriFile) is
  return inStriFile.position <= length(inStriFile.content);


(**
 *  Obtain the length of a ''aStriFile''.
 *  The file length is measured in characters.
 *  @return the length of a file.
 *)
const func integer: length (in striFile: aStriFile) is
  return length(aStriFile.content);


(**
 *  Truncate ''aStriFile'' to the given ''length''.
 *  If the file previously was larger than ''length'', the extra data is lost.
 *  If the file previously was shorter, it is extended, and the extended
 *  part is filled with null bytes ('\0;').
 *  The file length is measured in characters.
 *)
const proc: truncate (inout striFile: aStriFile, in integer: length) is func
  begin
    if length < 0 then
      raise RANGE_ERROR;
    elsif length(aStriFile.content) > length then
      aStriFile.content := aStriFile.content[.. length];
    elsif length(aStriFile.content) < length then
      aStriFile.content := aStriFile.content &
          "\0;" mult (length - length(aStriFile.content));
    end if;
  end func;


(**
 *  Determine if the file ''aFile'' is seekable.
 *  If a file is seekable the functions ''seek'' and ''tell''
 *  can be used to set and and obtain the current file position.
 *  @return TRUE, since a ''striFile'' is seekable.
 *)
const boolean: seekable (in striFile: aFile) is TRUE;


(**
 *  Set the current file position.
 *  The file position is measured in characters from the start of the file.
 *  The first character in the file has the position 1.
 *  @exception RANGE_ERROR The file position is negative or zero.
 *)
const proc: seek (inout striFile: aStriFile, in integer: position) is func
  begin
    if position <= 0 then
      raise RANGE_ERROR;
    else
      aStriFile.position := position;
    end if;
  end func;


(**
 *  Obtain the current file position of ''aStriFile''.
 *  The file position is measured in characters from the start of the file.
 *  The first character in the file has the position 1.
 *  @return the current file position.
 *)
const func integer: tell (in striFile: aStriFile) is
  return aStriFile.position;