/****************************************************************************
**
** Copyright (C) 2015 The Qt Company Ltd.
** Contact: http://www.qt.io/licensing/
**
** This file is part of the WBMP plugin in the Qt ImageFormats module.
**
** $QT_BEGIN_LICENSE:LGPL21$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see http://www.qt.io/terms-conditions. For further
** information use the contact form at http://www.qt.io/contact-us.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 2.1 or version 3 as published by the Free
** Software Foundation and appearing in the file LICENSE.LGPLv21 and
** LICENSE.LGPLv3 included in the packaging of this file. Please review the
** following information to ensure the GNU Lesser General Public License
** requirements will be met: https://www.gnu.org/licenses/lgpl.html and
** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
**
** As a special exception, The Qt Company gives you certain additional
** rights. These rights are described in The Qt Company LGPL Exception
** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
**
** $QT_END_LICENSE$
**
****************************************************************************/

#include "qwbmphandler_p.h"

/*!
    \class QWbmpHandler
    \since 5.0
    \brief The QWbmpHandler class provides support for the WBMP image format.
    \internal
*/

#include <qimage.h>
#include <qvariant.h>

QT_BEGIN_NAMESPACE

// This struct represents header of WBMP image file
struct WBMPHeader
{
    quint8 type;              // Type of WBMP image (always equal to 0)
    quint8 format;            // Format of WBMP image
    quint32 width;            // Width of the image already decoded from multibyte integer
    quint32 height;           // Height of the image already decoded from multibyte integer
};
#define WBMPFIXEDHEADER_SIZE 2

// Data renderers and writers which takes care of data alignment endiness and stuff
static bool readMultiByteInt(QIODevice *iodev, quint32 *num)
{
    quint32 res = 0;

    quint8 c;
    unsigned int count = 0;
    do {
        // Do not allow to read longer
        // then we can store in num
        if (++count > sizeof(*num))
            return false;

        if (!iodev->getChar(reinterpret_cast<char *>(&c)))
            return false;

        res = (res << 7) | (c & 0x7F);

    } while (c & 0x80);

    *num = res;
    return true;
}

static bool writeMultiByteInt(QIODevice *iodev, quint32 num)
{
    quint64 tmp = num & 0x7F;
    num >>= 7;

    while (num) {
        quint8 c = num & 0x7F;
        num = num >> 7;
        tmp = (tmp << 8) | (c | 0x80);
    }

    while (tmp) {
        quint8 c = tmp & 0xFF;
        if (!iodev->putChar(c))
            return false;
        tmp >>= 8;
    }
    return true;
}

static bool readWBMPHeader(QIODevice *iodev, WBMPHeader *hdr)
{
    if (!iodev)
        return false;

    uchar tmp[WBMPFIXEDHEADER_SIZE];
    if (iodev->read(reinterpret_cast<char *>(tmp), WBMPFIXEDHEADER_SIZE) == WBMPFIXEDHEADER_SIZE) {
        hdr->type = tmp[0];
        hdr->format = tmp[1];
    } else {
        return false;
    }

    if (readMultiByteInt(iodev, &hdr->width)
        && readMultiByteInt(iodev, &hdr->height)) {
        return true;
    }
    return false;
}

static bool writeWBMPHeader(QIODevice *iodev, const WBMPHeader &hdr)
{
    if (iodev) {
        uchar tmp[WBMPFIXEDHEADER_SIZE];
        tmp[0] = hdr.type;
        tmp[1] = hdr.format;
        if (iodev->write(reinterpret_cast<char *>(tmp),  WBMPFIXEDHEADER_SIZE) != WBMPFIXEDHEADER_SIZE)
            return false;

        if (writeMultiByteInt(iodev, hdr.width) &&
            writeMultiByteInt(iodev, hdr.height))
            return true;
    }
    return false;
}

static bool writeWBMPData(QIODevice *iodev, const QImage &image)
{
    if (iodev) {
        int h = image.height();
        int bpl = (image.width() + 7) / 8;

        for (int l=0; l<h; l++) {
            if (iodev->write(reinterpret_cast<const char *>(image.constScanLine(l)), bpl) != bpl)
                return false;
        }
        return true;
    }
    return false;
}

static bool readWBMPData(QIODevice *iodev, QImage &image)
{
    if (iodev) {
        int h = image.height();
        int bpl = (image.width() + 7) / 8;

        for (int l = 0; l < h; l++) {
            if (iodev->read(reinterpret_cast<char *>(image.scanLine(l)), bpl) != bpl)
                return false;
        }
        return true;
    }
    return false;
}

class WBMPReader
{
public:
    WBMPReader(QIODevice *iodevice);

    QImage readImage();
    bool writeImage(QImage image);

    static bool canRead(QIODevice *iodevice);

private:
    QIODevice *iodev;
    WBMPHeader hdr;
};

// WBMP common reader and writer implementation
WBMPReader::WBMPReader(QIODevice *iodevice) : iodev(iodevice)
{
    memset(&hdr, 0, sizeof(hdr));
}

QImage WBMPReader::readImage()
{
    if (!readWBMPHeader(iodev, &hdr))
        return QImage();

    QImage image(hdr.width, hdr.height, QImage::Format_Mono);
    if (!readWBMPData(iodev, image))
        return QImage();

    return image;
}

bool WBMPReader::writeImage(QImage image)
{
    if (image.format() != QImage::Format_Mono)
        image = image.convertToFormat(QImage::Format_Mono);

    if (image.colorTable().at(0) == image.colorTable().at(1)) {
        // degenerate image: actually blank.
        image.fill((qGray(image.colorTable().at(0)) < 128) ? 0 : 1);
    } else if (qGray(image.colorTable().at(0)) > qGray(image.colorTable().at(1))) {
        // Conform to WBMP's convention about black and white
        image.invertPixels();
    }

    hdr.type = 0;
    hdr.format = 0;
    hdr.width = image.width();
    hdr.height = image.height();

    if (!writeWBMPHeader(iodev, hdr))
        return false;

    if (!writeWBMPData(iodev, image))
        return false;

    return true;
}

bool WBMPReader::canRead(QIODevice *device)
{
    if (device) {

        if (device->isSequential())
            return false;

        // Save previous position
        qint64 oldPos = device->pos();

        WBMPHeader hdr;
        if (readWBMPHeader(device, &hdr)) {
            if ((hdr.type == 0) && (hdr.format == 0)) {
                qint64 imageSize = hdr.height * ((hdr.width + 7) / 8);
                qint64 available = device->bytesAvailable();
                device->seek(oldPos);
                return (imageSize == available);
            }
        }
        device->seek(oldPos);
    }
    return false;
}

/*!
    Constructs an instance of QWbmpHandler initialized to use \a device.
*/
QWbmpHandler::QWbmpHandler(QIODevice *device) :
    m_reader(new WBMPReader(device))
{
}

/*!
    Destructor for QWbmpHandler.
*/
QWbmpHandler::~QWbmpHandler()
{
    delete m_reader;
}

/*!
 * Verifies if some values (magic bytes) are set as expected in the header of the file.
 * If the magic bytes were found, it is assumed that the QWbmpHandler can read the file.
 */
bool QWbmpHandler::canRead() const
{
    bool bCanRead = false;

    QIODevice *device = QImageIOHandler::device();
    if (device) {
        bCanRead = QWbmpHandler::canRead(device);
        if (bCanRead)
            setFormat("wbmp");

    } else {
        qWarning("QWbmpHandler::canRead() called with no device");
    }

    return bCanRead;
}

/*! \reimp
*/
bool QWbmpHandler::read(QImage *image)
{
    bool bSuccess = false;
    QImage img = m_reader->readImage();

    if (!img.isNull()) {
        bSuccess = true;
        *image = img;
    }

    return bSuccess;
}

/*! \reimp
*/
bool QWbmpHandler::write(const QImage &image)
{
    if (image.isNull())
        return false;

    return m_reader->writeImage(image);
}

/*!
    Only Size option is supported
*/
QVariant QWbmpHandler::option(ImageOption option) const
{
    if (option == QImageIOHandler::Size) {
        QIODevice *device = QImageIOHandler::device();
        if (device->isSequential())
            return QVariant();

        // Save old position
        qint64 oldPos = device->pos();

        WBMPHeader hdr;
        if (readWBMPHeader(device, &hdr)) {
            device->seek(oldPos);
            return QSize(hdr.width, hdr.height);
        }

        device->seek(oldPos);

    } else if (option == QImageIOHandler::ImageFormat) {
        return QVariant(QImage::Format_Mono);
    }

    return QVariant();
}

bool QWbmpHandler::supportsOption(ImageOption option) const
{
    return (option == QImageIOHandler::Size) ||
           (option == QImageIOHandler::ImageFormat);
}

bool QWbmpHandler::canRead(QIODevice *device)
{
    return WBMPReader::canRead(device);
}

QT_END_NAMESPACE
