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

This program 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 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 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA

See COPYING file for the full LGPL text.

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

#include "quazipfile.h"

using namespace std;

/// The implementation class for QuaZip.
/**
\internal

This class contains all the private stuff for the QuaZipFile class, thus
allowing to preserve binary compatibility between releases, the
technique known as the Pimpl (private implementation) idiom.
*/
class QuaZipFilePrivate
{
    friend class QuaZipFile;
private:
    /// The pointer to the associated QuaZipFile instance.
    QuaZipFile *q;
    /// The QuaZip object to work with.
    QuaZip *zip;
    /// The file name.
    QString fileName;
    /// Case sensitivity mode.
    QuaZip::CaseSensitivity caseSensitivity;
    /// Whether this file is opened in the raw mode.
    bool raw;
    /// Write position to keep track of.
    /**
      QIODevice::pos() is broken for non-seekable devices, so we need
      our own position.
      */
    qint64 writePos;
    /// Uncompressed size to write along with a raw file.
    ulong uncompressedSize;
    /// CRC to write along with a raw file.
    quint32 crc;
    /// Whether \ref zip points to an internal QuaZip instance.
    /**
      This is true if the archive was opened by name, rather than by
      supplying an existing QuaZip instance.
      */
    bool internal;
    /// The last error.
    int zipError;
    /// Resets \ref zipError.
    inline void resetZipError() const
    {
        setZipError(UNZ_OK);
    }
    /// Sets the zip error.
    /**
      This function is marked as const although it changes one field.
      This allows to call it from const functions that don't change
      anything by themselves.
      */
    void setZipError(int zipError) const;
    /// The constructor for the corresponding QuaZipFile constructor.
    inline QuaZipFilePrivate(QuaZipFile *q):
        q(q), zip(NULL), internal(true), zipError(UNZ_OK) {}
    /// The constructor for the corresponding QuaZipFile constructor.
    inline QuaZipFilePrivate(QuaZipFile *q, const QString &zipName):
        q(q), internal(true), zipError(UNZ_OK)
    {
        zip = new QuaZip(zipName);
    }
    /// The constructor for the corresponding QuaZipFile constructor.
    inline QuaZipFilePrivate(QuaZipFile *q, const QString &zipName, const QString &fileName,
                             QuaZip::CaseSensitivity cs):
        q(q), internal(true), zipError(UNZ_OK)
    {
        zip = new QuaZip(zipName);
        this->fileName = fileName;
        if(this->fileName.startsWith('/'))
        {
            this->fileName = this->fileName.mid(1);
        }
        this->caseSensitivity = cs;
    }
    /// The constructor for the QuaZipFile constructor accepting a file name.
    inline QuaZipFilePrivate(QuaZipFile *q, QuaZip *zip):
        q(q), zip(zip), internal(false), zipError(UNZ_OK) {}
    /// The destructor.
    inline ~QuaZipFilePrivate()
    {
        if(internal)
        {
            delete zip;
        }
    }
};

QuaZipFile::QuaZipFile():
    p(new QuaZipFilePrivate(this))
{
}

QuaZipFile::QuaZipFile(QObject *parent):
    QIODevice(parent),
    p(new QuaZipFilePrivate(this))
{
}

QuaZipFile::QuaZipFile(const QString& zipName, QObject *parent):
    QIODevice(parent),
    p(new QuaZipFilePrivate(this, zipName))
{
}

QuaZipFile::QuaZipFile(const QString& zipName, const QString& fileName,
                       QuaZip::CaseSensitivity cs, QObject *parent):
    QIODevice(parent),
    p(new QuaZipFilePrivate(this, zipName, fileName, cs))
{
}

QuaZipFile::QuaZipFile(QuaZip *zip, QObject *parent):
    QIODevice(parent),
    p(new QuaZipFilePrivate(this, zip))
{
}

QuaZipFile::~QuaZipFile()
{
    if(isOpen())
    {
        close();
    }
    delete p;
}

QString QuaZipFile::getZipName() const
{
    return p->zip == NULL ? QString() : p->zip->getZipName();
}

QuaZip *QuaZipFile::getZip() const
{
    return p->internal ? NULL : p->zip;
}

QString QuaZipFile::getActualFileName()const
{
    p->setZipError(UNZ_OK);
    if(p->zip == NULL || (openMode() & WriteOnly))
    {
        return QString();
    }
    QString name = p->zip->getCurrentFileName();
    if(name.isNull())
    {
        p->setZipError(p->zip->getZipError());
    }
    return name;
}

void QuaZipFile::setZipName(const QString& zipName)
{
    if(isOpen())
    {
        qWarning("QuaZipFile::setZipName(): file is already open - can not set ZIP name");
        return;
    }
    if(p->zip != NULL && p->internal)
    {
        delete p->zip;
    }
    p->zip = new QuaZip(zipName);
    p->internal = true;
}

void QuaZipFile::setZip(QuaZip *zip)
{
    if(isOpen())
    {
        qWarning("QuaZipFile::setZip(): file is already open - can not set ZIP");
        return;
    }
    if(p->zip != NULL && p->internal)
    {
        delete p->zip;
    }
    p->zip = zip;
    p->fileName = QString();
    p->internal = false;
}

void QuaZipFile::setFileName(const QString& fileName, QuaZip::CaseSensitivity cs)
{
    if(p->zip == NULL)
    {
        qWarning("QuaZipFile::setFileName(): call setZipName() first");
        return;
    }
    if(!p->internal)
    {
        qWarning("QuaZipFile::setFileName(): should not be used when not using internal QuaZip");
        return;
    }
    if(isOpen())
    {
        qWarning("QuaZipFile::setFileName(): can not set file name for already opened file");
        return;
    }
    p->fileName = fileName;
    if(p->fileName.startsWith('/'))
    {
        p->fileName = p->fileName.mid(1);
    }
    p->caseSensitivity = cs;
}

void QuaZipFilePrivate::setZipError(int zipError) const
{
    QuaZipFilePrivate *fakeThis = const_cast<QuaZipFilePrivate*>(this); // non-const
    fakeThis->zipError = zipError;
    if(zipError == UNZ_OK)
    {
        q->setErrorString(QString());
    }
    else
    {
        q->setErrorString(QString("ZIP/UNZIP API error %1").arg(zipError));
    }
}

bool QuaZipFile::open(OpenMode mode)
{
    return open(mode, NULL);
}

bool QuaZipFile::open(OpenMode mode, int *method, int *level, bool raw, const char *password)
{
    p->resetZipError();
    if(isOpen())
    {
        qWarning("QuaZipFile::open(): already opened");
        return false;
    }
    if(mode & Unbuffered)
    {
        qWarning("QuaZipFile::open(): Unbuffered mode is not supported");
        return false;
    }
    if((mode & ReadOnly) && !(mode & WriteOnly))
    {
        if(p->internal)
        {
            if(!p->zip->open(QuaZip::mdUnzip))
            {
                p->setZipError(p->zip->getZipError());
                return false;
            }
            if(!p->zip->setCurrentFile(p->fileName, p->caseSensitivity))
            {
                p->setZipError(p->zip->getZipError());
                p->zip->close();
                return false;
            }
        }
        else
        {
            if(p->zip == NULL)
            {
                qWarning("QuaZipFile::open(): zip is NULL");
                return false;
            }
            if(p->zip->getMode() != QuaZip::mdUnzip)
            {
                qWarning("QuaZipFile::open(): file open mode %d incompatible with ZIP open mode %d",
                         (int)mode, (int)p->zip->getMode());
                return false;
            }
            if(!p->zip->hasCurrentFile())
            {
                qWarning("QuaZipFile::open(): zip does not have current file");
                return false;
            }
        }
        p->setZipError(unzOpenCurrentFile3(p->zip->getUnzFile(), method, level, (int)raw, password));
        if(p->zipError == UNZ_OK)
        {
            setOpenMode(mode);
            p->raw = raw;
            return true;
        }
        else
        {
            return false;
        }
    }
    qWarning("QuaZipFile::open(): open mode %d not supported by this function", (int)mode);
    return false;
}

bool QuaZipFile::open(OpenMode mode, const QuaZipNewInfo& info,
                      const char *password, quint32 crc,
                      int method, int level, bool raw,
                      int windowBits, int memLevel, int strategy)
{
    zip_fileinfo info_z;
    p->resetZipError();
    if(isOpen())
    {
        qWarning("QuaZipFile::open(): already opened");
        return false;
    }
    if((mode & WriteOnly) && !(mode & ReadOnly))
    {
        if(p->internal)
        {
            qWarning("QuaZipFile::open(): write mode is incompatible with internal QuaZip approach");
            return false;
        }
        if(p->zip == NULL)
        {
            qWarning("QuaZipFile::open(): zip is NULL");
            return false;
        }
        if(p->zip->getMode() != QuaZip::mdCreate && p->zip->getMode() != QuaZip::mdAppend && p->zip->getMode() != QuaZip::mdAdd)
        {
            qWarning("QuaZipFile::open(): file open mode %d incompatible with ZIP open mode %d",
                     (int)mode, (int)p->zip->getMode());
            return false;
        }
        info_z.tmz_date.tm_year = info.dateTime.date().year();
        info_z.tmz_date.tm_mon = info.dateTime.date().month() - 1;
        info_z.tmz_date.tm_mday = info.dateTime.date().day();
        info_z.tmz_date.tm_hour = info.dateTime.time().hour();
        info_z.tmz_date.tm_min = info.dateTime.time().minute();
        info_z.tmz_date.tm_sec = info.dateTime.time().second();
        info_z.dosDate = 0;
        info_z.internal_fa = (uLong)info.internalAttr;
        info_z.external_fa = (uLong)info.externalAttr;
        if(!p->zip->isDataDescriptorWritingEnabled())
        {
            zipClearFlags(p->zip->getZipFile(), ZIP_WRITE_DATA_DESCRIPTOR);
        }
        p->setZipError(zipOpenNewFileInZip3(p->zip->getZipFile(),
                                            p->zip->getFileNameCodec()->fromUnicode(info.name).constData(), &info_z,
                                            info.extraLocal.constData(), info.extraLocal.length(),
                                            info.extraGlobal.constData(), info.extraGlobal.length(),
                                            p->zip->getCommentCodec()->fromUnicode(info.comment).constData(),
                                            method, level, (int)raw,
                                            windowBits, memLevel, strategy,
                                            password, (uLong)crc));
        if(p->zipError == UNZ_OK)
        {
            p->writePos = 0;
            setOpenMode(mode);
            p->raw = raw;
            if(raw)
            {
                p->crc = crc;
                p->uncompressedSize = info.uncompressedSize;
            }
            return true;
        }
        else
        {
            return false;
        }
    }
    qWarning("QuaZipFile::open(): open mode %d not supported by this function", (int)mode);
    return false;
}

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

qint64 QuaZipFile::pos()const
{
    if(p->zip == NULL)
    {
        qWarning("QuaZipFile::pos(): call setZipName() or setZip() first");
        return -1;
    }
    if(!isOpen())
    {
        qWarning("QuaZipFile::pos(): file is not open");
        return -1;
    }
    if(openMode()&ReadOnly)
        // QIODevice::pos() is broken for sequential devices,
        // but thankfully bytesAvailable() returns the number of
        // bytes buffered, so we know how far ahead we are.
    {
        return unztell(p->zip->getUnzFile()) - QIODevice::bytesAvailable();
    }
    else
    {
        return p->writePos;
    }
}

bool QuaZipFile::atEnd()const
{
    if(p->zip == NULL)
    {
        qWarning("QuaZipFile::atEnd(): call setZipName() or setZip() first");
        return false;
    }
    if(!isOpen())
    {
        qWarning("QuaZipFile::atEnd(): file is not open");
        return false;
    }
    if(openMode()&ReadOnly)
        // the same problem as with pos()
        return QIODevice::bytesAvailable() == 0
               && unzeof(p->zip->getUnzFile()) == 1;
    else
    {
        return true;
    }
}

qint64 QuaZipFile::size()const
{
    if(!isOpen())
    {
        qWarning("QuaZipFile::atEnd(): file is not open");
        return -1;
    }
    if(openMode()&ReadOnly)
    {
        return p->raw ? csize() : usize();
    }
    else
    {
        return p->writePos;
    }
}

qint64 QuaZipFile::csize()const
{
    unz_file_info info_z;
    p->setZipError(UNZ_OK);
    if(p->zip == NULL || p->zip->getMode() != QuaZip::mdUnzip)
    {
        return -1;
    }
    p->setZipError(unzGetCurrentFileInfo(p->zip->getUnzFile(), &info_z, NULL, 0, NULL, 0, NULL, 0));
    if(p->zipError != UNZ_OK)
    {
        return -1;
    }
    return info_z.compressed_size;
}

qint64 QuaZipFile::usize()const
{
    unz_file_info info_z;
    p->setZipError(UNZ_OK);
    if(p->zip == NULL || p->zip->getMode() != QuaZip::mdUnzip)
    {
        return -1;
    }
    p->setZipError(unzGetCurrentFileInfo(p->zip->getUnzFile(), &info_z, NULL, 0, NULL, 0, NULL, 0));
    if(p->zipError != UNZ_OK)
    {
        return -1;
    }
    return info_z.uncompressed_size;
}

bool QuaZipFile::getFileInfo(QuaZipFileInfo *info)
{
    if(p->zip == NULL || p->zip->getMode() != QuaZip::mdUnzip)
    {
        return false;
    }
    p->zip->getCurrentFileInfo(info);
    p->setZipError(p->zip->getZipError());
    return p->zipError == UNZ_OK;
}

void QuaZipFile::close()
{
    p->resetZipError();
    if(p->zip == NULL || !p->zip->isOpen())
    {
        return;
    }
    if(!isOpen())
    {
        qWarning("QuaZipFile::close(): file isn't open");
        return;
    }
    if(openMode()&ReadOnly)
    {
        p->setZipError(unzCloseCurrentFile(p->zip->getUnzFile()));
    }
    else if(openMode()&WriteOnly)
        if(isRaw())
        {
            p->setZipError(zipCloseFileInZipRaw(p->zip->getZipFile(), p->uncompressedSize, p->crc));
        }
        else
        {
            p->setZipError(zipCloseFileInZip(p->zip->getZipFile()));
        }
    else
    {
        qWarning("Wrong open mode: %d", (int)openMode());
        return;
    }
    if(p->zipError == UNZ_OK)
    {
        setOpenMode(QIODevice::NotOpen);
    }
    else
    {
        return;
    }
    if(p->internal)
    {
        p->zip->close();
        p->setZipError(p->zip->getZipError());
    }
}

qint64 QuaZipFile::readData(char *data, qint64 maxSize)
{
    p->setZipError(UNZ_OK);
    qint64 bytesRead = unzReadCurrentFile(p->zip->getUnzFile(), data, (unsigned)maxSize);
    if(bytesRead < 0)
    {
        p->setZipError((int) bytesRead);
        return -1;
    }
    return bytesRead;
}

qint64 QuaZipFile::writeData(const char* data, qint64 maxSize)
{
    p->setZipError(ZIP_OK);
    p->setZipError(zipWriteInFileInZip(p->zip->getZipFile(), data, (unsigned int)maxSize));
    if(p->zipError != ZIP_OK)
    {
        return -1;
    }
    else
    {
        p->writePos += maxSize;
        return maxSize;
    }
}

QString QuaZipFile::getFileName() const
{
    return p->fileName;
}

QuaZip::CaseSensitivity QuaZipFile::getCaseSensitivity() const
{
    return p->caseSensitivity;
}

bool QuaZipFile::isRaw() const
{
    return p->raw;
}

int QuaZipFile::getZipError() const
{
    return p->zipError;
}

qint64 QuaZipFile::bytesAvailable() const
{
    return size() - pos();
}
