// Copyright (C) 2016 The Qt Company Ltd.
// Copyright (C) 2016 Alex Char.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only

#include "qicnshandler_p.h"

#include <QtCore/qmath.h>
#include <QtCore/qendian.h>
#include <QtCore/qregularexpression.h>
#include <QtCore/qbuffer.h>
#include <QtGui/qimage.h>

#ifndef QT_NO_DATASTREAM

QT_BEGIN_NAMESPACE

static const quint8 ICNSBlockHeaderSize = 8;

static const QRgb ICNSColorTableMono[] = {
    qRgb(0xFF, 0xFF, 0xFF),
    qRgb(0x00, 0x00, 0x00)
};
Q_STATIC_ASSERT(sizeof(ICNSColorTableMono) / sizeof(ICNSColorTableMono[0]) == (1 << ICNSEntry::DepthMono));

static const QRgb ICNSColorTable4bit[] = {
    qRgb(0xFF, 0xFF, 0xFF),
    qRgb(0xFC, 0xF3, 0x05),
    qRgb(0xFF, 0x64, 0x02),
    qRgb(0xDD, 0x08, 0x06),
    qRgb(0xF2, 0x08, 0x84),
    qRgb(0x46, 0x00, 0xA5),
    qRgb(0x00, 0x00, 0xD4),
    qRgb(0x02, 0xAB, 0xEA),
    qRgb(0x1F, 0xB7, 0x14),
    qRgb(0x00, 0x64, 0x11),
    qRgb(0x56, 0x2C, 0x05),
    qRgb(0x90, 0x71, 0x3A),
    qRgb(0xC0, 0xC0, 0xC0),
    qRgb(0x80, 0x80, 0x80),
    qRgb(0x40, 0x40, 0x40),
    qRgb(0x00, 0x00, 0x00)
};
Q_STATIC_ASSERT(sizeof(ICNSColorTable4bit) / sizeof(ICNSColorTable4bit[0]) == (1 << ICNSEntry::Depth4bit));

static const QRgb ICNSColorTable8bit[] = {
    qRgb(0xFF, 0xFF, 0xFF),
    qRgb(0xFF, 0xFF, 0xCC),
    qRgb(0xFF, 0xFF, 0x99),
    qRgb(0xFF, 0xFF, 0x66),
    qRgb(0xFF, 0xFF, 0x33),
    qRgb(0xFF, 0xFF, 0x00),
    qRgb(0xFF, 0xCC, 0xFF),
    qRgb(0xFF, 0xCC, 0xCC),
    qRgb(0xFF, 0xCC, 0x99),
    qRgb(0xFF, 0xCC, 0x66),
    qRgb(0xFF, 0xCC, 0x33),
    qRgb(0xFF, 0xCC, 0x00),
    qRgb(0xFF, 0x99, 0xFF),
    qRgb(0xFF, 0x99, 0xCC),
    qRgb(0xFF, 0x99, 0x99),
    qRgb(0xFF, 0x99, 0x66),
    qRgb(0xFF, 0x99, 0x33),
    qRgb(0xFF, 0x99, 0x00),
    qRgb(0xFF, 0x66, 0xFF),
    qRgb(0xFF, 0x66, 0xCC),
    qRgb(0xFF, 0x66, 0x99),
    qRgb(0xFF, 0x66, 0x66),
    qRgb(0xFF, 0x66, 0x33),
    qRgb(0xFF, 0x66, 0x00),
    qRgb(0xFF, 0x33, 0xFF),
    qRgb(0xFF, 0x33, 0xCC),
    qRgb(0xFF, 0x33, 0x99),
    qRgb(0xFF, 0x33, 0x66),
    qRgb(0xFF, 0x33, 0x33),
    qRgb(0xFF, 0x33, 0x00),
    qRgb(0xFF, 0x00, 0xFF),
    qRgb(0xFF, 0x00, 0xCC),
    qRgb(0xFF, 0x00, 0x99),
    qRgb(0xFF, 0x00, 0x66),
    qRgb(0xFF, 0x00, 0x33),
    qRgb(0xFF, 0x00, 0x00),
    qRgb(0xCC, 0xFF, 0xFF),
    qRgb(0xCC, 0xFF, 0xCC),
    qRgb(0xCC, 0xFF, 0x99),
    qRgb(0xCC, 0xFF, 0x66),
    qRgb(0xCC, 0xFF, 0x33),
    qRgb(0xCC, 0xFF, 0x00),
    qRgb(0xCC, 0xCC, 0xFF),
    qRgb(0xCC, 0xCC, 0xCC),
    qRgb(0xCC, 0xCC, 0x99),
    qRgb(0xCC, 0xCC, 0x66),
    qRgb(0xCC, 0xCC, 0x33),
    qRgb(0xCC, 0xCC, 0x00),
    qRgb(0xCC, 0x99, 0xFF),
    qRgb(0xCC, 0x99, 0xCC),
    qRgb(0xCC, 0x99, 0x99),
    qRgb(0xCC, 0x99, 0x66),
    qRgb(0xCC, 0x99, 0x33),
    qRgb(0xCC, 0x99, 0x00),
    qRgb(0xCC, 0x66, 0xFF),
    qRgb(0xCC, 0x66, 0xCC),
    qRgb(0xCC, 0x66, 0x99),
    qRgb(0xCC, 0x66, 0x66),
    qRgb(0xCC, 0x66, 0x33),
    qRgb(0xCC, 0x66, 0x00),
    qRgb(0xCC, 0x33, 0xFF),
    qRgb(0xCC, 0x33, 0xCC),
    qRgb(0xCC, 0x33, 0x99),
    qRgb(0xCC, 0x33, 0x66),
    qRgb(0xCC, 0x33, 0x33),
    qRgb(0xCC, 0x33, 0x00),
    qRgb(0xCC, 0x00, 0xFF),
    qRgb(0xCC, 0x00, 0xCC),
    qRgb(0xCC, 0x00, 0x99),
    qRgb(0xCC, 0x00, 0x66),
    qRgb(0xCC, 0x00, 0x33),
    qRgb(0xCC, 0x00, 0x00),
    qRgb(0x99, 0xFF, 0xFF),
    qRgb(0x99, 0xFF, 0xCC),
    qRgb(0x99, 0xFF, 0x99),
    qRgb(0x99, 0xFF, 0x66),
    qRgb(0x99, 0xFF, 0x33),
    qRgb(0x99, 0xFF, 0x00),
    qRgb(0x99, 0xCC, 0xFF),
    qRgb(0x99, 0xCC, 0xCC),
    qRgb(0x99, 0xCC, 0x99),
    qRgb(0x99, 0xCC, 0x66),
    qRgb(0x99, 0xCC, 0x33),
    qRgb(0x99, 0xCC, 0x00),
    qRgb(0x99, 0x99, 0xFF),
    qRgb(0x99, 0x99, 0xCC),
    qRgb(0x99, 0x99, 0x99),
    qRgb(0x99, 0x99, 0x66),
    qRgb(0x99, 0x99, 0x33),
    qRgb(0x99, 0x99, 0x00),
    qRgb(0x99, 0x66, 0xFF),
    qRgb(0x99, 0x66, 0xCC),
    qRgb(0x99, 0x66, 0x99),
    qRgb(0x99, 0x66, 0x66),
    qRgb(0x99, 0x66, 0x33),
    qRgb(0x99, 0x66, 0x00),
    qRgb(0x99, 0x33, 0xFF),
    qRgb(0x99, 0x33, 0xCC),
    qRgb(0x99, 0x33, 0x99),
    qRgb(0x99, 0x33, 0x66),
    qRgb(0x99, 0x33, 0x33),
    qRgb(0x99, 0x33, 0x00),
    qRgb(0x99, 0x00, 0xFF),
    qRgb(0x99, 0x00, 0xCC),
    qRgb(0x99, 0x00, 0x99),
    qRgb(0x99, 0x00, 0x66),
    qRgb(0x99, 0x00, 0x33),
    qRgb(0x99, 0x00, 0x00),
    qRgb(0x66, 0xFF, 0xFF),
    qRgb(0x66, 0xFF, 0xCC),
    qRgb(0x66, 0xFF, 0x99),
    qRgb(0x66, 0xFF, 0x66),
    qRgb(0x66, 0xFF, 0x33),
    qRgb(0x66, 0xFF, 0x00),
    qRgb(0x66, 0xCC, 0xFF),
    qRgb(0x66, 0xCC, 0xCC),
    qRgb(0x66, 0xCC, 0x99),
    qRgb(0x66, 0xCC, 0x66),
    qRgb(0x66, 0xCC, 0x33),
    qRgb(0x66, 0xCC, 0x00),
    qRgb(0x66, 0x99, 0xFF),
    qRgb(0x66, 0x99, 0xCC),
    qRgb(0x66, 0x99, 0x99),
    qRgb(0x66, 0x99, 0x66),
    qRgb(0x66, 0x99, 0x33),
    qRgb(0x66, 0x99, 0x00),
    qRgb(0x66, 0x66, 0xFF),
    qRgb(0x66, 0x66, 0xCC),
    qRgb(0x66, 0x66, 0x99),
    qRgb(0x66, 0x66, 0x66),
    qRgb(0x66, 0x66, 0x33),
    qRgb(0x66, 0x66, 0x00),
    qRgb(0x66, 0x33, 0xFF),
    qRgb(0x66, 0x33, 0xCC),
    qRgb(0x66, 0x33, 0x99),
    qRgb(0x66, 0x33, 0x66),
    qRgb(0x66, 0x33, 0x33),
    qRgb(0x66, 0x33, 0x00),
    qRgb(0x66, 0x00, 0xFF),
    qRgb(0x66, 0x00, 0xCC),
    qRgb(0x66, 0x00, 0x99),
    qRgb(0x66, 0x00, 0x66),
    qRgb(0x66, 0x00, 0x33),
    qRgb(0x66, 0x00, 0x00),
    qRgb(0x33, 0xFF, 0xFF),
    qRgb(0x33, 0xFF, 0xCC),
    qRgb(0x33, 0xFF, 0x99),
    qRgb(0x33, 0xFF, 0x66),
    qRgb(0x33, 0xFF, 0x33),
    qRgb(0x33, 0xFF, 0x00),
    qRgb(0x33, 0xCC, 0xFF),
    qRgb(0x33, 0xCC, 0xCC),
    qRgb(0x33, 0xCC, 0x99),
    qRgb(0x33, 0xCC, 0x66),
    qRgb(0x33, 0xCC, 0x33),
    qRgb(0x33, 0xCC, 0x00),
    qRgb(0x33, 0x99, 0xFF),
    qRgb(0x33, 0x99, 0xCC),
    qRgb(0x33, 0x99, 0x99),
    qRgb(0x33, 0x99, 0x66),
    qRgb(0x33, 0x99, 0x33),
    qRgb(0x33, 0x99, 0x00),
    qRgb(0x33, 0x66, 0xFF),
    qRgb(0x33, 0x66, 0xCC),
    qRgb(0x33, 0x66, 0x99),
    qRgb(0x33, 0x66, 0x66),
    qRgb(0x33, 0x66, 0x33),
    qRgb(0x33, 0x66, 0x00),
    qRgb(0x33, 0x33, 0xFF),
    qRgb(0x33, 0x33, 0xCC),
    qRgb(0x33, 0x33, 0x99),
    qRgb(0x33, 0x33, 0x66),
    qRgb(0x33, 0x33, 0x33),
    qRgb(0x33, 0x33, 0x00),
    qRgb(0x33, 0x00, 0xFF),
    qRgb(0x33, 0x00, 0xCC),
    qRgb(0x33, 0x00, 0x99),
    qRgb(0x33, 0x00, 0x66),
    qRgb(0x33, 0x00, 0x33),
    qRgb(0x33, 0x00, 0x00),
    qRgb(0x00, 0xFF, 0xFF),
    qRgb(0x00, 0xFF, 0xCC),
    qRgb(0x00, 0xFF, 0x99),
    qRgb(0x00, 0xFF, 0x66),
    qRgb(0x00, 0xFF, 0x33),
    qRgb(0x00, 0xFF, 0x00),
    qRgb(0x00, 0xCC, 0xFF),
    qRgb(0x00, 0xCC, 0xCC),
    qRgb(0x00, 0xCC, 0x99),
    qRgb(0x00, 0xCC, 0x66),
    qRgb(0x00, 0xCC, 0x33),
    qRgb(0x00, 0xCC, 0x00),
    qRgb(0x00, 0x99, 0xFF),
    qRgb(0x00, 0x99, 0xCC),
    qRgb(0x00, 0x99, 0x99),
    qRgb(0x00, 0x99, 0x66),
    qRgb(0x00, 0x99, 0x33),
    qRgb(0x00, 0x99, 0x00),
    qRgb(0x00, 0x66, 0xFF),
    qRgb(0x00, 0x66, 0xCC),
    qRgb(0x00, 0x66, 0x99),
    qRgb(0x00, 0x66, 0x66),
    qRgb(0x00, 0x66, 0x33),
    qRgb(0x00, 0x66, 0x00),
    qRgb(0x00, 0x33, 0xFF),
    qRgb(0x00, 0x33, 0xCC),
    qRgb(0x00, 0x33, 0x99),
    qRgb(0x00, 0x33, 0x66),
    qRgb(0x00, 0x33, 0x33),
    qRgb(0x00, 0x33, 0x00),
    qRgb(0x00, 0x00, 0xFF),
    qRgb(0x00, 0x00, 0xCC),
    qRgb(0x00, 0x00, 0x99),
    qRgb(0x00, 0x00, 0x66),
    qRgb(0x00, 0x00, 0x33),
    qRgb(0xEE, 0x00, 0x00),
    qRgb(0xDD, 0x00, 0x00),
    qRgb(0xBB, 0x00, 0x00),
    qRgb(0xAA, 0x00, 0x00),
    qRgb(0x88, 0x00, 0x00),
    qRgb(0x77, 0x00, 0x00),
    qRgb(0x55, 0x00, 0x00),
    qRgb(0x44, 0x00, 0x00),
    qRgb(0x22, 0x00, 0x00),
    qRgb(0x11, 0x00, 0x00),
    qRgb(0x00, 0xEE, 0x00),
    qRgb(0x00, 0xDD, 0x00),
    qRgb(0x00, 0xBB, 0x00),
    qRgb(0x00, 0xAA, 0x00),
    qRgb(0x00, 0x88, 0x00),
    qRgb(0x00, 0x77, 0x00),
    qRgb(0x00, 0x55, 0x00),
    qRgb(0x00, 0x44, 0x00),
    qRgb(0x00, 0x22, 0x00),
    qRgb(0x00, 0x11, 0x00),
    qRgb(0x00, 0x00, 0xEE),
    qRgb(0x00, 0x00, 0xDD),
    qRgb(0x00, 0x00, 0xBB),
    qRgb(0x00, 0x00, 0xAA),
    qRgb(0x00, 0x00, 0x88),
    qRgb(0x00, 0x00, 0x77),
    qRgb(0x00, 0x00, 0x55),
    qRgb(0x00, 0x00, 0x44),
    qRgb(0x00, 0x00, 0x22),
    qRgb(0x00, 0x00, 0x11),
    qRgb(0xEE, 0xEE, 0xEE),
    qRgb(0xDD, 0xDD, 0xDD),
    qRgb(0xBB, 0xBB, 0xBB),
    qRgb(0xAA, 0xAA, 0xAA),
    qRgb(0x88, 0x88, 0x88),
    qRgb(0x77, 0x77, 0x77),
    qRgb(0x55, 0x55, 0x55),
    qRgb(0x44, 0x44, 0x44),
    qRgb(0x22, 0x22, 0x22),
    qRgb(0x11, 0x11, 0x11),
    qRgb(0x00, 0x00, 0x00)
};
Q_STATIC_ASSERT(sizeof(ICNSColorTable8bit) / sizeof(ICNSColorTable8bit[0]) == (1 << ICNSEntry::Depth8bit));

static inline QDataStream &operator>>(QDataStream &in, ICNSBlockHeader &p)
{
    in >> p.ostype;
    in >> p.length;
    return in;
}

static inline QDataStream &operator<<(QDataStream &out, const ICNSBlockHeader &p)
{
    out << p.ostype;
    out << p.length;
    return out;
}

static inline bool isPowOf2OrDividesBy16(quint32 u, qreal r)
{
    return u == r && ((u % 16 == 0) || (r >= 16 && (u & (u - 1)) == 0));
}

static inline bool isBlockHeaderValid(const ICNSBlockHeader &header, quint64 bound = 0)
{
    return header.ostype != 0 && (bound == 0
                || qBound(quint64(ICNSBlockHeaderSize), quint64(header.length), bound) == header.length);
}

static inline bool isIconCompressed(const ICNSEntry &icon)
{
    return icon.dataFormat == ICNSEntry::PNG || icon.dataFormat == ICNSEntry::JP2;
}

static inline bool isMaskSuitable(const ICNSEntry &mask, const ICNSEntry &icon, ICNSEntry::Depth target)
{
    return mask.variant == icon.variant && mask.depth == target
            && mask.height == icon.height && mask.width == icon.width;
}

static inline QByteArray nameFromOSType(quint32 ostype)
{
    const quint32 bytes = qToBigEndian(ostype);
    return QByteArray((const char*)&bytes, 4);
}

static inline quint32 nameToOSType(const QByteArray &ostype)
{
    if (ostype.size() != 4)
        return 0;
    return qFromBigEndian(*reinterpret_cast<const quint32*>(ostype.constData()));
}

static inline QByteArray nameForCompressedIcon(quint8 iconNumber)
{
    const bool portable = iconNumber < 7;
    const QByteArray base =  portable ? QByteArrayLiteral("icp") : QByteArrayLiteral("ic");
    if (!portable && iconNumber < 10)
        return base + "0" + QByteArray::number(iconNumber);
    return base + QByteArray::number(iconNumber);
}

static inline QList<QRgb> getColorTable(ICNSEntry::Depth depth)
{
    QList<QRgb> table;
    uint n = 1 << depth;
    const QRgb *data;
    switch (depth) {
    case ICNSEntry::DepthMono:
        data = ICNSColorTableMono;
        break;
    case ICNSEntry::Depth4bit:
        data = ICNSColorTable4bit;
        break;
    case ICNSEntry::Depth8bit:
        data = ICNSColorTable8bit;
        break;
    default:
        Q_UNREACHABLE();
        break;
    }
    table.resize(n);
    memcpy(table.data(), data, sizeof(QRgb) * n);
    return table;
}

static bool parseIconEntryData(ICNSEntry &icon, QIODevice *device)
{
    const qint64 oldPos = device->pos();
    if (oldPos != icon.dataOffset && !device->seek(icon.dataOffset))
        return false;

    const QByteArray magic = device->peek(12);
    const bool isPNG = magic.startsWith(QByteArrayLiteral("\211PNG\r\n\032\n\000\000\000\r"));
    const bool isJP2 = !isPNG && magic == QByteArrayLiteral("\000\000\000\014jP  \r\n\207\n");
    if (isPNG || isJP2) {
        // TODO: Add parsing of png/jp2 headers to enable feature reporting by plugin?
        icon.flags = ICNSEntry::IsIcon;
        icon.dataFormat = isPNG? ICNSEntry::PNG : ICNSEntry::JP2;
    }
    if (oldPos != icon.dataOffset && !device->seek(oldPos))
        return false;
    return true;
}

static bool parseIconEntryInfo(ICNSEntry &icon)
{
    const QString ostype = QString::fromLatin1(nameFromOSType(icon.ostype));
    // Typical OSType naming: <junk><group><depth><mask>;
    // For icons OSType should be strictly alphanumeric + '#' character for masks/mono.
    const QString ptrn = QStringLiteral("^(?<junk>[a-z|A-Z]{0,4})(?<group>[a-z|A-Z]{1})(?<depth>[\\d]{0,2})(?<mask>[#mk]{0,2})$");
    QRegularExpression regexp(ptrn);
    QRegularExpressionMatch match = regexp.match(ostype);
    if (!match.hasMatch()) {
        qWarning("parseIconEntryInfo(): Failed, OSType doesn't match: \"%s\"", qPrintable(ostype));
        return false;
    }
    const QString group = match.captured(QStringLiteral("group"));
    const QString depth = match.captured(QStringLiteral("depth"));
    const QString mask = match.captured(QStringLiteral("mask"));
    // Icon group:
    if (!group.isEmpty())
        icon.group = ICNSEntry::Group(group.at(0).toLatin1());

    // That's enough for compressed ones
    if (isIconCompressed(icon))
        return true;
    // Icon depth:
    if (!depth.isEmpty()) {
        const uint depthUInt = depth.toUInt();
        if (depthUInt > 32)
            return false;
        icon.depth = ICNSEntry::Depth(depthUInt);
    }
    // Try mono if depth not found
    if (icon.depth == ICNSEntry::DepthUnknown)
        icon.depth = ICNSEntry::DepthMono;
    // Detect size:
    const qreal bytespp = (qreal)icon.depth / 8;
    const qreal r1 = qSqrt(icon.dataLength / bytespp);
    const qreal r2 = qSqrt((icon.dataLength / bytespp) / 2);
    const quint32 r1u = qRound(r1);
    const quint32 r2u = qRound(r2);
    const bool singleEntry = isPowOf2OrDividesBy16(r1u, r1);
    const bool doubleSize = isPowOf2OrDividesBy16(r2u, r2);
    if (singleEntry) {
        icon.flags = mask.isEmpty() ? ICNSEntry::IsIcon : ICNSEntry::IsMask;
        icon.dataFormat = ICNSEntry::RawIcon;
        icon.width = r1u;
        icon.height = r1u;
    } else if (doubleSize) {
        icon.flags = ICNSEntry::IconPlusMask;
        icon.dataFormat = ICNSEntry::RawIcon;
        icon.width = r2u;
        icon.height = r2u;
    } else if (icon.group == ICNSEntry::GroupMini) {
        // Legacy 16x12 icons are an exception from the generic square formula
        const bool doubleSize = icon.dataLength == 192 * bytespp * 2;
        icon.flags = doubleSize ? ICNSEntry::IconPlusMask : ICNSEntry::IsIcon;
        icon.dataFormat = ICNSEntry::RawIcon;
        icon.width = 16;
        icon.height = 12;
    } else if (icon.depth == ICNSEntry::Depth32bit) {
        // We have a formula mismatch in a 32bit icon there, probably RLE24
        icon.dataFormat = ICNSEntry::RLE24;
        icon.flags = mask.isEmpty() ? ICNSEntry::IsIcon : ICNSEntry::IsMask;
        switch (icon.group) {
        case ICNSEntry::GroupSmall:
            icon.width = 16;
            break;
        case ICNSEntry::GroupLarge:
            icon.width = 32;
            break;
        case ICNSEntry::GroupHuge:
            icon.width = 48;
            break;
        case ICNSEntry::GroupThumbnail:
            icon.width = 128;
            break;
        default:
            qWarning("parseIconEntryInfo(): Failed, 32bit icon from an unknown group. OSType: \"%s\"",
                     qPrintable(ostype));
        }
        icon.height = icon.width;
    }
    // Sanity check
    if (icon.width == 0 || icon.width > 4096)
        return false;
    return true;
}

static QImage readMask(const ICNSEntry &mask, QDataStream &stream)
{
    if ((mask.flags & ICNSEntry::IsMask) == 0)
        return QImage();
    if (mask.depth != ICNSEntry::DepthMono && mask.depth != ICNSEntry::Depth8bit) {
        qWarning("readMask(): Failed, unusual bit depth: %u OSType: \"%s\"",
                 mask.depth, nameFromOSType(mask.ostype).constData());
        return QImage();
    }
    const bool isMono = mask.depth == ICNSEntry::DepthMono;
    const bool doubleSize = mask.flags == ICNSEntry::IconPlusMask;
    const quint32 imageDataSize = (mask.width * mask.height * mask.depth) / 8;
    const qint64 pos = doubleSize ? (mask.dataOffset + imageDataSize) : mask.dataOffset;
    const qint64 oldPos = stream.device()->pos();
    if (!stream.device()->seek(pos))
        return QImage();
    QImage img;
    if (!QImageIOHandler::allocateImage(QSize(mask.width, mask.height), QImage::Format_RGB32, &img))
        return QImage();
    quint8 byte = 0;
    quint32 pixel = 0;
    for (quint32 y = 0; y < mask.height; y++) {
        QRgb *line = reinterpret_cast<QRgb *>(img.scanLine(y));
        for (quint32 x = 0; x < mask.width; x++) {
            if (pixel % (8 / mask.depth) == 0)
                stream >> byte;
            else if (isMono)
                byte <<= 1;
            const quint8 alpha = isMono ? (((byte >> 7) & 0x01) * 255) : byte;
            line[x] = qRgb(alpha, alpha, alpha);
            pixel++;
        }
    }
    stream.device()->seek(oldPos);
    return img;
}

template <ICNSEntry::Depth depth>
static QImage readLowDepthIcon(const ICNSEntry &icon, QDataStream &stream)
{
    Q_STATIC_ASSERT(depth == ICNSEntry::DepthMono || depth == ICNSEntry::Depth4bit
                    || depth == ICNSEntry::Depth8bit);

    const bool isMono = depth == ICNSEntry::DepthMono;
    const QImage::Format format = isMono ? QImage::Format_Mono : QImage::Format_Indexed8;
    const QList<QRgb> colortable = getColorTable(depth);
    if (colortable.isEmpty())
        return QImage();
    QImage img;
    if (!QImageIOHandler::allocateImage(QSize(icon.width, icon.height), format, &img))
        return QImage();
    img.setColorTable(colortable);
    quint32 pixel = 0;
    quint8 byte = 0;
    for (quint32 y = 0; y < icon.height; y++) {
        for (quint32 x = 0; x < icon.width; x++) {
            if (pixel % (8 / depth) == 0)
                stream >> byte;
            quint8 cindex;
            switch (depth) {
            case ICNSEntry::DepthMono:
                cindex = (byte >> 7) & 0x01; // left 1 bit
                byte <<= 1;
                break;
            case ICNSEntry::Depth4bit:
                cindex = (byte >> 4) & 0x0F; // left 4 bits
                byte <<= 4;
                break;
            default:
                cindex = byte; // 8 bits
                break;
            }
            img.setPixel(x, y, cindex);
            pixel++;
        }
    }
    return img;
}

static QImage read32bitIcon(const ICNSEntry &icon, QDataStream &stream)
{
    QImage img;
    if (!QImageIOHandler::allocateImage(QSize(icon.width, icon.height), QImage::Format_RGB32, &img))
        return QImage();
    if (icon.dataFormat != ICNSEntry::RLE24) {
        for (quint32 y = 0; y < icon.height; y++) {
            QRgb *line = reinterpret_cast<QRgb *>(img.scanLine(y));
            for (quint32 x = 0; x < icon.width; x++) {
                quint8 r, g, b, a;
                stream >> r >> g >> b >> a;
                line[x] = qRgb(r, g, b);
            }
        }
    } else {
        const quint32 estPxsNum = icon.width * icon.height;
        const QByteArray &bytes = stream.device()->peek(4);
        if (bytes.isEmpty())
            return QImage();
        // Zero-padding may be present:
        if (qFromBigEndian<quint32>(*bytes.constData()) == 0)
            stream.skipRawData(4);
        for (quint8 colorNRun = 0; colorNRun < 3; colorNRun++) {
            quint32 pixel = 0;
            QRgb *line = 0;
            while (pixel < estPxsNum && !stream.atEnd()) {
                quint8 byte, value;
                stream >> byte;
                const bool bitIsClear = (byte & 0x80) == 0;
                // If high bit is clear: run of different values; else: same value
                quint8 runLength = bitIsClear ? ((0xFF & byte) + 1) : ((0xFF & byte) - 125);
                // Length of the run for for different values: 1 <= len <= 128
                // Length of the run for same values: 3 <= len <= 130
                if (!bitIsClear)
                    stream >> value;
                for (quint8 i = 0; i < runLength && pixel < estPxsNum; i++) {
                    if (bitIsClear)
                        stream >> value;
                    const quint32 y = pixel / icon.height;
                    const quint32 x = pixel - (icon.width * y);
                    if (pixel % icon.height == 0)
                        line = reinterpret_cast<QRgb *>(img.scanLine(y));
                    QRgb rgb = line[x];
                    const int r = (colorNRun == 0) ? value : qRed(rgb);
                    const int g = (colorNRun == 1) ? value : qGreen(rgb);
                    const int b = (colorNRun == 2) ? value : qBlue(rgb);
                    line[x] = qRgb(r, g, b);
                    pixel++;
                }
            }
        }
    }
    return img;
}

QICNSHandler::QICNSHandler() :
    m_currentIconIndex(0), m_state(ScanNotScanned)
{
}

bool QICNSHandler::canRead(QIODevice *device)
{
    if (!device || !device->isReadable()) {
        qWarning("QICNSHandler::canRead() called without a readable device");
        return false;
    }

    if (device->peek(4) == QByteArrayLiteral("icns")) {
        if (device->isSequential()) {
            qWarning("QICNSHandler::canRead() called on a sequential device");
            return false;
        }
        return true;
    }

    return false;
}

bool QICNSHandler::canRead() const
{
    if (m_state == ScanNotScanned && !canRead(device()))
        return false;

    if (m_state != ScanError) {
        setFormat(QByteArrayLiteral("icns"));
        return true;
    }

    return false;
}

bool QICNSHandler::read(QImage *outImage)
{
    QImage img;
    if (!ensureScanned() || m_currentIconIndex >= m_icons.size()) {
        qWarning("QICNSHandler::read(): The device wasn't parsed properly!");
        return false;
    }

    const ICNSEntry &icon = m_icons.at(m_currentIconIndex);
    QDataStream stream(device());
    stream.setByteOrder(QDataStream::BigEndian);
    if (!device()->seek(icon.dataOffset))
        return false;

    switch (icon.dataFormat) {
    case ICNSEntry::RawIcon:
    case ICNSEntry::RLE24:
        if (qMin(icon.width, icon.height) == 0)
            break;
        switch (icon.depth) {
        case ICNSEntry::DepthMono:
            img = readLowDepthIcon<ICNSEntry::DepthMono>(icon, stream);
            break;
        case ICNSEntry::Depth4bit:
            img = readLowDepthIcon<ICNSEntry::Depth4bit>(icon, stream);
            break;
        case ICNSEntry::Depth8bit:
            img = readLowDepthIcon<ICNSEntry::Depth8bit>(icon, stream);
            break;
        case ICNSEntry::Depth32bit:
            img = read32bitIcon(icon, stream);
            break;
        default:
            qWarning("QICNSHandler::read(): Failed, unsupported icon bit depth: %u, OSType: \"%s\"",
                     icon.depth, nameFromOSType(icon.ostype).constData());
        }
        if (!img.isNull()) {
            QImage alpha = readMask(getIconMask(icon), stream);
            if (!alpha.isNull())
                img.setAlphaChannel(alpha);
        }
        break;
    default:
        const char *format = 0;
        if (icon.dataFormat == ICNSEntry::PNG)
            format = "png";
        else if (icon.dataFormat == ICNSEntry::JP2)
            format = "jp2";
        // Even if JP2 or PNG magic is not detected, try anyway for unknown formats
        img = QImage::fromData(device()->read(icon.dataLength), format);
        if (img.isNull()) {
            if (format == 0)
                format = "unknown";
            qWarning("QICNSHandler::read(): Failed, compressed format \"%s\" is not supported " \
                     "by your Qt library or this file is corrupt. OSType: \"%s\"",
                     format, nameFromOSType(icon.ostype).constData());
        }
    }
    *outImage = img;
    return !img.isNull();
}

bool QICNSHandler::write(const QImage &image)
{
    /*
    Notes:
    * Experimental implementation. Just for simple converting tasks / testing purposes.
    * Min. size is 16x16, Max. size is 1024x1024.
    * Performs downscale to a square image if width != height.
    * Performs upscale to 16x16, if the image is smaller.
    * Performs downscale to a nearest power of two if size is not a power of two.
    * Currently uses non-hardcoded OSTypes.
    */
    QIODevice *device = this->device();
    if (!device->isWritable() || image.isNull() || qMin(image.width(), image.height()) == 0)
        return false;
    const int minSize = qMin(image.width(), image.height());
    const int oldSize = (minSize < 16) ? 16 : minSize;
    // Calc power of two:
    int size = oldSize;
    uint pow = 0;
    // Note: Values over 10 are reserved for retina icons.
    while (pow < 10 && (size >>= 1))
        pow++;
    const int newSize = 1 << pow;
    QImage img = image;
    // Let's enforce resizing if size differs:
    if (newSize != oldSize || qMax(image.width(), image.height()) != minSize)
        img = img.scaled(newSize, newSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
    // Construct OSType and headers:
    const quint32 ostype = nameToOSType(nameForCompressedIcon(pow));
    ICNSBlockHeader fileHeader;
    fileHeader.ostype = ICNSBlockHeader::TypeIcns;
    ICNSBlockHeader tocHeader;
    tocHeader.ostype = ICNSBlockHeader::TypeToc;
    ICNSBlockHeader iconEntry;
    iconEntry.ostype = ostype;
    QByteArray imageData;
    QBuffer buffer(&imageData);
    if (!buffer.open(QIODevice::WriteOnly) || !img.save(&buffer, "png"))
        return false;
    buffer.close();
    iconEntry.length = ICNSBlockHeaderSize + imageData.size();
    tocHeader.length = ICNSBlockHeaderSize * 2;
    fileHeader.length = ICNSBlockHeaderSize + tocHeader.length + iconEntry.length;
    if (!isBlockHeaderValid(iconEntry))
        return false;

    QDataStream stream(device);
    // iconEntry is also a TOC entry
    stream << fileHeader << tocHeader << iconEntry << iconEntry;
    stream.writeRawData(imageData.constData(), imageData.size());
    return stream.status() == QDataStream::Ok;
}

bool QICNSHandler::supportsOption(ImageOption option) const
{
    return option == SubType;
}

QVariant QICNSHandler::option(ImageOption option) const
{
    if (!supportsOption(option) || !ensureScanned())
        return QVariant();

    if (option == SubType) {
        if (imageCount() > 0 && m_currentIconIndex <= imageCount()) {
            const ICNSEntry &icon = m_icons.at(m_currentIconIndex);
            if (icon.variant != 0)
                return QByteArray(nameFromOSType(icon.variant) + '-' + nameFromOSType(icon.ostype));
            return nameFromOSType(icon.ostype);
        }
    }

    return QVariant();
}

int QICNSHandler::imageCount() const
{
    if (!ensureScanned())
        return 0;

    return m_icons.size();
}

bool QICNSHandler::jumpToImage(int imageNumber)
{
    if (imageNumber >= imageCount())
        return false;

    m_currentIconIndex = imageNumber;
    return true;
}

bool QICNSHandler::jumpToNextImage()
{
    return jumpToImage(m_currentIconIndex + 1);
}

bool QICNSHandler::ensureScanned() const
{
    if (m_state == ScanNotScanned) {
        QICNSHandler *that = const_cast<QICNSHandler *>(this);
        that->m_state = that->scanDevice() ? ScanSuccess : ScanError;
    }

    return m_state == ScanSuccess;
}

bool QICNSHandler::addEntry(const ICNSBlockHeader &header, qint64 imgDataOffset, quint32 variant)
{
    // Note: This function returns false only when a device positioning error occurred
    ICNSEntry entry;
    entry.ostype = header.ostype;
    entry.variant = variant;
    entry.dataOffset = imgDataOffset;
    entry.dataLength = header.length - ICNSBlockHeaderSize;
    // Check for known magic numbers:
    if (!parseIconEntryData(entry, device()))
        return false;
    // Parse everything else and index this entry:
    if (parseIconEntryInfo(entry)) {
        if ((entry.flags & ICNSEntry::IsMask) != 0)
            m_masks << entry;
        if ((entry.flags & ICNSEntry::IsIcon) != 0)
            m_icons << entry;
    }
    return true;
}

bool QICNSHandler::scanDevice()
{
    if (m_state == ScanSuccess)
        return true;

    if (!device()->seek(0))
        return false;

    QDataStream stream(device());
    stream.setByteOrder(QDataStream::BigEndian);

    bool scanIsIncomplete = false;
    qint64 filelength = device()->size();
    ICNSBlockHeader blockHeader;
    while (!stream.atEnd() || device()->pos() < filelength) {
        stream >> blockHeader;
        if (stream.status() != QDataStream::Ok)
            return false;

        const qint64 blockDataOffset = device()->pos();
        if (!isBlockHeaderValid(blockHeader, ICNSBlockHeaderSize + filelength - blockDataOffset)) {
            qWarning("QICNSHandler::scanDevice(): Failed, bad header at pos %s. OSType \"%s\", length %u",
                     QByteArray::number(blockDataOffset).constData(),
                     nameFromOSType(blockHeader.ostype).constData(), blockHeader.length);
            return false;
        }
        const quint64 blockDataLength = blockHeader.length - ICNSBlockHeaderSize;
        const qint64 nextBlockOffset = blockDataOffset + blockDataLength;

        switch (blockHeader.ostype) {
        case ICNSBlockHeader::TypeIcns:
            if (blockDataOffset != ICNSBlockHeaderSize) {
                // Icns container definition should be in the beginning of the device.
                // If we meet this block somewhere else, then just ignore it.
                stream.skipRawData(blockDataLength);
                break;
            }
            filelength = blockHeader.length;
            if (device()->size() < blockHeader.length) {
                qWarning("QICNSHandler::scanDevice(): Failed, file is incomplete.");
                return false;
            }
            break;
        case ICNSBlockHeader::TypeIcnv:
        case ICNSBlockHeader::TypeClut:
            // We don't have a good use for these blocks... yet.
            stream.skipRawData(blockDataLength);
            break;
        case ICNSBlockHeader::TypeTile:
        case ICNSBlockHeader::TypeOver:
        case ICNSBlockHeader::TypeOpen:
        case ICNSBlockHeader::TypeDrop:
        case ICNSBlockHeader::TypeOdrp:
            // Icns container seems to have an embedded icon variant container
            // Let's start a scan for entries
            while (!stream.atEnd() && device()->pos() < nextBlockOffset) {
                ICNSBlockHeader icon;
                stream >> icon;
                if (stream.status() != QDataStream::Ok)
                    return false;
                // Check for incorrect variant entry header and stop scan
                quint64 remaining = blockDataLength - (device()->pos() - blockDataOffset);
                if (!isBlockHeaderValid(icon, ICNSBlockHeaderSize + remaining))
                    break;
                if (!addEntry(icon, device()->pos(), blockHeader.ostype))
                    return false;
                if (stream.skipRawData(icon.length - ICNSBlockHeaderSize) < 0)
                    return false;
            }
            if (device()->pos() != nextBlockOffset) {
                // Scan of this container didn't end where we expected.
                // Let's generate some output about this incident:
                qWarning("Scan of the icon variant container (\"%s\") failed at pos %s.\n" \
                         "Reason: Scan didn't reach the end of this container's block, " \
                         "delta: %s bytes. This file may be corrupted.",
                         nameFromOSType(blockHeader.ostype).constData(),
                         QByteArray::number(device()->pos()).constData(),
                         QByteArray::number(nextBlockOffset - device()->pos()).constData());
                if (!device()->seek(nextBlockOffset))
                    return false;
            }
            break;
        case ICNSBlockHeader::TypeToc: {
            // Quick scan, table of contents
            if (blockDataOffset != ICNSBlockHeaderSize * 2) {
                // TOC should be the first block in the file after container definition.
                // Ignore and go on with a deep scan.
                stream.skipRawData(blockDataLength);
                break;
            }
            // First image data offset:
            qint64 imgDataOffset = blockDataOffset + blockHeader.length;
            for (uint i = 0, count = blockDataLength / ICNSBlockHeaderSize; i < count; i++) {
                ICNSBlockHeader tocEntry;
                stream >> tocEntry;
                if (!isBlockHeaderValid(tocEntry)) {
                    // TOC contains incorrect header, we should skip TOC since we can't trust it
                    qWarning("QICNSHandler::scanDevice(): Warning! Table of contents contains a bad " \
                             "entry! Stop at device pos: %s bytes. This file may be corrupted.",
                             QByteArray::number(device()->pos()).constData());
                    if (!device()->seek(nextBlockOffset))
                        return false;
                    break;
                }
                if (!addEntry(tocEntry, imgDataOffset))
                    return false;
                imgDataOffset += tocEntry.length;
                // If TOC covers all the blocks in the file, then quick scan is complete
                if (imgDataOffset == filelength)
                    return true;
            }
            // Else just start a deep scan to salvage anything left after TOC's end
            scanIsIncomplete = true;
            break;
        }
        default:
            // Deep scan, block by block
            if (scanIsIncomplete) {
                // Check if entry with this offset is added somewhere
                // But only if we have incomplete TOC, otherwise just try to add
                bool exists = false;
                for (int i = 0; i < m_icons.size() && !exists; i++)
                    exists = m_icons.at(i).dataOffset == blockDataOffset;
                for (int i = 0; i < m_masks.size() && !exists; i++)
                    exists = m_masks.at(i).dataOffset == blockDataOffset;
                if (!exists && !addEntry(blockHeader, blockDataOffset))
                    return false;
            } else if (!addEntry(blockHeader, blockDataOffset)) {
                return false;
            }
            stream.skipRawData(blockDataLength);
            break;
        }
    }
    return (m_icons.size() > 0);
}

const ICNSEntry &QICNSHandler::getIconMask(const ICNSEntry &icon) const
{
    const bool is32bit = icon.depth == ICNSEntry::Depth32bit;
    ICNSEntry::Depth targetDepth = is32bit ? ICNSEntry::Depth8bit : ICNSEntry::DepthMono;
    for (int i = 0; i < m_masks.size(); i++) {
        const ICNSEntry &mask = m_masks.at(i);
        if (isMaskSuitable(mask, icon, targetDepth))
            return mask;
    }
    return icon;
}

QT_END_NAMESPACE

#endif // QT_NO_DATASTREAM
