/*

    Common GPSBabel file I/O API

    Copyright (C) 2006,2007,2008 Olaf Klein, o.b.klein@gpsbabel.org

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    This program 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 General Public License for more details.

    You should have received a copy of the GNU 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.

 */

#include <QByteArray>          // for QByteArray
#include <QChar>               // for QChar, operator==, operator!=
#include <QString>             // for QString
#include <QtGlobal>            // for qPrintable

#include <cassert>             // for assert
#include <cctype>              // for tolower
#include <cstdarg>             // for va_list, va_end, va_copy, va_start
#include <cstdio>              // for EOF, ferror, ftell, SEEK_SET, SEEK_CUR, SEEK_END, clearerr, fclose, feof, fflush, fileno, fread, fseek, fwrite, ungetc, vsnprintf, FILE, stdin, stdout
#include <cstring>             // for memcpy, strlen, strchr, strcpy, strncat

#include "defs.h"
#include "gbfile.h"
#include "src/core/logging.h"

#if __WIN32__
/* taken from minigzip.c (part of the zlib project) */
#  include <fcntl.h>
#  include <io.h>
#  define SET_BINARY_MODE(file) _setmode(fileno(file), O_BINARY)
#else
#  define SET_BINARY_MODE(file)
#endif

#define MYNAME "gbfile"
#define NO_ZLIB MYNAME ": No zlib support.\n"

/* About the ZLIB_INHIBITED stuff:
 *
 * If a user goes out of his way to build with ZLIB_INHIBITED set,
 * we jettison our use of zlib entirely within this file, replacing
 * all calls out to zlib with calls to abort() as that's an internal
 * consistency error.
 *
 */


/*******************************************************************************/
/* %%%                          file api wrappers                           %%% */
/*******************************************************************************/

#if !ZLIB_INHIBITED

/*******************************************************************************/
/* %%%                            Zlib file api                            %%% */
/*******************************************************************************/

static gbfile*
gzapi_open(gbfile* self, const char* mode)
{
  char openmode[32];

  self->gzapi = 1;

  /* under non-posix systems files MUST be opened in binary mode */

  strcpy(openmode, mode);
  if (strchr(mode, 'b') == nullptr) {
    strncat(openmode, "b", sizeof(openmode) - strlen(openmode) - 1);
  }

  if (self->is_pipe) {
    FILE* fd;
    if (self->mode == 'r') {
      fd = stdin;
    } else {
      fd = stdout;
    }
    SET_BINARY_MODE(fd);
    self->handle.gz = gzdopen(fileno(fd), openmode);
  } else {
#if __WIN32__
    // On Windows, convert UTF-8 to wchar_t[] and use gzopen_w().
    QString name(self->name);
    self->handle.gz = gzopen_w((const wchar_t*) name.utf16(), openmode);
#else
    // On other platforms, convert to native locale (UTF-8 or other 8-bit).
    self->handle.gz = gzopen(qPrintable(QString(self->name)), openmode);
#endif
  }

  if (self->handle.gz == nullptr) {
    fatal("%s: Cannot %s file '%s'!\n",
          self->module,
          (self->mode == 'r') ? "open" : "create",
          self->name);
  }

  return self;
}

static int
gzapi_close(gbfile* self)
{
  return gzclose(self->handle.gz);
}

static int
gzapi_seek(gbfile* self, int32_t offset, int whence)
{
  assert(whence != SEEK_END);

  if ((whence == SEEK_CUR) && (self->back != -1)) {
    offset--;
  }
  int result = gzseek(self->handle.gz, offset, whence);
  self->back = -1;

  if (result < 0) {
    if (self->is_pipe) {
      fatal("%s: This format cannot be used in piped commands!\n", self->module);
    }
    fatal("%s: online compression not yet supported for this format!", self->module);
  }
  return 0;
}

static gbsize_t
gzapi_read(void* buf, const gbsize_t size, const gbsize_t members, gbfile* self)
{
  int result = 0;
  char* target = (char*) buf;
  int count = size * members;

  if (self->back != -1) {
    *target++ = self->back;
    count--;
    result++;
    self->back = -1;
  }
  result += gzread(self->handle.gz, target, count);

  /* Check for an incomplete READ */
  if ((members == 1) && (size > 1) && (result > 0) && (result < (int)size)) {
    fatal("%s: Unexpected end of file (EOF)!\n", self->module);
  }

  result /= size;

  if ((result < 0) || ((gbsize_t)result < members)) {
    int errnum;

    const char* errtxt = gzerror(self->handle.gz, &errnum);

    /* Workaround for zlib bug: buffer error on empty files */
    if ((errnum == Z_BUF_ERROR) && (gztell(self->handle.gz) == 0)) {
      return (gbsize_t) 0;
    }
    if ((errnum != Z_STREAM_END) && (errnum != 0))
      fatal("%s: zlib returned error %d ('%s')!\n",
            self->module, errnum, errtxt);
  }
  return (gbsize_t) result;
}

static gbsize_t
gzapi_write(const void* buf, const gbsize_t size, const gbsize_t members, gbfile* self)
{
  return gzwrite(self->handle.gz, buf, size * members) / size;
}

static int
gzapi_flush(gbfile* self)
{
  return gzflush(self->handle.gz, Z_SYNC_FLUSH);
}

static gbsize_t
gzapi_tell(gbfile* self)
{
  gbsize_t result = gztell(self->handle.gz);
  if (self->back != -1) {
    result--;
  }

  return result;
}

static int
gzapi_eof(gbfile* self)
{
  int res = 0;

  if (self->back != -1) {
    return res;
  }

  res  = gzeof(self->handle.gz);
  if (!res) {
    unsigned char test;
    int len = gzread(self->handle.gz, &test, 1);
    if (len == 1) {
      /* No EOF, put the single byte back into stream */
      self->back = test;
    } else {
      /* we are at the end of the file */
      if (global_opts.debug_level > 0) {
        /* now gzeof() should return 1 */
        if (!gzeof(self->handle.gz)) {
          fatal("zlib gzeof error!\n");
        }
      }
      res = 1;
    }
  }
  return res;
}

static int
gzapi_ungetc(const int c, gbfile* self)
{
  if (self->back == -1) {
    self->back = c;
  } else {
    fatal(MYNAME ": Cannot store more than one byte back!\n");
  }
  return c;
}

static void
gzapi_clearerr(gbfile* self)
{
  gzclearerr(self->handle.gz);
}

static int
gzapi_error(gbfile* self)
{
  int errnum;

  (void)gzerror(self->handle.gz, &errnum);

  return errnum;
}
#endif	// #if !ZLIB_INHIBITED


/*******************************************************************************/
/* %%%                         Standard C file api                         %%% */
/*******************************************************************************/

static gbfile*
stdapi_open(gbfile* self, const char* mode)
{
  self->handle.std = xfopen(self->name, mode, self->module);
  return self;
}

static int
stdapi_close(gbfile* self)
{
  return fclose(self->handle.std);
}

static int
stdapi_seek(gbfile* self, int32_t offset, int whence)
{
  gbsize_t pos = 0;

  if (whence != SEEK_SET) {
    pos = ftell(self->handle.std);
  }

  int result = fseek(self->handle.std, offset, whence);
  if (result != 0) {
    switch (whence) {
    case SEEK_CUR:
    case SEEK_END:
      pos = pos + offset;
      break;
    case SEEK_SET:
      pos = offset;
      break;
    default:
      fatal("%s: Unknown seek operation (%d) for file %s!\n",
            self->module, whence, self->name);
    }
    fatal("%s: Unable to set file (%s) to position (%llu)!\n",
          self->module, self->name, (long long unsigned) pos);
  }
  return 0;
}

static gbsize_t
stdapi_read(void* buf, const gbsize_t size, const gbsize_t members, gbfile* self)
{
  int error_number;
  gbsize_t result = fread(buf, size, members, self->handle.std);

  if ((result < members) && (error_number = ferror(self->handle.std))) {
    fatal("%s: Error %d occurred during read of file '%s'!\n",
          self->module, error_number, self->name);
  }
  return result;
}

static gbsize_t
stdapi_write(const void* buf, const gbsize_t size, const gbsize_t members, gbfile* self)
{
  return fwrite(buf, size, members, self->handle.std);
}

static int
stdapi_flush(gbfile* self)
{
  return fflush(self->handle.std);
}

static gbsize_t
stdapi_tell(gbfile* self)
{
  return ftell(self->handle.std);
}

static int
stdapi_eof(gbfile* self)
{
  return feof(self->handle.std);
}

static int
stdapi_ungetc(const int c, gbfile* self)
{
  return ungetc(c, self->handle.std);
}

static void
stdapi_clearerr(gbfile* self)
{
  clearerr(self->handle.std);
}

static int
stdapi_error(gbfile* self)
{
  return ferror(self->handle.std);
}


/*******************************************************************************/
/* %%%                     Memory stream (memapi)                          %%% */
/*******************************************************************************/

static gbfile*
memapi_open(gbfile* self, const char* mode)
{
  (void)mode;

  self->mempos = 0;
  self->memsz = 0;
  self->handle.mem = nullptr;

  return self;
}

static int
memapi_close(gbfile* self)
{
  if (self->handle.mem) {
    xfree(self->handle.mem);
  }

  return 0;
}

static int
memapi_seek(gbfile* self, int32_t offset, int whence)
{
  long long pos = (int)self->mempos;

  switch (whence) {
  case SEEK_CUR:
  case SEEK_END:
    pos = pos + offset;
    break;
  case SEEK_SET:
    pos = offset;
    break;
  }

  if ((pos < 0) || (pos > self->memlen)) {
    return -1;
  }

  self->mempos = pos;
  return 0;
}

static gbsize_t
memapi_read(void* buf, const gbsize_t size, const gbsize_t members, gbfile* self)
{
  gbsize_t result = (self->memlen - self->mempos) / size;

  if (result > members) {
    result = members;
  }
  gbsize_t count = result * size;
  if (count) {
    memcpy(buf, self->handle.mem + self->mempos, count);
    self->mempos += count;
  }

  return result;
}

static gbsize_t
memapi_write(const void* buf, const gbsize_t size, const gbsize_t members, gbfile* self)
{
  if ((size == 0) && (members == 0)) {	/* truncate stream */
    self->memlen = self->mempos;
    return 0;
  }

  gbsize_t count = size * members;

  if (self->mempos + count > self->memsz) {
    self->memsz = ((self->mempos + count + 4095) / 4096) * 4096;
    self->handle.mem = (unsigned char*) xrealloc(self->handle.mem, self->memsz);
  }
  memcpy(self->handle.mem + self->mempos, buf, count);
  self->mempos += count;
  if (self->mempos > self->memlen) {
    self->memlen = self->mempos;
  }

  return members;
}

static int
memapi_flush(gbfile* self)
{
  (void)self;
  return 0;
}

static gbsize_t
memapi_tell(gbfile* self)
{
  return self->mempos;
}

static int
memapi_eof(gbfile* self)
{
  return (self->mempos == self->memlen);
}

static int
memapi_ungetc(const int c, gbfile* self)
{
  if (self->mempos == 0) {
    return EOF;
  } else {
    self->mempos--;
    self->handle.mem[self->mempos] = (unsigned char) c;
    return c;
  }
}

static void
memapi_clearerr(gbfile* self)
{
  (void)self;
}

static int
memapi_error(gbfile* self)
{
  (void)self;
  return 0;
}


/* GPSBabel 'file' standard calls */

/*
 * gbfopen: (as xfopen) plus the name of the calling GPSBabel module (MYNAME)
 */

gbfile*
gbfopen(const QString& filename, const char* mode, const char* module)
{
  auto* file = (gbfile*) xcalloc(1, sizeof(gbfile));

  file->module = xstrdup(module);
  file->mode = 'r'; // default
  file->binary = (strchr(mode, 'b') != nullptr);
  file->back = -1;
  file->memapi = (filename == nullptr);

  for (const char* m = mode; *m; m++) {
    switch (tolower(*m)) {
    case 'r':
      file->mode = 'r';
#if !ZLIB_INHIBITED
      file->gzapi = 1;	/* native or transparent */
#endif
      break;
    case 'w':
      file->mode = 'w';
      break;
    }
  }

  if (file->memapi) {
    file->gzapi = 0;
    file->name = xstrdup("(Memory stream)");

    file->fileclearerr = memapi_clearerr;
    file->fileclose = memapi_close;
    file->fileeof = memapi_eof;
    file->fileerror = memapi_error;
    file->fileflush = memapi_flush;
    file->fileopen = memapi_open;
    file->fileread = memapi_read;
    file->fileseek = memapi_seek;
    file->filetell = memapi_tell;
    file->fileungetc = memapi_ungetc;
    file->filewrite = memapi_write;
  } else {
    file->name = xstrdup(filename);
    file->is_pipe = (filename == "-");

    /* Do we have a '.gz' extension in the filename ? */
    int len = strlen(file->name);
    if ((len > 3) && (case_ignore_strcmp(&file->name[len-3], ".gz") == 0)) {
#if !ZLIB_INHIBITED
      /* force gzipped files on output */
      file->gzapi = 1;
#else
      fatal(NO_ZLIB);
#endif
    }

    if (file->gzapi) {
#if !ZLIB_INHIBITED

      file->fileclearerr = gzapi_clearerr;
      file->fileclose = gzapi_close;
      file->fileeof = gzapi_eof;
      file->fileerror = gzapi_error;
      file->fileflush = gzapi_flush;
      file->fileopen = gzapi_open;
      file->fileread = gzapi_read;
      file->fileseek = gzapi_seek;
      file->filetell = gzapi_tell;
      file->fileungetc = gzapi_ungetc;
      file->filewrite = gzapi_write;
#else
      /* This is the only runtime test we make */
      fatal("%s: Zlib was not included in this build.\n", file->module);
#endif
    } else {
      file->fileclearerr = stdapi_clearerr;
      file->fileclose = stdapi_close;
      file->fileeof = stdapi_eof;
      file->fileerror = stdapi_error;
      file->fileflush = stdapi_flush;
      file->fileopen = stdapi_open;
      file->fileread = stdapi_read;
      file->fileseek = stdapi_seek;
      file->filetell = stdapi_tell;
      file->fileungetc = stdapi_ungetc;
      file->filewrite = stdapi_write;
    }
  }

  file->fileopen(file, mode);

  file->buffsz = 256;
  file->buff = (char*) xmalloc(file->buffsz);

  return file;
}

/*
 * gbfopen_be: as gbfopen, but set the BIG-ENDIAN flag
 */

gbfile*
gbfopen_be(const QString& filename, const char* mode, const char* module)
{
  gbfile* result = gbfopen(filename, mode, module);
  result->big_endian = 1;

  return result;
}

/*
 * gbfclose: (as fclose)
 */

void
gbfclose(gbfile* file)
{
  if (!file) {
    return;
  }

  file->fileclose(file);

  xfree(file->name);
  xfree(file->module);
  xfree(file->buff);
  xfree(file);
}

/*
 * gbfgetc: (as fgetc)
 */

int
gbfgetc(gbfile* file)
{
  unsigned char c;

  /* errors are caught in gbfread */
  if (gbfread(&c, 1, 1, file) == 0) {
    return EOF;
  } else {
    return (unsigned int)c;
  }
}

/*
 * gbfgets: (as fgets)
 */

QString
gbfgets(char* buf, int len, gbfile* file)
{
  char* result = buf;

  while (--len > 0) {
    int c = gbfgetc(file);

    if (c == EOF) {
      break;
    }

    *(unsigned char*)buf = (unsigned char)c;
    buf++;

    if (c == '\r') {
      c = gbfgetc(file);
      if ((c != '\n') && (c != EOF)) {
        gbfungetc(c, file);
      }
      break;
    } else if (c == '\n') {
      break;
    }
  }
  *buf = '\0';

  QString rv(result);
  return rv;

}

/*
 * gbfread: (as fread)
 */

gbsize_t
gbfread(void* buf, const gbsize_t size, const gbsize_t members, gbfile* file)
{
  if ((size == 0) || (members == 0)) {
    return 0;
  }
  return file->fileread(buf, size, members, file);
}
// This probably makes an unnecessary alloc/copy, but keeps the above (kinda

// goofy) calling signature.
gbsize_t
gbfread(QString& buf, const gbsize_t size,
        const gbsize_t members, gbfile* file)
{
  QByteArray tmp;
  tmp.resize(members * size);
  gbsize_t retval = gbfread(tmp.data(), size, members, file);
  buf = QString(tmp);
  return retval;
}

QByteArray gbfreadbuf(gbsize_t size, gbfile* file) {
  QByteArray tmp;
  tmp.resize(size);
  gbsize_t nbytes = gbfread(tmp.data(), 1, size, file);

  if (nbytes != size) {
    fatal(FatalMsg() << file->module << "Attempted to read " << size <<
    "bytes, but only " << nbytes << "were available.");
  }
  return tmp;
}

/*
 * gbvfprintf: (as vfprintf)
 */

int gbvfprintf(gbfile* file, const char* format, va_list ap)
{
  int len;

  for (;;) {
    va_list args;

    va_copy(args, ap);
    len = vsnprintf(file->buff, file->buffsz, format, args);
    va_end(args);

    /* Unambiguous Success */
    if ((len > -1) && (len < file->buffsz)) {
      break;
    }

    /* First case: C99 behaviour.  Len is correctly sized.
     * add space for null terminator.  Next time through the
     * loop we're guaranteed success.
     *
     * Second case: SUS (and Windows) behaviour.  We know it
     * doesn't fit, but we don't know how big it has to be.
    `		 * double it and try again.  We'll loop until we succeed.
     *
     * Since we keep the I/O buffer in the file handle, we
     * quickly reach a steady state on the size of these buffers.
     */
    if (len > -1) {
      file->buffsz = len + 1;
    } else {
      file->buffsz *= 2;
    }

    file->buff = (char*) xrealloc(file->buff, file->buffsz);
  }
  return gbfwrite(file->buff, 1, len, file);
}

/*
 * gbfprintf: (as fprintf)
 */

int
gbfprintf(gbfile* file, const char* format, ...)
{
  va_list args;

  va_start(args, format);
  int result = gbvfprintf(file, format, args);
  va_end(args);

  return result;
}

/*
 * gbfputc: (as fputc)
 */

int
gbfputc(int c, gbfile* file)
{
  unsigned char temp = (unsigned int) c;

  gbfwrite(&temp, 1, 1, file);

  return c;
}

/*
 * gbfputs: (as fputs)
 */

// This is a depressing hack, meant to ease the pain from C strings
// to QStrings, which are consistently encoded.
int
gbfputs(const QString& s, gbfile* file)
{
  QByteArray qs = s.toUtf8();
  int rv =  gbfwrite(qs.constData(), 1, qs.size(), file);
  return rv;
}

/*
 * gbfwrite: (as fwrite)
 */

int
gbfwrite(const void* buf, const gbsize_t size, const gbsize_t members, gbfile* file)
{
  unsigned int result = file->filewrite(buf, size, members, file);
  if (result != members) {
    fatal("%s: Could not write %lld bytes to %s (result %d)!\n",
          file->module,
          (long long int)(members - result) * size,
          file->name,
          result);
  }

  return result;
}

/*
 * gbfflush: (as fflush)
 */

int
gbfflush(gbfile* file)
{
  return file->fileflush(file);
}

/*
 * gbfclearerr: (as clearerr)
 */

void
gbfclearerr(gbfile* file)
{
  file->fileclearerr(file);
}

/*
 * gbferror: (as ferror)
 */

int
gbferror(gbfile* file)
{
  return file->fileerror(file);
}

/*
 * gbfrewind: (as frewind)
 */

void
gbfrewind(gbfile* file)
{
  (void) gbfseek(file, 0, SEEK_SET);
  gbfclearerr(file);
}

/*
 * gbfseek: (as fseek)
 */

int
gbfseek(gbfile* file, int32_t offset, int whence)
{
  return file->fileseek(file, offset, whence);
}

/*
 * gbftell: (as ftell)
 */

gbsize_t
gbftell(gbfile* file)
{
  gbsize_t result = file->filetell(file);
  if ((signed) result == -1)
    fatal("%s: Could not determine position of file '%s'!\n",
          file->module, file->name);
  return result;
}

/*
 * gbfeof: (as feof)
 */

int
gbfeof(gbfile* file)
{
  return file->fileeof(file);
}

/*
 * gbfungetc: (as fungetc)
 */

int
gbfungetc(const int c, gbfile* file)
{
  return file->fileungetc(c, file);
}

/* GPSBabel 'file' enhancements */

/*
 * gbfgetint32: read a signed 32-bit integer from input stream
 */

int32_t
gbfgetint32(gbfile* file)
{
  char buf[4];

  if (gbfread(&buf, 1, sizeof(buf), file) != sizeof(buf)) {
    fatal("%s: Unexpected end of file (%s)!\n", file->module, file->name);
  }

  if (file->big_endian) {
    return be_read32(buf);
  } else {
    return le_read32(buf);
  }
}

/*
 * gbfgetint16: read a signed 16-bit integer from input stream
 */

int16_t
gbfgetint16(gbfile* file)
{
  char buf[2];

  if (gbfread(&buf, 1, sizeof(buf), file) != sizeof(buf)) {
    fatal("%s: Unexpected end of file (%s)!\n", file->module, file->name);
  }

  if (file->big_endian) {
    return be_read16(buf);
  } else {
    return le_read16(buf);
  }
}

/*
 * gbfgetdbl: read a double value (8 byte, double precision) from input stream
 */

double
gbfgetdbl(gbfile* file)
{
  char buf[8];

  if (gbfread(&buf, 1, sizeof(buf), file) != sizeof(buf)) {
    fatal("%s: Unexpected end of file (%s)!\n", file->module, file->name);
  }

  return endian_read_double(buf, ! file->big_endian);
}

/*
 * gbfgetflt: read a float value (4 byte, single precision) from input stream
 */

float
gbfgetflt(gbfile* file)
{
  char buf[4];

  if (gbfread(&buf, 1, sizeof(buf), file) != sizeof(buf)) {
    fatal("%s: Unexpected end of file (%s)!\n", file->module, file->name);
  }

  return endian_read_float(buf, ! file->big_endian);
}

/*
 * gbfgetcstr: Reads a string from file until either a '\0' or eof.
 *             The result is a temporary allocated entity: use it or free it!
 */

char*
gbfgetcstr_old(gbfile* file)
{
  int len = 0;
  char* str = file->buff;

  for (;;) {
    int c = gbfgetc(file);

    if (c == 0) {
      break;
    }

    if (c == EOF) {
      fatal("%s: Unexpected end of file (%s)!\n", file->module, file->name);
    }

    if (len == file->buffsz) {
      file->buffsz += 64;
      str = file->buff = (char*) xrealloc(file->buff, file->buffsz + 1);
    }
    str[len] = c;
    len++;
  }

  char* result = (char*) xmalloc(len + 1);
  if (len > 0) {
    memcpy(result, str, len);
  }
  result[len] = '\0';

  return result;
}

QString
gbfgetcstr(gbfile* file)
{
  char* result = gbfgetcstr_old(file);
  QString rv(result);
  xfree(result);
  return rv;
}

QByteArray
gbfgetnativecstr(gbfile* file)
{
  char* result = gbfgetcstr_old(file);
  QByteArray rv(result);
  xfree(result);
  return rv;
}

/*
 * gbfgetpstr: Reads a pascal short string (first byte is length) from file.
 */

QString
gbfgetpstr(gbfile* file)
{
  int len = gbfgetc(file);
  if (len == EOF) {
    fatal("%s: Unexpected end of file (%s)!\n", file->module, file->name);
  }
  QByteArray ba;
  ba.resize(len);
  if (gbfread(ba.data(), 1, len, file) != (gbsize_t) len) {
    fatal("%s: Unexpected end of file (%s)!\n", file->module, file->name);
  }

  return QString(ba);
}

static QChar
gbfgetutf16char(gbfile* file)
{
    int c0 = gbfgetc(file);
    if (c0 == EOF) {
      return QChar();
    }

    int c1 = gbfgetc(file);

    if (c1 == EOF) {
        fatal("%s: Incomplete unicode (UTF-16%cE) character at EOF!\n",
              file->module,
              file->big_endian ? 'B' : 'L');
    }

    unsigned char cell;
    unsigned char row;
    if (file->big_endian) {
      cell = static_cast<unsigned char>(c1);
      row = static_cast<unsigned char>(c0);
    } else {
      cell = static_cast<unsigned char>(c0);
      row = static_cast<unsigned char>(c1);
    }
    return QChar(cell, row);
}

/*
 * Reads a string from utf16 encoded file.
 * Terminates at EOF or a line ending(\r\n, \n).
 * Line endings are not included in the returned string.
 * Returns a nullptr if at EOF, otherwise it returns a pointer
 * to a possibly empty null terminated utf-8 character array.
 * Fatal errors can occur if:
 * i) the file ends with either an incomplete utf-16 character, or
 * ii) the file ends with an incomplete surrogate pair, or
 * iii) a high surrogate is not followd by a low surrogate, or
 * iv) a low surrogate isn't preceeded by a high surrogate.
 */
static char*
gbfgetutf16str(gbfile* file)
{
  int len = 0;
  char* result = file->buff;

  for (;;) {
    QChar qch = gbfgetutf16char(file);
    if (qch.isNull()) {
      if (len == 0) {
        return nullptr;
      } else {
        break;
      }
    }

    if (qch == u'\r') {
      QChar qch2 = gbfgetutf16char(file);
      if (qch2 != u'\n') {  // including qch2.isNull()
        // Putting back two chars may not be supported, e.g. with gzapi_ungetc.
        fatal("%s: Invalid unicode (UTF-16%cE) line break!\n",
              file->module,
              file->big_endian ? 'B' : 'L');
      }
      break;
    } else if (qch == u'\n') {
      break;
    }

    if (qch.isLowSurrogate()) {
      fatal("%s: Leading unicode (UTF-16%cE) low surrogate!\n",
            file->module,
            file->big_endian ? 'B' : 'L');
    }

    QString str(qch);
    if (qch.isHighSurrogate()) {
      QChar qch2 = gbfgetutf16char(file);
      if (!qch2.isLowSurrogate()) { // including qch2.isNull()
        fatal("%s: Missing unicode (UTF-16%cE) low surrogate!\n",
              file->module,
              file->big_endian ? 'B' : 'L');
      }
      str.append(qch2);
    }

    QByteArray ba = str.toUtf8();
    int clen = ba.size();

    if (len+clen >= file->buffsz) {
      file->buffsz += 64;
      result = file->buff = (char*) xrealloc(file->buff, file->buffsz + 1);
    }
    memcpy(&result[len], ba.constData(), clen);
    len += clen;
  }
  result[len] = '\0';	// terminate resulting string

  return result;
}

/*
 * gbfgetstr: Reads a string from file (util any type of line-breaks or eof or error)
 *            except xfree and free you can do all possible things with the result
 */

char*
gbfgetstr(gbfile* file)
{
  int len = 0;
  char* result = file->buff;

  if (file->unicode) {
    return gbfgetutf16str(file);
  }

  for (;;) {
    int c = gbfgetc(file);

    if ((c == EOF) || (c == 0x1A)) {
      if (len == 0) {
        return nullptr;
      }
      break;
    } else if (c == '\r') {
      c = gbfgetc(file);
      if ((c != '\n') && (c != EOF)) {
        gbfungetc(c, file);
      }
      break;
    } else if (c == '\n') {
      break;
    } else if (((c == 0xFE) || (c == 0xFF)) && (! file->unicode_checked)) {
      int c1 = gbfgetc(file);
      if (c1 != EOF) {
        int cx = c | (c1 << 8);
        if (cx == 0xFEFF) {
          file->unicode = 1;
          file->big_endian = 0;
          return gbfgetutf16str(file);
        } else if (cx == 0xFFFE) {
          file->unicode = 1;
          file->big_endian = 1;
          return gbfgetutf16str(file);
        } else {
          gbfungetc(c1, file);
        }
      }
    }

    file->unicode_checked = 1;

    if ((len + 1) == file->buffsz) {
      file->buffsz += 64;
      result = file->buff = (char*) xrealloc(file->buff, file->buffsz + 1);
    }
    result[len] = (char)c;
    len++;
  }
  result[len] = '\0';	// terminate resulting string

  return result;
}

/*
 * gbfputint16: write a signed 16-bit integer value into output stream
 */

int
gbfputint16(const int16_t i, gbfile* file)
{
  char buf[2];

  if (file->big_endian) {
    be_write16(buf, i);
  } else {
    le_write16(buf, i);
  }
  return gbfwrite(buf, 1, sizeof(buf), file);
}

/*
 * gbfputint32: write a signed 32-bit integer value into output stream
 */

int
gbfputint32(const int32_t i, gbfile* file)
{
  char buf[4];

  if (file->big_endian) {
    be_write32(buf, i);
  } else {
    le_write32(buf, i);
  }
  return gbfwrite(buf, 1, sizeof(buf), file);
}

/*
 * gbfputdbl: write a double value (8 byte, double precision) into output stream
 */

int
gbfputdbl(const double d, gbfile* file)
{
  char buf[8];

  endian_write_double(buf, d, ! file->big_endian);
  return gbfwrite(buf, 1, sizeof(buf), file);
}

/*
 * gbfputflt: write a float value (4 byte, single precision) into output stream
 */

int
gbfputflt(const float f, gbfile* file)
{
  char buf[4];

  endian_write_float(buf, f, ! file->big_endian);
  return gbfwrite(buf, 1, sizeof(buf), file);
}

/*
 * gbfputcstr: write a NULL terminated string into a stream (!) including NULL
 *             return the number of written characters
 */
int
gbfputcstr(const char* s, gbfile* file)
{
  int len = (s == nullptr) ? 0 : strlen(s);
  if (len > 0) {
    return gbfwrite(s, 1, len + 1, file);
  } else {
    gbfputc(0, file);
    return 1;
  }
}

/*
 * gbfputpstr: write a pascal string into a stream
 *             return the number of written characters
 */

int
gbfputpstr(const QString& s, gbfile* file)
{
  QString out(s);
  // Pascal strings can be a max of 255 bytes.
  out.truncate(255);

  gbfputc(out.size(), file);
  QByteArray qs = s.toUtf8();
  int rv =  gbfwrite(qs.constData(), 1, qs.size(), file);
  return rv;
}

/* Much more higher level functions */

gbsize_t
gbfcopyfrom(gbfile* file, gbfile* src, gbsize_t count)
{
  char buf[1024];
  gbsize_t copied = 0;

  while (count) {
    gbsize_t n = gbfread(buf, 1, (count < sizeof(buf)) ? count : sizeof(buf), src);
    if (n > 0) {
      gbfwrite(buf, 1, n, file);
      count -= n;
      copied += n;
    } else {
      break;
    }
  }
  return copied;
}


/* That's all, sorry. */
