/****************************************************************************
**
** Copyright (C) 2018 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the tools applications of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:GPL-EXCEPT$
** 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 https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 as published by the Free Software
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/

#include "distancefieldmodelworker.h"

#include "distancefieldmodel.h"
#include <qendian.h>
#include <QtGui/private/qdistancefield_p.h>

QT_BEGIN_NAMESPACE

#   pragma pack(1)
struct MaxpHeader
{
    quint32 version;
    quint16 numGlyphs;
};

struct CmapHeader {
    quint16 version;
    quint16 numTables;
};

struct CmapEncodingRecord {
    quint16 platformId;
    quint16 encodingId;
    quint32 offset;
};

struct CmapSubtable
{
    quint16 format;
    quint16 length;
    quint16 language;
};

struct CmapSubtable0 : public CmapSubtable
{
    quint8 glyphIdArray[256];
};

struct CmapSubtable4 : public CmapSubtable
{
    quint16 segCountX2;
    quint16 searchRange;
    quint16 entrySelector;
    quint16 rangeShift;
};

struct CmapSubtable6 : public CmapSubtable
{
    quint16 firstCode;
    quint16 entryCount;
};

struct CmapSubtable10
{
    quint32 format;
    quint32 length;
    quint32 language;
    quint32 startCharCode;
    quint32 numChars;
};

struct CmapSubtable12
{
    quint16 format;
    quint16 reserved;
    quint32 length;
    quint32 language;
    quint32 numGroups;
};

struct SequentialMapGroup
{
    quint32 startCharCode;
    quint32 endCharCode;
    quint32 glyphIndex;
};

#   pragma pack()

DistanceFieldModelWorker::DistanceFieldModelWorker(QObject *parent)
    : QObject(parent)
    , m_glyphCount(0)
    , m_nextGlyphId(0)
    , m_doubleGlyphResolution(false)
{
}

template <typename T>
static void readCmapSubtable(DistanceFieldModelWorker *worker, const QByteArray &cmap, quint32 tableOffset, quint16 format)
{
    if (uint(cmap.size()) < tableOffset + sizeof(T)) {
        emit worker->error(QObject::tr("End of file when reading subtable of format '%1'").arg(format));
        return;
    }

    const T *subtable = reinterpret_cast<const T *>(cmap.constData() + tableOffset);
    quint16 length = qFromBigEndian(subtable->length);
    if (uint(cmap.size()) < tableOffset + length) {
        emit worker->error(QObject::tr("Corrupt data found when reading subtable of format '%1'. Table offset: %2. Length: %3. Cmap length: %4.")
                           .arg(format).arg(tableOffset).arg(length).arg(cmap.size()));
        return;
    }

    const void *end = cmap.constData() + tableOffset + subtable->length;
    worker->readCmapSubtable(subtable, end);
}

void DistanceFieldModelWorker::readCmapSubtable(const CmapSubtable0 *subtable, const void *end)
{
    Q_UNUSED(end); // Already checked for length
    for (int i = 0; i < 256; ++i)
        m_cmapping[glyph_t(subtable->glyphIdArray[i])] = i;
}

void DistanceFieldModelWorker::readCmapSubtable(const CmapSubtable4 *subtable, const void *end)
{
    quint16 segCount = qFromBigEndian(subtable->segCountX2) / 2;
    const quint16 *endCodes = reinterpret_cast<const quint16 *>(subtable + 1);
    const quint16 *startCodes = endCodes + segCount + 1; // endCodes[segCount] + reservedPad
    const qint16 *idDeltas = reinterpret_cast<const qint16 *>(startCodes + segCount);
    const quint16 *idRangeOffsets = reinterpret_cast<const quint16 *>(idDeltas + segCount);
    const quint16 *glyphIdArray = idRangeOffsets + segCount;
    if (glyphIdArray >= end) {
        emit error(tr("End of cmap table reached when parsing subtable format '4'"));
        return;
    }

    for (int i = 0; i < segCount - 1; ++i) { // Last entry in arrays is the sentinel
        quint16 startCode = qFromBigEndian(startCodes[i]);
        quint16 endCode = qFromBigEndian(endCodes[i]);
        quint16 rangeOffset = qFromBigEndian(idRangeOffsets[i]);

        for (quint16 c = startCode; c <= endCode; ++c) {
            if (rangeOffset != 0) {
                const quint16 *glyphIndex = (idRangeOffsets + i) + (c - startCode) + rangeOffset / 2;
                if (glyphIndex + 1 > end) {
                    emit error(tr("End of cmap, subtable format '4', reached when fetching character '%1' in range [%2, %3]").arg(c).arg(startCode).arg(endCode));
                    return;
                }

                m_cmapping[glyph_t(qFromBigEndian(*glyphIndex))] = quint32(c);
            } else {
                quint16 idDelta = qFromBigEndian(idDeltas[i]);
                m_cmapping[glyph_t((idDelta + c) % 65536)] = quint32(c);
            }
        }

    }
}

void DistanceFieldModelWorker::readCmapSubtable(const CmapSubtable6 *subtable, const void *end)
{
    quint16 entryCount = qFromBigEndian(subtable->entryCount);
    const quint16 *glyphIndexes = reinterpret_cast<const quint16 *>(subtable + 1);
    if (glyphIndexes + entryCount > end) {
        emit error(tr("End of cmap reached while parsing subtable format '6'"));
        return;
    }

    quint16 firstCode = qFromBigEndian(subtable->firstCode);
    for (quint16 i = 0; i < entryCount; ++i)
        m_cmapping[glyph_t(qFromBigEndian(glyphIndexes[i]))] = firstCode + i;
}

void DistanceFieldModelWorker::readCmapSubtable(const CmapSubtable10 *subtable, const void *end)
{
    quint32 numChars = qFromBigEndian(subtable->numChars);
    const quint16 *glyphs = reinterpret_cast<const quint16 *>(subtable + 1);
    if (glyphs + numChars > end) {
        emit error(tr("End of cmap reached while parsing subtable of format '10'"));
        return;
    }

    quint32 startCharCode = qFromBigEndian(subtable->startCharCode);
    for (quint32 i = 0; i < numChars; ++i)
        m_cmapping[glyph_t(qFromBigEndian(glyphs[i]))] = startCharCode + i;
}

void DistanceFieldModelWorker::readCmapSubtable(const CmapSubtable12 *subtable, const void *end)
{
    quint32 numGroups = qFromBigEndian(subtable->numGroups);
    const SequentialMapGroup *sequentialMapGroups = reinterpret_cast<const SequentialMapGroup *>(subtable + 1);
    if (sequentialMapGroups + numGroups > end) {
        emit error(tr("End of cmap reached while parsing subtable of format '12'"));
        return;
    }

    for (quint32 i = 0; i < numGroups; ++i) {
        quint32 startCharCode = qFromBigEndian(sequentialMapGroups[i].startCharCode);
        quint32 endCharCode = qFromBigEndian(sequentialMapGroups[i].endCharCode);
        quint32 startGlyphIndex = qFromBigEndian(sequentialMapGroups[i].glyphIndex);

        for (quint32 j = 0; j < endCharCode - startCharCode + 1; ++j)
            m_cmapping[glyph_t(startGlyphIndex + j)] = startCharCode + j;
    }
}

void DistanceFieldModelWorker::readCmap()
{
    if (m_font.isValid()) {
        QByteArray cmap = m_font.fontTable("cmap");
        if (uint(cmap.size()) < sizeof(CmapHeader)) {
            emit error(tr("Invalid cmap table. No header."));
            return;
        }

        const CmapHeader *header = reinterpret_cast<const CmapHeader *>(cmap.constData());
        quint16 numTables = qFromBigEndian(header->numTables);

        if (uint(cmap.size()) < sizeof(CmapHeader) + numTables * sizeof(CmapEncodingRecord)) {
            emit error(tr("Invalid cmap table. No space for %1 encoding records.").arg(numTables));
            return;
        }

        // Support the same encodings as macOS (and same order of prefernece), since this should
        // cover most fonts
        static quint32 encodingPreferenceOrder[] =
        {
            quint32(0) << 16 | 4, // Unicode 2.0 +
            quint32(0) << 16 | 3, // Unicode 2.0 BMP
            quint32(0) << 16 | 1, // Unicode 1.1
            quint32(3) << 16 | 10, // Windows, UCS-4
            quint32(3) << 16 | 1, // Windows, UCS-2
            quint32(0)
        };

        QHash<quint32, const CmapEncodingRecord *> encodingRecords;
        {
            const CmapEncodingRecord *encodingRecord = reinterpret_cast<const CmapEncodingRecord *>(cmap.constData() + sizeof(CmapHeader));
            while (numTables-- > 0) {
                quint32 encoding = quint32(qFromBigEndian(encodingRecord->platformId)) << 16 | qFromBigEndian(encodingRecord->encodingId);
                encodingRecords[encoding] = encodingRecord++;
            }
        }

        // Find the first subtable we support in order of preference
        for (int i = 0; encodingPreferenceOrder[i] != 0; ++i) {
            const CmapEncodingRecord *encodingRecord = encodingRecords.value(encodingPreferenceOrder[i], nullptr);
            if (encodingRecord != nullptr) {
                quint32 offset = qFromBigEndian(encodingRecord->offset);
                if (uint(cmap.size()) < offset + sizeof(quint16)) {
                    emit error(tr("Invalid offset '%1' in cmap").arg(offset));
                    return;
                }

                quint16 format = qFromBigEndian(*reinterpret_cast<const quint16 *>(cmap.constData() + offset));
                switch (format) {
                case 0:
                    ::readCmapSubtable<CmapSubtable0>(this, cmap, offset, format);
                    return;
                case 4:
                    ::readCmapSubtable<CmapSubtable4>(this, cmap, offset, format);
                    return;
                case 6:
                    ::readCmapSubtable<CmapSubtable6>(this, cmap, offset, format);
                    return;
                case 10:
                    ::readCmapSubtable<CmapSubtable10>(this, cmap, offset, format);
                    return;
                case 12:
                    ::readCmapSubtable<CmapSubtable12>(this, cmap, offset, format);
                    return;
                default:
                    qWarning() << tr("Unsupported cmap subtable format '%1'").arg(format);
                };
            }
        }

        emit error(tr("No suitable cmap subtable found"));
    }
}

void DistanceFieldModelWorker::readGlyphCount()
{
    m_nextGlyphId = 0;
    m_glyphCount = 0;
    if (m_font.isValid()) {
        QByteArray maxp = m_font.fontTable("maxp");
        if (uint(maxp.size()) >= sizeof(MaxpHeader)) {
            const MaxpHeader *header = reinterpret_cast<const MaxpHeader *>(maxp.constData());
            m_glyphCount = qFromBigEndian(header->numGlyphs);
        }
    }

    m_doubleGlyphResolution = qt_fontHasNarrowOutlines(m_font) && m_glyphCount < QT_DISTANCEFIELD_HIGHGLYPHCOUNT();
}

void DistanceFieldModelWorker::loadFont(const QString &fileName)
{
    m_font = QRawFont(fileName, 64);
    if (!m_font.isValid())
        emit error(tr("File '%1' is not a valid font file.").arg(fileName));

    readGlyphCount();
    readCmap();

    qreal pixelSize = QT_DISTANCEFIELD_BASEFONTSIZE(m_doubleGlyphResolution) * QT_DISTANCEFIELD_SCALE(m_doubleGlyphResolution);
    m_font.setPixelSize(pixelSize);

    emit fontLoaded(m_glyphCount,
                    m_doubleGlyphResolution,
                    pixelSize);
}

void DistanceFieldModelWorker::generateOneDistanceField()
{
    Q_ASSERT(m_nextGlyphId <= m_glyphCount);

    if (m_nextGlyphId == m_glyphCount) {
        emit fontGenerated();
        return;
    }

    QPainterPath path = m_font.pathForGlyph(m_nextGlyphId);
    QDistanceField distanceField(path, m_nextGlyphId, m_doubleGlyphResolution);
    emit distanceFieldGenerated(distanceField.toImage(QImage::Format_Alpha8),
                                path,
                                m_nextGlyphId,
                                m_cmapping.value(m_nextGlyphId));

    m_nextGlyphId++;
}

QT_END_NAMESPACE
