/*
Copyright (C) 2005-2014 Sergey A. Tachenov

This file is part of QuaZIP.

QuaZIP 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.

QuaZIP 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 QuaZIP.  If not, see <http://www.gnu.org/licenses/>.

See COPYING file for the full LGPL text.

Original ZIP package is copyrighted by Gilles Vollant and contributors,
see quazip/(un)zip.h files for details. Basically it's the zlib license.
*/

#include "quaziodevice.h"

#define QUAZIO_INBUFSIZE 4096
#define QUAZIO_OUTBUFSIZE 4096

/// \cond internal
class QuaZIODevicePrivate {
    friend class QuaZIODevice;
    QuaZIODevicePrivate(QIODevice *io);
    ~QuaZIODevicePrivate();
    QIODevice *io;
    z_stream zins;
    z_stream zouts;
    char *inBuf;
    int inBufPos;
    int inBufSize;
    char *outBuf;
    int outBufPos;
    int outBufSize;
    bool zBufError;
    int doFlush(QString &error);
};

QuaZIODevicePrivate::QuaZIODevicePrivate(QIODevice *io):
  io(io),
  inBuf(NULL),
  inBufPos(0),
  inBufSize(0),
  outBuf(NULL),
  outBufPos(0),
  outBufSize(0),
  zBufError(false)
{
  zins.zalloc = (alloc_func) NULL;
  zins.zfree = (free_func) NULL;
  zins.opaque = NULL;
  zouts.zalloc = (alloc_func) NULL;
  zouts.zfree = (free_func) NULL;
  zouts.opaque = NULL;
  inBuf = new char[QUAZIO_INBUFSIZE];
  outBuf = new char[QUAZIO_OUTBUFSIZE];
#ifdef QUAZIP_ZIODEVICE_DEBUG_OUTPUT
  debug.setFileName("debug.out");
  debug.open(QIODevice::WriteOnly);
#endif
#ifdef QUAZIP_ZIODEVICE_DEBUG_INPUT
  indebug.setFileName("debug.in");
  indebug.open(QIODevice::WriteOnly);
#endif
}

QuaZIODevicePrivate::~QuaZIODevicePrivate()
{
#ifdef QUAZIP_ZIODEVICE_DEBUG_OUTPUT
  debug.close();
#endif
#ifdef QUAZIP_ZIODEVICE_DEBUG_INPUT
  indebug.close();
#endif
  if (inBuf != NULL)
    delete[] inBuf;
  if (outBuf != NULL)
    delete[] outBuf;
}

int QuaZIODevicePrivate::doFlush(QString &error)
{
  int flushed = 0;
  while (outBufPos < outBufSize) {
    int more = io->write(outBuf + outBufPos, outBufSize - outBufPos);
    if (more == -1) {
      error = io->errorString();
      return -1;
    }
    if (more == 0)
      break;
    outBufPos += more;
    flushed += more;
  }
  if (outBufPos == outBufSize) {
    outBufPos = outBufSize = 0;
  }
  return flushed;
}

/// \endcond

// #define QUAZIP_ZIODEVICE_DEBUG_OUTPUT
// #define QUAZIP_ZIODEVICE_DEBUG_INPUT
#ifdef QUAZIP_ZIODEVICE_DEBUG_OUTPUT
#include <QFile>
static QFile debug;
#endif
#ifdef QUAZIP_ZIODEVICE_DEBUG_INPUT
#include <QFile>
static QFile indebug;
#endif

QuaZIODevice::QuaZIODevice(QIODevice *io, QObject *parent):
    QIODevice(parent),
    d(new QuaZIODevicePrivate(io))
{
  connect(io, SIGNAL(readyRead()), SIGNAL(readyRead()));
}

QuaZIODevice::~QuaZIODevice()
{
    if (isOpen())
        close();
    delete d;
}

QIODevice *QuaZIODevice::getIoDevice() const
{
    return d->io;
}

bool QuaZIODevice::open(QIODevice::OpenMode mode)
{
    if ((mode & QIODevice::Append) != 0) {
        setErrorString(trUtf8("QIODevice::Append is not supported for"
                    " QuaZIODevice"));
        return false;
    }
    if ((mode & QIODevice::ReadWrite) == QIODevice::ReadWrite) {
        setErrorString(trUtf8("QIODevice::ReadWrite is not supported for"
                    " QuaZIODevice"));
        return false;
    }
    if ((mode & QIODevice::ReadOnly) != 0) {
        if (inflateInit(&d->zins) != Z_OK) {
            setErrorString(d->zins.msg);
            return false;
        }
    }
    if ((mode & QIODevice::WriteOnly) != 0) {
        if (deflateInit(&d->zouts, Z_DEFAULT_COMPRESSION) != Z_OK) {
            setErrorString(d->zouts.msg);
            return false;
        }
    }
    return QIODevice::open(mode);
}

void QuaZIODevice::close()
{
    if ((openMode() & QIODevice::ReadOnly) != 0) {
        if (inflateEnd(&d->zins) != Z_OK) {
            setErrorString(d->zins.msg);
        }
    }
    if ((openMode() & QIODevice::WriteOnly) != 0) {
        flush();
        if (deflateEnd(&d->zouts) != Z_OK) {
            setErrorString(d->zouts.msg);
        }
    }
    QIODevice::close();
}

qint64 QuaZIODevice::readData(char *data, qint64 maxSize)
{
  int read = 0;
  while (read < maxSize) {
    if (d->inBufPos == d->inBufSize) {
      d->inBufPos = 0;
      d->inBufSize = d->io->read(d->inBuf, QUAZIO_INBUFSIZE);
      if (d->inBufSize == -1) {
        d->inBufSize = 0;
        setErrorString(d->io->errorString());
        return -1;
      }
      if (d->inBufSize == 0)
        break;
    }
    while (read < maxSize && d->inBufPos < d->inBufSize) {
      d->zins.next_in = (Bytef *) (d->inBuf + d->inBufPos);
      d->zins.avail_in = d->inBufSize - d->inBufPos;
      d->zins.next_out = (Bytef *) (data + read);
      d->zins.avail_out = (uInt) (maxSize - read); // hope it's less than 2GB
      int more = 0;
      switch (inflate(&d->zins, Z_SYNC_FLUSH)) {
      case Z_OK:
        read = (char *) d->zins.next_out - data;
        d->inBufPos = (char *) d->zins.next_in - d->inBuf;
        break;
      case Z_STREAM_END:
        read = (char *) d->zins.next_out - data;
        d->inBufPos = (char *) d->zins.next_in - d->inBuf;
        return read;
      case Z_BUF_ERROR: // this should never happen, but just in case
        if (!d->zBufError) {
          qWarning("Z_BUF_ERROR detected with %d/%d in/out, weird",
              d->zins.avail_in, d->zins.avail_out);
          d->zBufError = true;
        }
        memmove(d->inBuf, d->inBuf + d->inBufPos, d->inBufSize - d->inBufPos);
        d->inBufSize -= d->inBufPos;
        d->inBufPos = 0;
        more = d->io->read(d->inBuf + d->inBufSize, QUAZIO_INBUFSIZE - d->inBufSize);
        if (more == -1) {
          setErrorString(d->io->errorString());
          return -1;
        }
        if (more == 0)
          return read;
        d->inBufSize += more;
        break;
      default:
        setErrorString(QString::fromLocal8Bit(d->zins.msg));
        return -1;
      }
    }
  }
#ifdef QUAZIP_ZIODEVICE_DEBUG_INPUT
  indebug.write(data, read);
#endif
  return read;
}

qint64 QuaZIODevice::writeData(const char *data, qint64 maxSize)
{
  int written = 0;
  QString error;
  if (d->doFlush(error) == -1) {
    setErrorString(error);
    return -1;
  }
  while (written < maxSize) {
      // there is some data waiting in the output buffer
    if (d->outBufPos < d->outBufSize)
      return written;
    d->zouts.next_in = (Bytef *) (data + written);
    d->zouts.avail_in = (uInt) (maxSize - written); // hope it's less than 2GB
    d->zouts.next_out = (Bytef *) d->outBuf;
    d->zouts.avail_out = QUAZIO_OUTBUFSIZE;
    switch (deflate(&d->zouts, Z_NO_FLUSH)) {
    case Z_OK:
      written = (char *) d->zouts.next_in - data;
      d->outBufSize = (char *) d->zouts.next_out - d->outBuf;
      break;
    default:
      setErrorString(QString::fromLocal8Bit(d->zouts.msg));
      return -1;
    }
    if (d->doFlush(error) == -1) {
      setErrorString(error);
      return -1;
    }
  }
#ifdef QUAZIP_ZIODEVICE_DEBUG_OUTPUT
  debug.write(data, written);
#endif
  return written;
}

bool QuaZIODevice::flush()
{
    QString error;
    if (d->doFlush(error) < 0) {
        setErrorString(error);
        return false;
    }
    // can't flush buffer, some data is still waiting
    if (d->outBufPos < d->outBufSize)
        return true;
    Bytef c = 0;
    d->zouts.next_in = &c; // fake input buffer
    d->zouts.avail_in = 0; // of zero size
    do {
        d->zouts.next_out = (Bytef *) d->outBuf;
        d->zouts.avail_out = QUAZIO_OUTBUFSIZE;
        switch (deflate(&d->zouts, Z_SYNC_FLUSH)) {
        case Z_OK:
          d->outBufSize = (char *) d->zouts.next_out - d->outBuf;
          if (d->doFlush(error) < 0) {
              setErrorString(error);
              return false;
          }
          if (d->outBufPos < d->outBufSize)
              return true;
          break;
        case Z_BUF_ERROR: // nothing to write?
          return true;
        default:
          setErrorString(QString::fromLocal8Bit(d->zouts.msg));
          return false;
        }
    } while (d->zouts.avail_out == 0);
    return true;
}

bool QuaZIODevice::isSequential() const
{
  return true;
}
