/*
  libc-style File I/O Routines for Quake III: Arena
  Copyright 2001  PhaethonH <phaethon@linux.ucla.edu>

  Permission granted to copy, modify, distribute, or otherwise use this code,
  provided this copyright notice remains intact
*/

/* Last revision 2002.03.09 */

#include "q_shared.h"
#include "bg_io.h"


#define debug Com_Printf


/*

Sections code:
chaoZ
Clotho
Lachesis
Atropos
charoN
Iris
.macro'd

Methods mentioned in BSD manpage for stdio:
L clearerr - checks and resets stream status
Z fclose - close stream
//fdopen - open stream from file descriptor -- impossible to do correctly --
L feof - check stream status
L ferror - check stream error status
A fflush - flush stream
C fgetc - get next character from stream
//fgetpos - get position in stream -- needlessly complicated for QVM --
C fgets - get line from stream
L fileno - get file descriptor of stream
Z fopen - open file as stream
A fprintf - formatted output
//fpurge - flush stream -- not found in glibc --
A fputc - output character to stream
A fputs - output string to stream
C fread - binary stream input
Z freopen - open different file as the same stream
  fscanf - formatted input from stream
N fseek - reposition stream
//fsetpos - reposition stream -- needlessly complicated for QVM --
N ftell - get position in stream
A fwrite - binary stream output
C getc - get next character from stream
. getchar - get next character from stdin
//gets - get line from stdin  --  *** DO NOT IMPLEMENT!!! *** use fgets instead
//getw - get next (SysV) word from stream -- SVID only --
//mktemp - create unique temporary filename from template -- DO NOT IMPLEMENT -- use tmpfile instead.
I perror - prints system error message to stderr
A printf - formatted output to stdout
A putc - output character to stream
. putchar - output character to stdout
A puts - output string to stream
//putw - output (SysV) word character to stream -- SVID only --
//remove - remove directory -- impossible to do in QVM --
N rewind - reposition stream to beginning
  scanf - formatted input from stdin
//setbuf - set stream buffer, size set by library -- not applicable to QVM --
//setbuffer - set stream buffer, size set by caller -- not applicable to QVM --
//setlinebuf - set file to line-buffered mode -- not applicable to QVM --
//setvbuf - meddle with file buffering mode -- not applicable to QVM --
I sprintf - formatted output to string
//sscanf - formatted input from string -- already implemented in bg_lib --
I strerror - string describing an error code
I sys_errlist - system error message  -- array of string --
I sys_nerr - system error message  -- global integer --
//tempnam - create temporary file name given directory and filename prefix. -- DO NOT IMPLEMENT -- use tmpfile instead.
  tmpfile - creates FILE of temporary file, mode "w+b", closed on program end.
//tmpnam - return unique temporary filename of length L_tmpnam. -- DO NOT IMPLEMENT --- use tmpfile instead.
C ungetc - unget a character from stream (pushback for later read)
A vfprintf - varargs formatted output to stream -- redundant --
//vfscanf - varargs formatted input from stream -- redundant --
A vprintf - varargs formatted output to stdout -- redundant --
//vscanf - varargs formatted input from stdin -- redundant --
//vsprintf - varargs formatted output to string -- already implemented in bg_lib --
//vsscanf - varargs formatted input from string -- redundant --
 
*/





#define _ioerror(n) (errno = n)
#define _ferror (stream->flags |= FILE_ERROR)
#define READABLE (stream->flags & FILE_READ)
#define WRITABLE (stream->flags & FILE_WRITE)

#define SANITY if ((stream < _qfiles) || (stream > (_qfiles + FOPEN_MAX))) return
#define INSANE ((stream < _qfiles) || (stream > (_qfiles + FOPEN_MAX)))
#define ERRCHECK if (stream->errno) return EOF





/****************************
** Global variable  errno   **
 ****************************/

int errno;


/* The collection of files. */
FILE _qfiles[MAX_FILES] = { 0, };










/*********************
   Helper functions.
**********************/


/*
  Output to terminal (screen).
  This really ought to be generalized into a Unix-like devices framework.
*/
static
int
_terminal_out (const void *ptr, int size)
{
  char buf[256];
  int chew, bite;

  chew = bite = 0;
  while (chew < size)
    {
      bite = (size  - chew > sizeof(buf)) ? sizeof(buf) : size - chew + 1;
      Q_strncpyz(buf, ((char*)ptr) + chew, bite);
      chew += bite - 1;
      Com_Printf("%s", buf);
    }
  return chew;
}

static
int
_terminal_err (const void *ptr, int size)
{
  return _terminal_out(ptr, size);
}


/*
  Find a new file slot among the rabble.
*/
static
FILE *
_fnewslot ()
{
  int i;

  for (i = 0; (i < FOPEN_MAX) && (_qfiles[i].flags & FILE_ACTIVE); i++);
  if (i < FOPEN_MAX)
      return _qfiles + i;
  return NULL;
}


/*
  Convert from string to Q3-style numerical permission value.
*/
static
int
_fmode2mode (const char *s)
/*
 "r" =  open for read, stream at start
 "r+" = open for read-write, stream at start -- not allowed
 "w" = open for write, erase content, stream at start
 "w+" = open for read-write, erase content, stream at start -- not allowed
 "a" = open for append, stream at end
 "a+" = open for read-append, stream at end -- reinterpreted as sync'd append
 "b" = binary mode


*/
{
  int mode;
  int i;

  for(mode = 0, i = 0; s[i]; i++)
    {
      switch (s[i])
        {
          case 'r': mode = FS_READ; break;
          case 'w': mode = FS_WRITE; break;
          case 'a': mode = FS_APPEND; break;
          case '+': if (mode == FS_APPEND) mode = FS_APPEND_SYNC; break;
        }
    }
  return mode;
}



/*
  Determine if character is a line terminator.

Well, someone royally f*cked-up with line-terminator choice.

Un*x goes with C's literal specs on line-terminator (\n => 10 => LF)
CPM/MS-DOS's is justifiable by analogy to teletypewriters (but in software?!)
 (CR for carriage (cursor) to far left, LF to go down one line)
MacOS corresponds literally to the Enter/Return key (CR => 13)

But this having to check for text file type is silly.
One-character line terminator (Unix/Mac) is far easier to deal with.
*/
static int _lineterm (int c, FILE *stream)
{
  if ((c != 10) && (c != 13))  return 0;
  if (stream->eol == EOL_UNKNOWN) {
    if (c == 10) stream->eol = EOL_UNIX;
    else if (c == 13) {
      stream->eolcheck = c, c = fgetc(stream);
      stream->eol = (c == 10) ? EOL_CPM : EOL_MAC;
    }
  }
    
  switch (stream->eol) {
    case EOL_UNIX: case EOL_CPM: if (c == 10) return 1; break;
    case EOL_MAC: if (c == 13) return 1; break;
  }
  return 0;
}


/*
  Initialize sys_errlist
*/
static
void
_errlist_init()
{
  sys_errlist[ENONE] = "Success";
  sys_errlist[EBADF] = "Bad file descriptor";
  sys_errlist[EINVAL] = "Invalid seek";
  sys_errlist[ENAMETOOLONG] = "Filename too long";
  sys_errlist[EFAULT] = "No such file or directory";
  sys_errlist[EMFILE] = "Too many files open";
  sys_errlist[ENFILE] = "Too many files open in system";
  sys_errlist[EACCESS] = "Permission denied";
}


/* Do-nothing function. */
static void _nop() { return; }

void (*files_init)();

/*
  Install sanity into stdin/out/err.
*/
static
void
_qfiles_init()
{
  _qfiles[0].flags |= (FILE_ACTIVE | FILE_READ);
  _qfiles[0].eol = EOL_UNIX;
  _qfiles[0].fd = -1;
  _qfiles[0].len = 0x7FFFFFFF;
  strcpy(_qfiles[0].name, "stdin");

  _qfiles[1].flags |= (FILE_ACTIVE | FILE_WRITE);
  _qfiles[1].eol = EOL_UNIX;
  _qfiles[1].fd = -2;
  _qfiles[1].len = 0x7FFFFFFF;
  strcpy(_qfiles[1].name, "stdout");

  _qfiles[2].flags |= (FILE_ACTIVE | FILE_WRITE);
  _qfiles[2].eol = EOL_UNIX;
  _qfiles[2].fd = -3;
  _qfiles[2].len = 0x7FFFFFFF;
  strcpy(_qfiles[2].name, "stderr");

  files_init = _nop;
}



void (*files_init)() = _qfiles_init;













/*************
***************
**           **
**   CHAOS   **
**           **
** Construct **
**  Destruct **
**           **
***************
 *************/



/*
  With stream, closes old file, opens 'path', assigns to stream.
  Usually used for redirecting stdin, stdout, stderr.

  Returns:
    * stream
    * EOF on error
*/
FILE *
freopen (const char *path, const char *mode, FILE *stream)
{
  int qmode;
  int newlen, newfd;

  files_init();
  if INSANE { _ioerror(EBADF); return NULL; }
  if (stream->fd > 0)
      trap_FS_FCloseFile(stream->fd);
  stream->flags |= FILE_ACTIVE;
  qmode = _fmode2mode(mode);
  switch (qmode)
    {
      case FS_READ: stream->flags |= FILE_READ; break;
      case FS_WRITE: case FS_APPEND: case FS_APPEND_SYNC:
          stream->flags |= FILE_WRITE; break;
    }
  stream->len = trap_FS_FOpenFile(path, &(stream->fd), qmode);
  if (!stream->fd)
    {
      _ioerror((stream->flags & FILE_READ) ? EACCESS : EFAULT);
      return NULL;
    }
  strcpy(stream->name, path);
  return stream;
}




/*
  Opens a file by name.

  Returns:
   * pointer to stream object
   * NULL on error
*/
FILE *
fopen (const char *filename, const char *mode)
{
  FILE *retval;

  retval = _fnewslot();
  if (!retval) return NULL;
  memset(retval, 0, sizeof(*retval));
  return freopen(filename, mode, retval);  /* Cheap cheap cheap... */
}



/*
  Returns:
   0 on success
   EOF on error, 'errno' is set, further access to stream is undefined.

  Errors:
   EBADF - underlying file descriptor is invalid.
*/
int
fclose (FILE *stream)
{
  files_init();

  if INSANE { _ioerror(EBADF); return EOF; }

  if (stream->fd > 0)
    {
      trap_FS_FCloseFile(stream->fd);
      memset(stream, 0, sizeof(*stream));
    }
  return 0;
}









/*************
***************
**           **
**  CLOTHO   **
**           **
**  Data In  **
**           **
***************
 *************/

/*
  Returns:
    number of items (nmemb) read from file.
    Upon EOF or error, shortened value (possibly 0).

  The interface is designed to deal with endianness.
  Since QVM remains little-endian (Intel byte order) across platforms, we ignore byte swapping.

  This could have been written in terms of fgetc (especially with that
stream->buf part), but the code was done this way on the principle that the
fewer calls made to the VM (traps), the better the performance will be.
*/
size_t
fread (void *ptr, size_t size, size_t nmemb, FILE *stream)
{
  unsigned char *dest;
  int maxlen;
  int i;

  files_init();

  if INSANE return 0;
  if (!READABLE) return 0;

  switch (stream->fd)
    {
      case -1: return 0; break;
      case -2:
      case -3: return 0; break;
    }
  if (stream->fd < 1) { return 0; }

  maxlen = size * nmemb;
  if (maxlen > (stream->len - stream->pos))
      maxlen = stream->len - stream->pos;
  if (stream->flags & FILE_BUFFED)
    {
      *((char*)ptr) = stream->buf;
      maxlen--;
      ptr = ((char*)ptr) + 1;
    }
//void    trap_FS_Read( void *buffer, int len, fileHandle_t f );
  trap_FS_Read(ptr, maxlen, stream->fd);
  stream->pos += maxlen;
  maxlen /= size;
  return maxlen;
}


/*
  Reads one character from stream.

  Returns:
   * the actual character read.
   * EOF on error
*/
int
fgetc (FILE *stream)
{
  unsigned char ch;

  if INSANE return EOF;
  if (!READABLE) return EOF;

  if (stream->flags & FILE_BUFFED)
    {
      stream->flags &= ~FILE_BUFFED;
      return stream->buf;
    }
  if (fread((void*)&ch, sizeof(ch), 1, stream))
      return ch;
  _ferror;
  return EOF;
}



/*
  Pushes back a character to the stream for later re-reading.
  Only one pushback is guaranteed.

  Returns:
   * the pushed character
   * EOF on error
*/
int
ungetc (int c, FILE *stream)
{
  if INSANE return EOF;
  if (!READABLE) return EOF;

  stream->flags |= FILE_BUFFED;
  stream->buf = (unsigned char)c;
  return stream->buf;
}



/*
  Reads in at most size-1 characters from stream and stores into buffer at s.
  Line terminators are preserved.
  A \0 (string terminator) is written to the buffer at the end.

  Returns:
   * s on success
   * NULL on error or immediate EOF
*/
char *
fgets (char *s, int size, FILE *stream)
{
  char c;
  int i;

  if INSANE return NULL;
  if (!READABLE) return NULL;

  if (stream->pos >= stream->len)
      return NULL;
  i = 0;
  while ( ((c = fgetc(stream)) >= 0)
//         && (!_lineterm(c, stream))
         && (i < (size - 1)) )
    {
      s[i++] = c;
      if (_lineterm(c, stream))
          break;
    }
  s[i] = 0;
  if (c == EOF)
    { _ferror; return NULL; }
  return s;
}



















/**************
****************
**            **
**  ATROPOS   **
**            **
**  Data Out  **
**            **
****************
 **************/

/*
  Write character into stream.

  Returns:
   * the actual character written.
   * EOF on error.
*/
int
fputc (int c, FILE *stream)
{
  unsigned char ch;

  if INSANE return EOF;
  if (!WRITABLE) return EOF;

  ch = (unsigned char)c;
  if (fwrite((void*)&ch, sizeof(unsigned char), 1, stream))
      return ch;
  _ferror;
  return EOF;
}



/*
 Returns:
  Number of items (not bytes) successfully written.
  If an error occurs, return value is shortened (probably 0).

  The interface is designed to deal with endianness.
  Since QVM remains little-endian (Intel byte order) across platforms, we ignore byte swapping.
*/
size_t
fwrite (const void *ptr, size_t size, size_t nmemb, FILE *stream)
{
  int outbytes;

  files_init();

  if INSANE return 0;
  if (!WRITABLE) return 0;

  outbytes = size * nmemb;
//void    trap_FS_Write( const void *buffer, int len, fileHandle_t f );
  switch (stream->fd)
    {
      case -1: return 0; break;
      case -2: return _terminal_out(ptr, outbytes); break;
      case -3: return _terminal_err(ptr, outbytes); break;
      default:
        if (stream->fd < 1) { return 0; }
        trap_FS_Write(ptr, outbytes, stream->fd);
        break;
    }
  return nmemb;
}



/*
  Writes string s to stream (without the final \0 put into the stream).

  Returns:
   * non-negative (zero or positive) number on success
   * EOF on error
*/
int
fputs (const char *s, FILE *stream)
{
#if 0   //One-by-one.
  char *p;

  if INSANE return EOF;
  if (!WRITABLE) return EOF;

  for(p = s; *p; fputc(*p, stream), p++);
  return (p - s);
#else   //Block write.
  if INSANE return EOF;
  if (!WRITABLE) return EOF;

  return fwrite(s, sizeof(char), strlen(s), stream);
#endif
}



/*
  Force the flushing of output of a stream.

  Since we can't actually force the VM to do anything about file buffering,
  we just pretend we succeed all the time.

  Returns:
   * 0 on success
   * EOF on error
*/
int
fflush (FILE *stream)
{
  if INSANE return EOF;
  return 0;
}



/*
  Formatted output to stream with varargs args.

  Returns number of characters written.
*/
int
vfprintf (FILE *stream, const char *format, va_list argptr)
{
  char buf[32000];  /* Total local vars space must be < 32768 */

  files_init();

  vsprintf(buf, format, argptr);
  return fputs(buf, stream);
}


/*
  Formatted output to stream.

  Returns number of characters written.
*/
int
fprintf (FILE *stream, const char *format, ...)
{
  va_list argptr;
  int i;

  files_init();

  va_start(argptr, format);
  i = vfprintf(stream, format, argptr);
  va_end(argptr);
  return i;
}


/*
  Formatted output to stdout with varargs args.

  Returns number of characters written.
*/
int
vprintf (const char *format, va_list argptr)
{
  return vfprintf(stdout, format, argptr);
}


/*
  Formatted output to stdout.

  Returns number of characters written.
*/
int
printf (const char *format, ...)
{
  va_list argptr;
  int i;

  files_init();

  va_start(argptr, format);
  i = vfprintf(stdout, format, argptr);
//  i = fputs(va((char*)format, argptr), stdout);
  va_end(argptr);
  return i;
}










/*************
***************
**           **
**  CHARON   **
**           **
** Traveling **
**    the    **
**   Stream  **
**           **
***************
 *************/


/*
  Current value of file position indicator.

  Returns:
   * 0 on success
   * -1 on error, errno set
*/
long
ftell (FILE *stream)
{
  files_init();
  if INSANE { _ioerror(EBADF); return -1; }
  return stream->pos;
}



/*
  Set file position indicator.
  Due to QVM, only read-mode seek works.

  Returns:
   * 0 on success
   * -1 on error, errno set
*/
int
fseek (FILE *stream, long offset, int whence)
{
  int retval;
  long target;
  int skipped;
  int newlen, newfd;
#define JUMPSIZE 256
  char z[JUMPSIZE];

//debug("fseek: start %d %d from %d\n", offset, whence, stream->pos);
  files_init();

  if INSANE { _ioerror(EBADF); return -1; }
  if (WRITABLE) { _ioerror(EBADF); return -1; }

  switch (whence)
    {
      case SEEK_CUR: target = stream->pos + offset; break;
      case SEEK_SET: target = offset; break;
      case SEEK_END: target = stream->len - offset; break;
/* Check interpretation on _END.  Positive means plus or minus? */
      default: _ioerror(EINVAL); _ferror; return -1; break;
    }
//debug("fseek: targetting absolute offset %d\n", target);
  if (target < 0)
      target = 0;
  newlen = trap_FS_FOpenFile(stream->name, &(newfd), FS_READ);
//debug("fseek: reopen => %d (%d)\n", newfd, newlen);
  if (!newfd)
    { _ioerror(EBADF); _ferror; return -1; }
  if (stream->fd > 0)
      trap_FS_FCloseFile(stream->fd);
  stream->fd = newfd;
  stream->len = newlen;
  stream->pos = 0;
  retval = 0;
  if (target > stream->len)
    {
//debug("fseek: target exceeds filesize.  Error\n");
      retval = -1;
      _ioerror(1);
      target = stream->len;
    }
//  target = (target < stream->len) ? target : stream->len;
  while (target > 0)
    {
      skipped = (JUMPSIZE < target) ? JUMPSIZE : target;
      trap_FS_Read(z, skipped, stream->fd);
      stream->pos += skipped;
      target -= skipped;
    }
//  stream->errno = 0;
//debug("fseek: settled on %d\n", stream->pos);
  stream->flags &= ~(FILE_BUFFED | FILE_ERROR);
  return retval;
}


int
rewind (FILE *stream)
{
  return fseek(stream, 0, FS_SEEK_SET);
}













/************
**************
**          **
** LACHESIS **
**          **
**   File   **
**  Status  **
**          **
**************
 ************/


/*
  Checks for EOF condition.

  Returns:
   * non-zero on End Of File
   * zero if not end of file
*/
int
feof (FILE *stream)
{
  files_init();
  if INSANE return 1;  /* If it's not a file, it ended. */
  if (stream->pos >= stream->len)
      return 1;
  return 0;
}


/*
  Returns the underlying filedescriptor for stream.
  Returns -1 on error, and sets errno to EBADF.
*/
int
fileno (FILE *stream)
{
  files_init();
  if INSANE { _ioerror(EBADF); return -1; }
  return stream->fd;
}


/*
  BSD manpage hints this just returns the truth of an error condition.
*/
int
ferror (FILE *stream)
{
  files_init();
  if INSANE return EBADF;  /* convenient. */
  return ((stream->flags & FILE_ERROR) ? 1 : 0);
//  return stream->errno;
}


/*
  Clears the error marker (code) on stream.
*/
void
clearerr (FILE *stream)
{
  files_init();
  if INSANE return;
//  stream->errno = ENONE;
  stream->flags &= ~(FILE_ERROR | FILE_BUFFED);
}









/************
**************
**          **
**   IRIS   **
**          **
** Messages **
**          **
**************
 ************/


char *sys_errlist[sys_nerr];

/*
  Returns:
   * Error message associated with errnum
   * NULL on error
*/
char *
strerror (int errnum)
{
  if (*sys_errlist == 0) _errlist_init();
  if ((errnum < 0) || (errnum > sys_nerr))
      return NULL;
  return sys_errlist[errnum];
}



/*
  Print error message, with prefix, to stderr.
*/
void
perror (const char *s)
{
  return;
}



/*
  Formatted output to string.
*/
int
sprintf (char *str, const char *format, ...)
{
  va_list argptr;
  int i;

  files_init();

  va_start(argptr, format);
  i = vsprintf(str, format, argptr);
  va_end(argptr);
  return i;
}
