/****************************************************************************
**
** 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 "mainwindow.h"
#include "ui_mainwindow.h"
#include "distancefieldmodel.h"

#include <QtCore/qdir.h>
#include <QtCore/qdatastream.h>
#include <QtCore/qmath.h>
#include <QtCore/qendian.h>
#include <QtCore/qbuffer.h>
#include <QtGui/qdesktopservices.h>
#include <QtGui/qrawfont.h>
#include <QtWidgets/qmessagebox.h>
#include <QtWidgets/qlabel.h>
#include <QtWidgets/qprogressbar.h>
#include <QtWidgets/qfiledialog.h>
#include <QtWidgets/qinputdialog.h>

#include <QtCore/private/qunicodetables_p.h>
#include <QtGui/private/qdistancefield_p.h>
#include <QtQuick/private/qsgareaallocator_p.h>
#include <QtQuick/private/qsgadaptationlayer_p.h>

QT_BEGIN_NAMESPACE

static void openHelp()
{
    QDesktopServices::openUrl(QUrl(QLatin1String("http://doc.qt.io/qt-5/qtdistancefieldgenerator-index.html")));
}

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
    , m_settings(qApp->organizationName(), qApp->applicationName())
    , m_model(new DistanceFieldModel(this))
    , m_statusBarLabel(nullptr)
    , m_statusBarProgressBar(nullptr)
{
    ui->setupUi(this);
    ui->lvGlyphs->setModel(m_model);

    ui->actionHelp->setShortcut(QKeySequence::HelpContents);

    m_statusBarLabel = new QLabel(this);
    m_statusBarLabel->setText(tr("Ready"));
    ui->statusbar->addPermanentWidget(m_statusBarLabel);

    m_statusBarProgressBar = new QProgressBar(this);
    ui->statusbar->addPermanentWidget(m_statusBarProgressBar);
    m_statusBarProgressBar->setVisible(false);

    if (m_settings.contains(QStringLiteral("fontDirectory")))
        m_fontDir = m_settings.value(QStringLiteral("fontDirectory")).toString();
    else
        m_fontDir = QDir::currentPath();

    qRegisterMetaType<glyph_t>("glyph_t");
    qRegisterMetaType<QPainterPath>("QPainterPath");

    restoreGeometry(m_settings.value(QStringLiteral("geometry")).toByteArray());

    setupConnections();
}

MainWindow::~MainWindow()
{
    delete ui;
}

void MainWindow::open(const QString &path)
{
    m_fileName.clear();
    m_fontFile = path;
    m_fontDir = QFileInfo(path).absolutePath();
    m_settings.setValue(QStringLiteral("fontDirectory"), m_fontDir);

    ui->lwUnicodeRanges->clear();
    ui->lwUnicodeRanges->setDisabled(true);
    ui->action_Save->setDisabled(true);
    ui->action_Save_as->setDisabled(true);
    ui->tbSave->setDisabled(true);
    ui->action_Open->setDisabled(true);
    m_model->setFont(path);
}

void MainWindow::closeEvent(QCloseEvent * /*event*/)
{
    m_settings.setValue(QStringLiteral("geometry"), saveGeometry());
}

void MainWindow::setupConnections()
{
    connect(ui->action_Open, &QAction::triggered, this, &MainWindow::openFont);
    connect(ui->actionE_xit, &QAction::triggered, qApp, &QApplication::quit);
    connect(ui->action_Save, &QAction::triggered, this, &MainWindow::save);
    connect(ui->action_Save_as, &QAction::triggered, this, &MainWindow::saveAs);
    connect(ui->tbSave, &QToolButton::clicked, this, &MainWindow::save);
    connect(ui->tbSelectAll, &QToolButton::clicked, this, &MainWindow::selectAll);
    connect(ui->actionSelect_all, &QAction::triggered, this, &MainWindow::selectAll);
    connect(ui->actionSelect_string, &QAction::triggered, this, &MainWindow::selectString);
    connect(ui->actionHelp, &QAction::triggered, this, openHelp);
    connect(ui->actionAbout_App, &QAction::triggered, this, &MainWindow::about);
    connect(ui->actionAbout_Qt, &QAction::triggered, this, [this]() {
        QMessageBox::aboutQt(this);
    });
    connect(ui->lwUnicodeRanges, &QListWidget::itemSelectionChanged, this, &MainWindow::updateUnicodeRanges);

    connect(ui->lvGlyphs->selectionModel(),
            &QItemSelectionModel::selectionChanged,
            this,
            &MainWindow::updateSelection);
    connect(m_model, &DistanceFieldModel::startGeneration, this, &MainWindow::startProgressBar);
    connect(m_model, &DistanceFieldModel::stopGeneration, this, &MainWindow::stopProgressBar);
    connect(m_model, &DistanceFieldModel::distanceFieldGenerated, this, &MainWindow::updateProgressBar);
    connect(m_model, &DistanceFieldModel::stopGeneration, this, &MainWindow::populateUnicodeRanges);
    connect(m_model, &DistanceFieldModel::error, this, &MainWindow::displayError);
}

void MainWindow::saveAs()
{
    QString fileName = QFileDialog::getSaveFileName(this,
                                                    tr("Save distance field-enriched file"),
                                                    m_fontDir,
                                                    tr("Font files (*.ttf *.otf);;All files (*)"));
    if (!fileName.isEmpty()) {
        m_fileName = fileName;
        m_fontDir = QFileInfo(m_fileName).absolutePath();
        m_settings.setValue(QStringLiteral("fontDirectory"), m_fontDir);
        save();
    }
}


#   pragma pack(1)
struct FontDirectoryHeader
{
    quint32 sfntVersion;
    quint16 numTables;
    quint16 searchRange;
    quint16 entrySelector;
    quint16 rangeShift;
};

struct TableRecord
{
    quint32 tag;
    quint32 checkSum;
    quint32 offset;
    quint32 length;
};

struct QtdfHeader
{
    quint8 majorVersion;
    quint8 minorVersion;
    quint16 pixelSize;
    quint32 textureSize;
    quint8 flags;
    quint8 padding;
    quint32 numGlyphs;
};

struct QtdfGlyphRecord
{
    quint32 glyphIndex;
    quint32 textureOffsetX;
    quint32 textureOffsetY;
    quint32 textureWidth;
    quint32 textureHeight;
    quint32 xMargin;
    quint32 yMargin;
    qint32 boundingRectX;
    qint32 boundingRectY;
    quint32 boundingRectWidth;
    quint32 boundingRectHeight;
    quint16 textureIndex;
};

struct QtdfTextureRecord
{
    quint32 allocatedX;
    quint32 allocatedY;
    quint32 allocatedWidth;
    quint32 allocatedHeight;
    quint8 padding;
};

struct Head
{
    quint16 majorVersion;
    quint16 minorVersion;
    quint32 fontRevision;
    quint32 checkSumAdjustment;
};
#   pragma pack()

#define PAD_BUFFER(buffer, size) \
    { \
        int paddingNeed = size % 4; \
        if (paddingNeed > 0) { \
            const char padding[3] = { 0, 0, 0 }; \
            buffer.write(padding, 4 - paddingNeed); \
        } \
    }

#define ALIGN_OFFSET(offset) \
    { \
        int paddingNeed = offset % 4; \
        if (paddingNeed > 0) \
            offset += 4 - paddingNeed; \
    }

#define TO_FIXED_POINT(value) \
    ((int)(value*qreal(65536)))

void MainWindow::save()
{
    QModelIndexList list = ui->lvGlyphs->selectionModel()->selectedIndexes();
    if (list.isEmpty()) {
        QMessageBox::warning(this,
                             tr("Nothing to save"),
                             tr("No glyphs selected for saving."),
                             QMessageBox::Ok);
        return;
    }

    if (m_fileName.isEmpty()) {
        saveAs();
        return;
    }

    QFile inFile(m_fontFile);
    if (!inFile.open(QIODevice::ReadOnly)) {
        QMessageBox::warning(this,
                             tr("Can't read original font"),
                             tr("Cannot open '%s' for reading. The original font file must remain in place until the new file has been saved.").arg(m_fontFile),
                             QMessageBox::Ok);
        return;
    }

    QByteArray output;
    quint32 headOffset = 0;

    {
        QBuffer outBuffer(&output);
        outBuffer.open(QIODevice::WriteOnly);

        uchar *inData = inFile.map(0, inFile.size());
        if (inData == nullptr) {
            QMessageBox::warning(this,
                                 tr("Can't map input file"),
                                 tr("Unable to memory map input file '%s'.").arg(m_fontFile));
            return;
        }

        uchar *end = inData + inFile.size();
        if (inData + sizeof(FontDirectoryHeader) > end) {
            QMessageBox::warning(this,
                                 tr("Can't read font directory"),
                                 tr("Input file seems to be invalid or corrupt."),
                                 QMessageBox::Ok);
            return;
        }

        FontDirectoryHeader fontDirectoryHeader;
        memcpy(&fontDirectoryHeader, inData, sizeof(FontDirectoryHeader));
        quint16 numTables = qFromBigEndian(fontDirectoryHeader.numTables) + 1;
        fontDirectoryHeader.numTables = qToBigEndian(numTables);
        {
            quint16 searchRange = qFromBigEndian(fontDirectoryHeader.searchRange);
            if (searchRange / 16 < numTables) {
                quint16 pot = (searchRange / 16) * 2;
                searchRange = pot * 16;
                fontDirectoryHeader.searchRange = qToBigEndian(searchRange);
                fontDirectoryHeader.rangeShift = qToBigEndian(numTables * 16 - searchRange);

                quint16 entrySelector = 0;
                while (pot > 1) {
                    pot >>= 1;
                    entrySelector++;
                }
                fontDirectoryHeader.entrySelector = qToBigEndian(entrySelector);
            }
        }

        outBuffer.write(reinterpret_cast<char *>(&fontDirectoryHeader),
                        sizeof(FontDirectoryHeader));

        QVarLengthArray<QPair<quint32, quint32>> offsetLengthPairs;
        offsetLengthPairs.reserve(numTables - 1);

        // Copy the offset table, updating offsets
        TableRecord *offsetTable = reinterpret_cast<TableRecord *>(inData + sizeof(FontDirectoryHeader));
        quint32 currentOffset = sizeof(FontDirectoryHeader) + sizeof(TableRecord) * numTables;
        for (int i = 0; i < numTables - 1; ++i) {
            ALIGN_OFFSET(currentOffset)

            quint32 originalOffset = qFromBigEndian(offsetTable->offset);
            quint32 length = qFromBigEndian(offsetTable->length);
            offsetLengthPairs.append(qMakePair(originalOffset, length));
            if (offsetTable->tag == qToBigEndian(MAKE_TAG('h', 'e', 'a', 'd')))
                headOffset = currentOffset;

            TableRecord newTableRecord;
            memcpy(&newTableRecord, offsetTable, sizeof(TableRecord));
            newTableRecord.offset = qToBigEndian(currentOffset);
            outBuffer.write(reinterpret_cast<char *>(&newTableRecord), sizeof(TableRecord));

            offsetTable++;
            currentOffset += length;
        }

        if (headOffset == 0) {
            QMessageBox::warning(this,
                                 tr("Invalid font file"),
                                 tr("Font file does not have 'head' table."),
                                 QMessageBox::Ok);
            return;
        }

        QByteArray qtdf = createSfntTable();
        if (qtdf.isEmpty())
            return;

        {
            ALIGN_OFFSET(currentOffset)

            TableRecord qtdfRecord;
            qtdfRecord.offset = qToBigEndian(currentOffset);
            qtdfRecord.length = qToBigEndian(qtdf.length());
            qtdfRecord.tag = qToBigEndian(MAKE_TAG('q', 't', 'd', 'f'));
            quint32 checkSum = 0;
            const quint32 *start = reinterpret_cast<const quint32 *>(qtdf.constData());
            const quint32 *end = reinterpret_cast<const quint32 *>(qtdf.constData() + qtdf.length());
            while (start < end)
                checkSum += *(start++);
            qtdfRecord.checkSum = qToBigEndian(checkSum);

            outBuffer.write(reinterpret_cast<char *>(&qtdfRecord),
                            sizeof(TableRecord));
        }

        // Copy all font tables
        for (const QPair<quint32, quint32> &offsetLengthPair : offsetLengthPairs) {
            PAD_BUFFER(outBuffer, output.size())
            outBuffer.write(reinterpret_cast<char *>(inData + offsetLengthPair.first),
                            offsetLengthPair.second);
        }

        PAD_BUFFER(outBuffer, output.size())
        outBuffer.write(qtdf);
    }

    // Clear 'head' checksum and calculate new check sum adjustment
    Head *head = reinterpret_cast<Head *>(output.data() + headOffset);
    head->checkSumAdjustment = 0;

    quint32 checkSum = 0;
    const quint32 *start = reinterpret_cast<const quint32 *>(output.constData());
    const quint32 *end = reinterpret_cast<const quint32 *>(output.constData() + output.length());
    while (start < end)
        checkSum += *(start++);

    head->checkSumAdjustment = qToBigEndian(0xB1B0AFBA - checkSum);

    QFile outFile(m_fileName);
    if (!outFile.open(QIODevice::WriteOnly)) {
        QMessageBox::warning(this,
                             tr("Can't write to file"),
                             tr("Cannot open the file '%s' for writing").arg(m_fileName),
                             QMessageBox::Ok);
        return;
    }

    outFile.write(output);
}

QByteArray MainWindow::createSfntTable()
{
    QModelIndexList list = ui->lvGlyphs->selectionModel()->selectedIndexes();
    Q_ASSERT(!list.isEmpty());

    QByteArray ret;
    {
        QBuffer buffer(&ret);
        buffer.open(QIODevice::WriteOnly);

        QtdfHeader header;
        header.majorVersion = 5;
        header.minorVersion = 12;
        header.pixelSize = qToBigEndian(quint16(qRound(m_model->pixelSize())));

        const quint8 padding = 2;
        qreal scaleFactor = qreal(1) / QT_DISTANCEFIELD_SCALE(m_model->doubleGlyphResolution());
        const int radius = QT_DISTANCEFIELD_RADIUS(m_model->doubleGlyphResolution())
                / QT_DISTANCEFIELD_SCALE(m_model->doubleGlyphResolution());

        quint32 textureSize = ui->sbMaximumTextureSize->value();

        // Since we are using a single area allocator that spans all textures, we need
        // to split the textures one row before the actual maximum size, otherwise
        // glyphs that fall on the edge between two textures will expand the texture
        // they are assigned to, and this will end up being larger than the max.
        textureSize -= quint32(qCeil(m_model->pixelSize() * scaleFactor) + radius * 2 + padding * 2);
        header.textureSize = qToBigEndian(textureSize);

        header.padding = padding;
        header.flags = m_model->doubleGlyphResolution() ? 1 : 0;
        header.numGlyphs = qToBigEndian(quint32(list.size()));
        buffer.write(reinterpret_cast<char *>(&header),
                     sizeof(QtdfHeader));

        // Maximum height allocator to find optimal number of textures
        QVector<QRect> allocatedAreaPerTexture;

        struct GlyphData {
            QSGDistanceFieldGlyphCache::TexCoord texCoord;
            QRectF boundingRect;
            QSize glyphSize;
            int textureIndex;
        };
        QVector<GlyphData> glyphDatas;
        glyphDatas.resize(m_model->rowCount());

        int textureCount = 0;

        {
            QTransform scaleDown;
            scaleDown.scale(scaleFactor, scaleFactor);

            {
                bool foundOptimalSize = false;
                while (!foundOptimalSize) {
                    allocatedAreaPerTexture.clear();

                    QSGAreaAllocator allocator(QSize(textureSize, textureSize * (++textureCount)));

                    int i;
                    for (i = 0; i < list.size(); ++i) {
                        int glyphIndex = list.at(i).row();
                        GlyphData &glyphData = glyphDatas[glyphIndex];

                        QPainterPath path = m_model->path(glyphIndex);
                        glyphData.boundingRect = scaleDown.mapRect(path.boundingRect());
                        int glyphWidth = qCeil(glyphData.boundingRect.width()) + radius * 2;
                        int glyphHeight = qCeil(glyphData.boundingRect.height()) + radius * 2;

                        glyphData.glyphSize = QSize(glyphWidth + padding * 2, glyphHeight + padding * 2);

                        if (glyphData.glyphSize.width() > qint32(textureSize)
                                || glyphData.glyphSize.height() > qint32(textureSize)) {
                            QMessageBox::warning(this,
                                                 tr("Glyph too large for texture"),
                                                 tr("Glyph %1 is too large to fit in texture of size %2.")
                                                 .arg(glyphIndex).arg(textureSize));
                            return QByteArray();
                        }

                        QRect rect = allocator.allocate(glyphData.glyphSize);
                        if (rect.isNull())
                            break;

                        glyphData.textureIndex = rect.y() / textureSize;
                        while (glyphData.textureIndex >= allocatedAreaPerTexture.size())
                            allocatedAreaPerTexture.append(QRect(0, 0, 1, 1));

                        allocatedAreaPerTexture[glyphData.textureIndex] |= QRect(rect.x(),
                                                            rect.y() % textureSize,
                                                            rect.width(),
                                                            rect.height());

                        glyphData.texCoord.xMargin = QT_DISTANCEFIELD_RADIUS(m_model->doubleGlyphResolution()) / qreal(QT_DISTANCEFIELD_SCALE(m_model->doubleGlyphResolution()));
                        glyphData.texCoord.yMargin = QT_DISTANCEFIELD_RADIUS(m_model->doubleGlyphResolution()) / qreal(QT_DISTANCEFIELD_SCALE(m_model->doubleGlyphResolution()));
                        glyphData.texCoord.x = rect.x() + padding;
                        glyphData.texCoord.y = rect.y() % textureSize + padding;
                        glyphData.texCoord.width = glyphData.boundingRect.width();
                        glyphData.texCoord.height = glyphData.boundingRect.height();

                        glyphDatas.append(glyphData);
                    }

                    foundOptimalSize = i == list.size();
                    if (foundOptimalSize)
                        buffer.write(allocator.serialize());
                }
            }
        }

        QVector<QDistanceField> textures;
        textures.resize(textureCount);

        for (int textureIndex = 0; textureIndex < textureCount; ++textureIndex) {
            textures[textureIndex] = QDistanceField(allocatedAreaPerTexture.at(textureIndex).width(),
                                                    allocatedAreaPerTexture.at(textureIndex).height());

            QRect rect = allocatedAreaPerTexture.at(textureIndex);

            QtdfTextureRecord record;
            record.allocatedX = qToBigEndian(rect.x());
            record.allocatedY = qToBigEndian(rect.y());
            record.allocatedWidth = qToBigEndian(rect.width());
            record.allocatedHeight = qToBigEndian(rect.height());
            record.padding = padding;
            buffer.write(reinterpret_cast<char *>(&record),
                         sizeof(QtdfTextureRecord));
        }

        {
            for (int i = 0; i < list.size(); ++i) {
                int glyphIndex = list.at(i).row();
                QImage image = m_model->distanceField(glyphIndex);

                const GlyphData &glyphData = glyphDatas.at(glyphIndex);

                QtdfGlyphRecord glyphRecord;
                glyphRecord.glyphIndex = qToBigEndian(glyphIndex);
                glyphRecord.textureOffsetX = qToBigEndian(TO_FIXED_POINT(glyphData.texCoord.x));
                glyphRecord.textureOffsetY = qToBigEndian(TO_FIXED_POINT(glyphData.texCoord.y));
                glyphRecord.textureWidth = qToBigEndian(TO_FIXED_POINT(glyphData.texCoord.width));
                glyphRecord.textureHeight = qToBigEndian(TO_FIXED_POINT(glyphData.texCoord.height));
                glyphRecord.xMargin = qToBigEndian(TO_FIXED_POINT(glyphData.texCoord.xMargin));
                glyphRecord.yMargin = qToBigEndian(TO_FIXED_POINT(glyphData.texCoord.yMargin));
                glyphRecord.boundingRectX = qToBigEndian(TO_FIXED_POINT(glyphData.boundingRect.x()));
                glyphRecord.boundingRectY = qToBigEndian(TO_FIXED_POINT(glyphData.boundingRect.y()));
                glyphRecord.boundingRectWidth = qToBigEndian(TO_FIXED_POINT(glyphData.boundingRect.width()));
                glyphRecord.boundingRectHeight = qToBigEndian(TO_FIXED_POINT(glyphData.boundingRect.height()));
                glyphRecord.textureIndex = qToBigEndian(quint16(glyphData.textureIndex));
                buffer.write(reinterpret_cast<char *>(&glyphRecord), sizeof(QtdfGlyphRecord));

                int expectedWidth = qCeil(glyphData.texCoord.width + glyphData.texCoord.xMargin * 2);
                image = image.copy(-padding, -padding,
                                   expectedWidth + padding  * 2,
                                   image.height() + padding * 2);

                uchar *inBits = image.scanLine(0);
                uchar *outBits = textures[glyphData.textureIndex].scanLine(int(glyphData.texCoord.y) - padding)
                                    + int(glyphData.texCoord.x) - padding;
                for (int y = 0; y < image.height(); ++y) {
                    memcpy(outBits, inBits, image.width());
                    inBits += image.bytesPerLine();
                    outBits += textures[glyphData.textureIndex].width();
                }
            }
        }

        for (int i = 0; i < textures.size(); ++i) {
            const QDistanceField &texture = textures.at(i);
            const QRect &allocatedArea = allocatedAreaPerTexture.at(i);
            buffer.write(reinterpret_cast<const char *>(texture.constBits()),
                       allocatedArea.width() * allocatedArea.height());
        }

        PAD_BUFFER(buffer, ret.size())
    }

    return ret;
}

void MainWindow::writeFile()
{
    Q_ASSERT(!m_fileName.isEmpty());

    QFile file(m_fileName);
    if (file.open(QIODevice::WriteOnly)) {

    } else {
        QMessageBox::warning(this,
                             tr("Can't open file for writing"),
                             tr("Unable to open file '%1' for writing").arg(m_fileName),
                             QMessageBox::Ok);
    }
}

void MainWindow::openFont()
{
    QString fileName = QFileDialog::getOpenFileName(this,
                                                    tr("Open font file"),
                                                    m_fontDir,
                                                    tr("Fonts (*.ttf *.otf);;All files (*)"));
    if (!fileName.isEmpty())
        open(fileName);
}

void MainWindow::updateProgressBar()
{
    m_statusBarProgressBar->setValue(m_statusBarProgressBar->value() + 1);
    updateSelection();
}

void MainWindow::startProgressBar(quint16 glyphCount)
{
    ui->action_Open->setDisabled(false);
    m_statusBarLabel->setText(tr("Generating"));
    m_statusBarProgressBar->setMaximum(glyphCount);
    m_statusBarProgressBar->setMinimum(0);
    m_statusBarProgressBar->setValue(0);
    m_statusBarProgressBar->setVisible(true);
}

void MainWindow::stopProgressBar()
{
    m_statusBarLabel->setText(tr("Ready"));
    m_statusBarProgressBar->setVisible(false);
}

void MainWindow::selectAll()
{
    QModelIndexList list = ui->lvGlyphs->selectionModel()->selectedIndexes();
    if (list.size() == ui->lvGlyphs->model()->rowCount())
        ui->lvGlyphs->clearSelection();
    else
        ui->lvGlyphs->selectAll();
}

void MainWindow::updateSelection()
{
    QModelIndexList list = ui->lvGlyphs->selectionModel()->selectedIndexes();
    QString label;
    if (list.size() == ui->lvGlyphs->model()->rowCount())
        label = tr("Deselect &All");
    else
        label = tr("Select &All");

    ui->tbSelectAll->setText(label);
    ui->actionSelect_all->setText(label);

    if (m_model != nullptr && ui->lwUnicodeRanges->count() > 0) {
        // Ignore selection changes until we are done
        disconnect(ui->lwUnicodeRanges, &QListWidget::itemSelectionChanged, this, &MainWindow::updateUnicodeRanges);

        QSet<int> selectedGlyphIndexes;
        for (const QModelIndex &modelIndex : list)
            selectedGlyphIndexes.insert(modelIndex.row());

        QList<DistanceFieldModel::UnicodeRange> unicodeRanges = m_model->unicodeRanges();
        std::sort(unicodeRanges.begin(), unicodeRanges.end());

        Q_ASSERT(ui->lwUnicodeRanges->count() == unicodeRanges.size());
        for (int i = 0; i < unicodeRanges.size(); ++i) {
            DistanceFieldModel::UnicodeRange unicodeRange = unicodeRanges.at(i);
            QListWidgetItem *item = ui->lwUnicodeRanges->item(i);

            QList<glyph_t> glyphIndexes = m_model->glyphIndexesForUnicodeRange(unicodeRange);
            Q_ASSERT(!glyphIndexes.isEmpty());

            item->setSelected(true);
            for (glyph_t glyphIndex : glyphIndexes) {
                if (!selectedGlyphIndexes.contains(glyphIndex)) {
                    item->setSelected(false);
                    break;
                }
            }
        }

        connect(ui->lwUnicodeRanges, &QListWidget::itemSelectionChanged, this, &MainWindow::updateUnicodeRanges);
    }
}

void MainWindow::updateUnicodeRanges()
{
    if (m_model == nullptr)
        return;

    disconnect(ui->lvGlyphs->selectionModel(),
               &QItemSelectionModel::selectionChanged,
               this,
               &MainWindow::updateSelection);

    for (int i = 0; i < ui->lwUnicodeRanges->count(); ++i) {
        QListWidgetItem *item = ui->lwUnicodeRanges->item(i);
        DistanceFieldModel::UnicodeRange unicodeRange = item->data(Qt::UserRole).value<DistanceFieldModel::UnicodeRange>();
        QList<glyph_t> glyphIndexes = m_model->glyphIndexesForUnicodeRange(unicodeRange);
        for (glyph_t glyphIndex : glyphIndexes) {
            QModelIndex index = m_model->index(glyphIndex);
            ui->lvGlyphs->selectionModel()->select(index, item->isSelected()
                                                            ? QItemSelectionModel::Select
                                                            : QItemSelectionModel::Deselect);
        }
    }

    connect(ui->lvGlyphs->selectionModel(),
            &QItemSelectionModel::selectionChanged,
            this,
            &MainWindow::updateSelection);
}

void MainWindow::populateUnicodeRanges()
{
    QList<DistanceFieldModel::UnicodeRange> unicodeRanges = m_model->unicodeRanges();
    std::sort(unicodeRanges.begin(), unicodeRanges.end());

    for (DistanceFieldModel::UnicodeRange unicodeRange : unicodeRanges) {
        QString name = m_model->nameForUnicodeRange(unicodeRange);
        QListWidgetItem *item = new QListWidgetItem(name, ui->lwUnicodeRanges);
        item->setData(Qt::UserRole, unicodeRange);
    }

    ui->lwUnicodeRanges->setDisabled(false);
    ui->action_Save->setDisabled(false);
    ui->action_Save_as->setDisabled(false);
    ui->tbSave->setDisabled(false);
}

void MainWindow::displayError(const QString &errorString)
{
    QMessageBox::warning(this, tr("Error when parsing font file"), errorString, QMessageBox::Ok);
}

void MainWindow::selectString()
{
    QString s = QInputDialog::getText(this,
                                      tr("Select glyphs for string"),
                                      tr("String to parse:"));
    if (!s.isEmpty()) {
        QVector<uint> ucs4String = s.toUcs4();
        for (uint ucs4 : ucs4String) {
            glyph_t glyph = m_model->glyphIndexForUcs4(ucs4);
            if (glyph != 0) {
                ui->lvGlyphs->selectionModel()->select(m_model->index(glyph),
                                                       QItemSelectionModel::Select);
            }
        }
    }
}

void MainWindow::about()
{
    QMessageBox *msgBox = new QMessageBox(this);
    msgBox->setAttribute(Qt::WA_DeleteOnClose);
    msgBox->setWindowTitle(tr("About Qt Distance Field Generator"));
    msgBox->setText(tr("<h3>Qt Distance Field Generator</h3>"
                       "<p>Version %1.<br/>"
                       "The Qt Distance Field Generator tool allows "
                       "to prepare a font cache for Qt applications.</p>"
                       "<p>Copyright (C) %2 The Qt Company Ltd.</p>")
                    .arg(QLatin1String(QT_VERSION_STR))
                    .arg(QLatin1String("2019")));
    msgBox->show();
}

QT_END_NAMESPACE
