/*
  This file is part of KDDockWidgets.

  Copyright (C) 2018-2020 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com
  Author: Sérgio Martins <sergio.martins@kdab.com>

  This program is free software; you can redistribute it and/or modify
  it under the terms of the GNU 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 General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

/**
 * @file
 * @brief Class to save and restore dock widget layouts.
 *
 * @author Sérgio Martins \<sergio.martins@kdab.com\>
 */

#include "LayoutSaver.h"
#include "LayoutSaver_p.h"
#include "Config.h"
#include "DockRegistry_p.h"
#include "DockWidgetBase.h"
#include "DropArea_p.h"
#include "Logging_p.h"
#include "Frame_p.h"
#include "Position_p.h"
#include "multisplitter/Item_p.h"
#include "FrameworkWidgetFactory.h"
#include "MainWindow.h"

#include <qmath.h>
#include <QDebug>
#include <QSettings>
#include <QApplication>
#include <QFile>

#include <memory>

using namespace KDDockWidgets;

QHash<QString, LayoutSaver::DockWidget::Ptr> LayoutSaver::DockWidget::s_dockWidgets;
LayoutSaver::Layout* LayoutSaver::Layout::s_currentLayoutBeingRestored = nullptr;

class KDDockWidgets::LayoutSaver::Private
{
public:

    struct RAIIIsRestoring
    {
        RAIIIsRestoring()
        {
            LayoutSaver::Private::s_restoreInProgress = true;
        }

        ~RAIIIsRestoring()
        {
            LayoutSaver::Private::s_restoreInProgress = false;
        }
        Q_DISABLE_COPY(RAIIIsRestoring)
    };

    Private(RestoreOptions options)
        : m_dockRegistry(DockRegistry::self())
        , m_restoreOptions(options)
    {
    }

    bool matchesAffinity(const QStringList &affinities) const {
        return m_affinityNames.isEmpty() || affinities.isEmpty() || DockRegistry::self()->affinitiesMatch(m_affinityNames, affinities);
    }

    template <typename T>
    void deserializeWindowGeometry(const T &saved, QWidgetOrQuick *topLevel);
    void deleteEmptyFrames();
    void clearRestoredProperty();

    std::unique_ptr<QSettings> settings() const;
    DockRegistry *const m_dockRegistry;
    const RestoreOptions m_restoreOptions;
    QStringList m_affinityNames;

    static bool s_restoreInProgress;
};

bool LayoutSaver::Private::s_restoreInProgress = false;


static QVariantList stringListToVariant(const QStringList &strs)
{
    QVariantList variantList;
    variantList.reserve(strs.size());
    for (const QString &str : strs)
        variantList.push_back(str);

    return variantList;
}

static QStringList variantToStringList(const QVariantList &variantList)
{
    QStringList stringList;
    stringList.reserve(variantList.size());
    for (const QVariant &variant : variantList)
        stringList.push_back(variant.toString());

    return stringList;
}

LayoutSaver::LayoutSaver(RestoreOptions options)
    : d(new Private(options))
{
}

LayoutSaver::~LayoutSaver()
{
    delete d;
}

bool LayoutSaver::saveToFile(const QString &jsonFilename)
{
    const QByteArray data = serializeLayout();

    QFile f(jsonFilename);
    if (!f.open(QIODevice::WriteOnly)) {
        qWarning() << Q_FUNC_INFO << "Failed to open" << jsonFilename << f.errorString();
        return false;
    }

    f.write(data);
    return true;
}

bool LayoutSaver::restoreFromFile(const QString &jsonFilename)
{
    QFile f(jsonFilename);
    if (!f.open(QIODevice::ReadOnly)) {
        qWarning() << Q_FUNC_INFO << "Failed to open" << jsonFilename << f.errorString();
        return false;
    }

    const QByteArray data = f.readAll();
    const bool result = restoreLayout(data);

    return result;
}

QByteArray LayoutSaver::serializeLayout() const
{
    if (!d->m_dockRegistry->isSane()) {
        qWarning() << Q_FUNC_INFO << "Refusing to serialize this layout. Check previous warnings.";
        return {};
    }

    LayoutSaver::Layout layout;

    // Just a simplification. One less type of windows to handle.
    d->m_dockRegistry->ensureAllFloatingWidgetsAreMorphed();

    const MainWindowBase::List mainWindows = d->m_dockRegistry->mainwindows();
    layout.mainWindows.reserve(mainWindows.size());
    for (MainWindowBase *mainWindow : mainWindows) {
        if (d->matchesAffinity(mainWindow->affinities()))
            layout.mainWindows.push_back(mainWindow->serialize());
    }

    const QVector<KDDockWidgets::FloatingWindow*> floatingWindows = d->m_dockRegistry->nestedwindows();
    layout.floatingWindows.reserve(floatingWindows.size());
    for (KDDockWidgets::FloatingWindow *floatingWindow : floatingWindows) {
        if (d->matchesAffinity(floatingWindow->affinities()))
            layout.floatingWindows.push_back(floatingWindow->serialize());
    }

    // Closed dock widgets also have interesting things to save, like geometry and placeholder info
    const DockWidgetBase::List closedDockWidgets = d->m_dockRegistry->closedDockwidgets();
    layout.closedDockWidgets.reserve(closedDockWidgets.size());
    for (DockWidgetBase *dockWidget : closedDockWidgets) {
        if (d->matchesAffinity(dockWidget->affinities()))
            layout.closedDockWidgets.push_back(dockWidget->serialize());
    }

    // Save the placeholder info. We do it last, as we also restore it last, since we need all items to be created
    // before restoring the placeholders

    const DockWidgetBase::List dockWidgets = d->m_dockRegistry->dockwidgets();
    layout.allDockWidgets.reserve(dockWidgets.size());
    for (DockWidgetBase *dockWidget : dockWidgets) {
        if (d->matchesAffinity(dockWidget->affinities())) {
            auto dw = dockWidget->serialize();
            dw->lastPosition = dockWidget->lastPositions().serialize();
            layout.allDockWidgets.push_back(dw);
        }
    }

    return layout.toJson();
}

bool LayoutSaver::restoreLayout(const QByteArray &data)
{
    d->clearRestoredProperty();
    if (data.isEmpty())
        return true;

    Private::RAIIIsRestoring isRestoring;

    struct FrameCleanup {
        FrameCleanup(LayoutSaver *saver)
            : m_saver(saver)
        {
        }

        ~FrameCleanup()
        {
            m_saver->d->deleteEmptyFrames();
        }

        LayoutSaver *const m_saver;
    };

    FrameCleanup cleanup(this);
    LayoutSaver::Layout layout;
    if (!layout.fromJson(data)) {
        qWarning() << Q_FUNC_INFO << "Failed to parse json data";
        return false;
    }

    if (!layout.isValid()) {
        return false;
    }

    if (d->m_restoreOptions & RestoreOption_RelativeToMainWindow)
        layout.scaleSizes();

    // Hide all dockwidgets and unparent them from any layout before starting restore
    // We only close the stuff that the loaded JSON knows about. Unknown widgets might be newer.
    d->m_dockRegistry->clear(d->m_dockRegistry->dockWidgets(layout.dockWidgetNames()),
                             d->m_dockRegistry->mainWindows(layout.mainWindowNames()),
                             d->m_affinityNames);

    // 1. Restore main windows
    for (const LayoutSaver::MainWindow &mw : qAsConst(layout.mainWindows)) {
        MainWindowBase *mainWindow = d->m_dockRegistry->mainWindowByName(mw.uniqueName);
        if (!mainWindow ) {
            if (auto mwFunc = Config::self().mainWindowFactoryFunc()) {
                mainWindow = mwFunc(mw.uniqueName);
            } else {
                qWarning() << "Failed to restore layout create MainWindow with name" << mw.uniqueName << "first";
                return false;
            }
        }

        if (!d->matchesAffinity(mainWindow->affinities()))
            continue;

        if (!(d->m_restoreOptions & RestoreOption_RelativeToMainWindow))
            d->deserializeWindowGeometry(mw, mainWindow->window()); // window(), as the MainWindow can be embedded

        if (!mainWindow->deserialize(mw))
            return false;
    }

    // 2. Restore FloatingWindows
    for (const LayoutSaver::FloatingWindow &fw : qAsConst(layout.floatingWindows)) {
        if (!d->matchesAffinity(fw.affinities))
            continue;

        MainWindowBase *parent = fw.parentIndex == -1 ? nullptr
                                                      : DockRegistry::self()->mainwindows().at(fw.parentIndex);

        auto floatingWindow = Config::self().frameworkWidgetFactory()->createFloatingWindow(parent);
        d->deserializeWindowGeometry(fw, floatingWindow);
        if (!floatingWindow->deserialize(fw)) {
            qWarning() << Q_FUNC_INFO << "Failed to deserialize floating window";
            return false;
        }
    }

    // 3. Restore closed dock widgets. They remain closed but acquire geometry and placeholder properties
    for (const auto &dw : qAsConst(layout.closedDockWidgets)) {
        if (d->matchesAffinity(dw->affinities)) {
            DockWidgetBase::deserialize(dw);
        }
    }

    // 4. Restore the placeholder info, now that the Items have been created
    for (const auto &dw : qAsConst(layout.allDockWidgets)) {
        if (!d->matchesAffinity(dw->affinities))
            continue;

        if (DockWidgetBase *dockWidget = d->m_dockRegistry->dockByName(dw->uniqueName)) {
            dockWidget->lastPositions().deserialize(dw->lastPosition);
        } else {
            qWarning() << Q_FUNC_INFO << "Couldn't find dock widget" << dw->uniqueName;
        }
    }

    return true;
}

void LayoutSaver::setAffinityNames(const QStringList &affinityNames)
{
    d->m_affinityNames = affinityNames;
    if (affinityNames.contains(QString())) {
        // Any window with empty affinity will also be subject to save/restore
        d->m_affinityNames << QString();
    }
}

DockWidgetBase::List LayoutSaver::restoredDockWidgets() const
{
    const DockWidgetBase::List &allDockWidgets = DockRegistry::self()->dockwidgets();
    DockWidgetBase::List result;
    result.reserve(allDockWidgets.size());
    for (DockWidgetBase *dw : allDockWidgets) {
        if (dw->property("kddockwidget_was_restored").toBool())
            result.push_back(dw);
    }

    return result;
}

void LayoutSaver::Private::clearRestoredProperty()
{
    const DockWidgetBase::List &allDockWidgets = DockRegistry::self()->dockwidgets();
    for (DockWidgetBase *dw : allDockWidgets) {
        dw->setProperty("kddockwidget_was_restored", QVariant());
    }
}

template <typename T>
void LayoutSaver::Private::deserializeWindowGeometry(const T &saved, QWidgetOrQuick *topLevel)
{
    topLevel->setGeometry(saved.geometry);
    topLevel->setVisible(saved.isVisible);
}

void LayoutSaver::Private::deleteEmptyFrames()
{
    // After a restore it can happen that some DockWidgets didn't exist, so weren't restored.
    // Delete their frame now.

    for (auto frame : m_dockRegistry->frames()) {
        if (!frame->beingDeletedLater() && frame->isEmpty() && !frame->isCentralFrame())
            delete frame;
    }
}

std::unique_ptr<QSettings> LayoutSaver::Private::settings() const
{
    auto settings = std::unique_ptr<QSettings>(new QSettings(qApp->organizationName(),
                                                             qApp->applicationName()));
    settings->beginGroup(QStringLiteral("KDDockWidgets::LayoutSaver"));

    return settings;
}

bool LayoutSaver::restoreInProgress()
{
    return Private::s_restoreInProgress;
}

bool LayoutSaver::Layout::isValid() const
{
    if (serializationVersion != KDDOCKWIDGETS_SERIALIZATION_VERSION) {
        qWarning() << Q_FUNC_INFO << "Serialization format is too old"
                   << serializationVersion << "current=" << KDDOCKWIDGETS_SERIALIZATION_VERSION;
        return false;
    }

    for (auto &m : mainWindows) {
        if (!m.isValid())
            return false;
    }

    for (auto &m : floatingWindows) {
        if (!m.isValid())
            return false;
    }

    for (auto &m : allDockWidgets) {
        if (!m->isValid())
            return false;
    }

    return true;
}

QByteArray LayoutSaver::Layout::toJson() const
{
    QJsonDocument doc = QJsonDocument::fromVariant(toVariantMap());
    return doc.toJson();
}

bool LayoutSaver::Layout::fromJson(const QByteArray &jsonData)
{
    QJsonParseError error;
    QJsonDocument doc = QJsonDocument::fromJson(jsonData, &error);
    if (error.error == QJsonParseError::NoError)  {
        fromVariantMap(doc.toVariant().toMap());
        return true;
    }

    return false;
}

QVariantMap LayoutSaver::Layout::toVariantMap() const
{
    QVariantMap map;
    map.insert(QStringLiteral("serializationVersion"), serializationVersion);
    map.insert(QStringLiteral("mainWindows"), toVariantList<LayoutSaver::MainWindow>(mainWindows));
    map.insert(QStringLiteral("floatingWindows"), toVariantList<LayoutSaver::FloatingWindow>(floatingWindows));
    map.insert(QStringLiteral("closedDockWidgets"), ::dockWidgetNames(closedDockWidgets));
    map.insert(QStringLiteral("allDockWidgets"), toVariantList(allDockWidgets));
    map.insert(QStringLiteral("screenInfo"), toVariantList<LayoutSaver::ScreenInfo>(screenInfo));

    return map;
}

void LayoutSaver::Layout::fromVariantMap(const QVariantMap &map)
{
    allDockWidgets.clear();
    const QVariantList dockWidgetsV = map.value(QStringLiteral("allDockWidgets")).toList();
    for (const QVariant &v : dockWidgetsV) {
        const QVariantMap dwV = v.toMap();
        const QString name = dwV.value(QStringLiteral("uniqueName")).toString();
        auto dw = LayoutSaver::DockWidget::dockWidgetForName(name);
        dw->fromVariantMap(dwV);
        allDockWidgets.push_back(dw);
    }

    closedDockWidgets.clear();
    const QVariantList closedDockWidgetsV = map.value(QStringLiteral("closedDockWidgets")).toList();
    closedDockWidgets.reserve(closedDockWidgetsV.size());
    for (const QVariant &v : closedDockWidgetsV) {
        closedDockWidgets.push_back(LayoutSaver::DockWidget::dockWidgetForName(v.toString()));
    }

    serializationVersion = map.value(QStringLiteral("serializationVersion")).toInt();
    mainWindows = fromVariantList<LayoutSaver::MainWindow>(map.value(QStringLiteral("mainWindows")).toList());
    floatingWindows = fromVariantList<LayoutSaver::FloatingWindow>(map.value(QStringLiteral("floatingWindows")).toList());
    screenInfo = fromVariantList<LayoutSaver::ScreenInfo>(map.value(QStringLiteral("screenInfo")).toList());
}

void LayoutSaver::Layout::scaleSizes()
{
    if (mainWindows.isEmpty())
        return;

    for (auto &mw : mainWindows)
        mw.scaleSizes();

    for (auto &fw : floatingWindows) {
        LayoutSaver::MainWindow mw = mainWindowForIndex(fw.parentIndex);
        if (mw.scalingInfo.isValid())
            fw.scaleSizes(mw.scalingInfo);
    }

    const ScalingInfo firstScalingInfo = mainWindows.constFirst().scalingInfo;
    if (firstScalingInfo.isValid()) {
        for (auto &dw : allDockWidgets) {
            // TODO: Determine the best main window. This only interesting for closed dock widget geometry
            // which was previously floating. But they still have some other main window as parent.
            dw->scaleSizes(firstScalingInfo);
        }
    }
}

LayoutSaver::MainWindow LayoutSaver::Layout::mainWindowForIndex(int index) const
{
    if (index < 0 || index >= mainWindows.size())
        return {};

    return mainWindows.at(index);
}

QStringList LayoutSaver::Layout::mainWindowNames() const
{
    QStringList names;
    names.reserve(mainWindows.size());
    for (const auto &mw : mainWindows) {
        names << mw.uniqueName;
    }

    return names;
}

QStringList LayoutSaver::Layout::dockWidgetNames() const
{
    QStringList names;
    names.reserve(allDockWidgets.size());
    for (const auto &dw : allDockWidgets) {
        names << dw->uniqueName;
    }

    return names;
}

bool LayoutSaver::Frame::isValid() const
{
    if (isNull)
        return true;

    if (!geometry.isValid()) {
        qWarning() << Q_FUNC_INFO << "Invalid geometry";
        return false;
    }

    if (id.isEmpty()) {
        qWarning() << Q_FUNC_INFO << "Invalid id";
        return false;
    }

    if (options > 3) {
        qWarning() << Q_FUNC_INFO << "Invalid options" << options;
        return false;
    }

    if (!dockWidgets.isEmpty()) {
        if (currentTabIndex >= dockWidgets.size() || currentTabIndex < 0) {
            qWarning() << Q_FUNC_INFO << "Invalid tab index" << currentTabIndex << dockWidgets.size();
            return false;
        }
    }

    for (auto &dw : dockWidgets) {
        if (!dw->isValid())
            return false;
    }

    return true;
}

void LayoutSaver::Frame::scaleSizes(const ScalingInfo &scalingInfo)
{
    scalingInfo.applyFactorsTo(geometry);
}

QVariantMap LayoutSaver::Frame::toVariantMap() const
{
    QVariantMap map;
    map.insert(QStringLiteral("id"), id);
    map.insert(QStringLiteral("isNull"), isNull);
    map.insert(QStringLiteral("objectName"), objectName);
    map.insert(QStringLiteral("geometry"), Layouting::rectToMap(geometry));
    map.insert(QStringLiteral("options"), options);
    map.insert(QStringLiteral("currentTabIndex"), currentTabIndex);

    map.insert(QStringLiteral("dockWidgets"), dockWidgetNames(dockWidgets));

    return map;
}

void LayoutSaver::Frame::fromVariantMap(const QVariantMap &map)
{
    if (map.isEmpty()) {
        isNull = true;
        dockWidgets.clear();
        return;
    }

    id = map.value(QStringLiteral("id")).toString();
    isNull = map.value(QStringLiteral("isNull")).toBool();
    objectName = map.value(QStringLiteral("objectName")).toString();
    geometry = Layouting::mapToRect(map.value(QStringLiteral("geometry")).toMap());
    options = map.value(QStringLiteral("options")).toUInt();
    currentTabIndex = map.value(QStringLiteral("currentTabIndex")).toInt();

    const QVariantList dockWidgetsV = map.value(QStringLiteral("dockWidgets")).toList();

    dockWidgets.clear();
    dockWidgets.reserve(dockWidgetsV.size());
    for (const auto &variant : dockWidgetsV) {
        DockWidget::Ptr dw = DockWidget::dockWidgetForName(variant.toString());
        dockWidgets.push_back(dw);
    }
}

bool LayoutSaver::DockWidget::isValid() const
{
    return !uniqueName.isEmpty();
}

void LayoutSaver::DockWidget::scaleSizes(const ScalingInfo &scalingInfo)
{
    lastPosition.scaleSizes(scalingInfo);
}

QVariantMap LayoutSaver::DockWidget::toVariantMap() const
{
    QVariantMap map;
    if (!affinities.isEmpty())
        map.insert(QStringLiteral("affinities"), stringListToVariant(affinities));
    map.insert(QStringLiteral("uniqueName"), uniqueName);
    map.insert(QStringLiteral("lastPosition"), lastPosition.toVariantMap());

    return map;
}

void LayoutSaver::DockWidget::fromVariantMap(const QVariantMap &map)
{
    affinities = variantToStringList(map.value(QStringLiteral("affinities")).toList());

    // Compatibility hack. Old json format had a single "affinityName" instead of an "affinities" list:
    const QString affinityName = map.value(QStringLiteral("affinityName")).toString();
    if (!affinityName.isEmpty() && !affinities.contains(affinityName)) {
        affinities.push_back(affinityName);
    }

    uniqueName = map.value(QStringLiteral("uniqueName")).toString();
    lastPosition.fromVariantMap(map.value(QStringLiteral("lastPosition")).toMap());
}

bool LayoutSaver::FloatingWindow::isValid() const
{
    if (!multiSplitterLayout.isValid())
        return false;

    if (!geometry.isValid()) {
        qWarning() << Q_FUNC_INFO << "Invalid geometry";
        return false;
    }

    return true;
}

void LayoutSaver::FloatingWindow::scaleSizes(const ScalingInfo &scalingInfo)
{
    scalingInfo.applyFactorsTo(/*by-ref*/geometry);
    multiSplitterLayout.scaleSizes(scalingInfo);
}

QVariantMap LayoutSaver::FloatingWindow::toVariantMap() const
{
    QVariantMap map;
    map.insert(QStringLiteral("multiSplitterLayout"), multiSplitterLayout.toVariantMap());
    map.insert(QStringLiteral("parentIndex"), parentIndex);
    map.insert(QStringLiteral("geometry"), Layouting::rectToMap(geometry));
    map.insert(QStringLiteral("screenIndex"), screenIndex);
    map.insert(QStringLiteral("screenSize"), Layouting::sizeToMap(screenSize));
    map.insert(QStringLiteral("isVisible"), isVisible);

    if (!affinities.isEmpty())
        map.insert(QStringLiteral("affinityName"), stringListToVariant(affinities));

    return map;
}

void LayoutSaver::FloatingWindow::fromVariantMap(const QVariantMap &map)
{
    multiSplitterLayout.fromVariantMap(map.value(QStringLiteral("multiSplitterLayout")).toMap());
    parentIndex = map.value(QStringLiteral("parentIndex")).toInt();
    geometry = Layouting::mapToRect(map.value(QStringLiteral("geometry")).toMap());
    screenIndex = map.value(QStringLiteral("screenIndex")).toInt();
    screenSize = Layouting::mapToSize(map.value(QStringLiteral("screenSize")).toMap());
    isVisible = map.value(QStringLiteral("isVisible")).toBool();
    affinities = variantToStringList(map.value(QStringLiteral("affinities")).toList());

    // Compatibility hack. Old json format had a single "affinityName" instead of an "affinities" list:
    const QString affinityName = map.value(QStringLiteral("affinityName")).toString();
    if (!affinityName.isEmpty() && !affinities.contains(affinityName)) {
        affinities.push_back(affinityName);
    }
}

bool LayoutSaver::MainWindow::isValid() const
{
    if (!multiSplitterLayout.isValid())
        return false;

    if (options != MainWindowOption_None && options != MainWindowOption_HasCentralFrame) {
        qWarning() << Q_FUNC_INFO << "Invalid option" << options;
        return false;
    }

    return true;
}

void LayoutSaver::MainWindow::scaleSizes()
{
    if (scalingInfo.isValid()) {
        // Doesn't happen, it's called only once
        Q_ASSERT(false);
        return;
    }

    scalingInfo = ScalingInfo(uniqueName, geometry);

    if (scalingInfo.isValid())
        multiSplitterLayout.scaleSizes(scalingInfo);
}

QVariantMap LayoutSaver::MainWindow::toVariantMap() const
{
    QVariantMap map;
    map.insert(QStringLiteral("options"), int(options));
    map.insert(QStringLiteral("multiSplitterLayout"), multiSplitterLayout.toVariantMap());
    map.insert(QStringLiteral("uniqueName"), uniqueName);
    map.insert(QStringLiteral("geometry"), Layouting::rectToMap(geometry));
    map.insert(QStringLiteral("screenIndex"), screenIndex);
    map.insert(QStringLiteral("screenSize"), Layouting::sizeToMap(screenSize));
    map.insert(QStringLiteral("isVisible"), isVisible);
    map.insert(QStringLiteral("affinities"), stringListToVariant(affinities));

    return map;
}

void LayoutSaver::MainWindow::fromVariantMap(const QVariantMap &map)
{
    options = KDDockWidgets::MainWindowOptions(map.value(QStringLiteral("options")).toInt());
    multiSplitterLayout.fromVariantMap(map.value(QStringLiteral("multiSplitterLayout")).toMap());
    uniqueName = map.value(QStringLiteral("uniqueName")).toString();
    geometry = Layouting::mapToRect(map.value(QStringLiteral("geometry")).toMap());
    screenIndex = map.value(QStringLiteral("screenIndex")).toInt();
    screenSize = Layouting::mapToSize(map.value(QStringLiteral("screenSize")).toMap());
    isVisible = map.value(QStringLiteral("isVisible")).toBool();
    affinities = variantToStringList(map.value(QStringLiteral("affinities")).toList());

    // Compatibility hack. Old json format had a single "affinityName" instead of an "affinities" list:
    const QString affinityName = map.value(QStringLiteral("affinityName")).toString();
    if (!affinityName.isEmpty() && !affinities.contains(affinityName)) {
        affinities.push_back(affinityName);
    }

}

bool LayoutSaver::MultiSplitterLayout::isValid() const
{
    if (layout.isEmpty())
        return false;

    /*if (!size.isValid()) {
        qWarning() << Q_FUNC_INFO << "Invalid size";
        return false;
    }*/

    return true;
}

void LayoutSaver::MultiSplitterLayout::scaleSizes(const ScalingInfo &)
{
    // scalingInfo.applyFactorsTo(/*by-ref*/size);
    //for (LayoutSaver::Item &item : items) TODO
    //    item.scaleSizes(scalingInfo);
}

QVariantMap LayoutSaver::MultiSplitterLayout::toVariantMap() const
{
    QVariantMap result;
    result.insert(QStringLiteral("layout"), layout);

    QVariantMap framesV;
    for (auto &frame : frames)
        framesV.insert(frame.id, frame.toVariantMap());

    result.insert(QStringLiteral("frames"), framesV);
    return result;
}

void LayoutSaver::MultiSplitterLayout::fromVariantMap(const QVariantMap &map)
{
    layout = map.value(QStringLiteral("layout")).toMap();
    const QVariantMap framesV = map.value(QStringLiteral("frames")).toMap();
    frames.clear();
    for (const QVariant &frameV : framesV) {
        LayoutSaver::Frame frame;
        frame.fromVariantMap(frameV.toMap());
        frames.insert(frame.id, frame);
    }
}

void LayoutSaver::Position::scaleSizes(const ScalingInfo &scalingInfo)
{
    scalingInfo.applyFactorsTo(/*by-ref*/lastFloatingGeometry);
}

QVariantMap LayoutSaver::Position::toVariantMap() const
{
    QVariantMap map;
    map.insert(QStringLiteral("lastFloatingGeometry"), Layouting::rectToMap(lastFloatingGeometry));
    map.insert(QStringLiteral("tabIndex"), tabIndex);
    map.insert(QStringLiteral("wasFloating"), wasFloating);
    map.insert(QStringLiteral("placeholders"), toVariantList<LayoutSaver::Placeholder>(placeholders));

    return map;
}

void LayoutSaver::Position::fromVariantMap(const QVariantMap &map)
{
    lastFloatingGeometry = Layouting::mapToRect(map.value(QStringLiteral("lastFloatingGeometry")).toMap());
    tabIndex = map.value(QStringLiteral("tabIndex")).toInt();
    wasFloating = map.value(QStringLiteral("wasFloating")).toBool();
    placeholders = fromVariantList<LayoutSaver::Placeholder>(map.value(QStringLiteral("placeholders")).toList());
}

QVariantMap LayoutSaver::ScreenInfo::toVariantMap() const
{
    QVariantMap map;
    map.insert(QStringLiteral("index"), index);
    map.insert(QStringLiteral("geometry"), Layouting::rectToMap(geometry));
    map.insert(QStringLiteral("name"), name);
    map.insert(QStringLiteral("devicePixelRatio"), devicePixelRatio);

    return map;
}

void LayoutSaver::ScreenInfo::fromVariantMap(const QVariantMap &map)
{
    index = map.value(QStringLiteral("index")).toInt();
    geometry = Layouting::mapToRect(map.value(QStringLiteral("geometry")).toMap());
    name = map.value(QStringLiteral("name")).toString();
    devicePixelRatio = map.value(QStringLiteral("devicePixelRatio")).toDouble();
}

QVariantMap LayoutSaver::Placeholder::toVariantMap() const
{
    QVariantMap map;
    map.insert(QStringLiteral("isFloatingWindow"), isFloatingWindow);
    map.insert(QStringLiteral("itemIndex"), itemIndex);

    if (isFloatingWindow)
        map.insert(QStringLiteral("indexOfFloatingWindow"), indexOfFloatingWindow);
    else
        map.insert(QStringLiteral("mainWindowUniqueName"), mainWindowUniqueName);

    return map;
}

void LayoutSaver::Placeholder::fromVariantMap(const QVariantMap &map)
{
    isFloatingWindow = map.value(QStringLiteral("isFloatingWindow")).toBool();
    indexOfFloatingWindow = map.value(QStringLiteral("indexOfFloatingWindow"), -1).toInt();
    itemIndex = map.value(QStringLiteral("itemIndex")).toInt();
    mainWindowUniqueName = map.value(QStringLiteral("mainWindowUniqueName")).toString();
}

LayoutSaver::ScalingInfo::ScalingInfo(const QString &mainWindowId, QRect savedMainWindowGeo)
{
    auto mainWindow = DockRegistry::self()->mainWindowByName(mainWindowId);
    if (!mainWindow) {
        qWarning() << Q_FUNC_INFO << "Failed to find main window with name" << mainWindowName;
        return;
    }

    if (!savedMainWindowGeo.isValid() || savedMainWindowGeo.isNull()) {
        qWarning() << Q_FUNC_INFO << "Invalid saved main window geometry" << savedMainWindowGeo;
        return;
    }

    if (!mainWindow->geometry().isValid() || mainWindow->geometry().isNull()) {
        qWarning() << Q_FUNC_INFO << "Invalid main window geometry" << mainWindow->geometry();
        return;
    }

    this->mainWindowName = mainWindowId;
    this->savedMainWindowGeometry = savedMainWindowGeo;
    realMainWindowGeometry = mainWindow->window()->geometry(); // window() as our main window might be embedded
    widthFactor = double(realMainWindowGeometry.width()) / savedMainWindowGeo.width();
    heightFactor = double(realMainWindowGeometry.height()) / savedMainWindowGeo.height();
}

void LayoutSaver::ScalingInfo::translatePos(QPoint &pt) const
{
    const int deltaX = pt.x() - savedMainWindowGeometry.x();
    const int deltaY = pt.y() - savedMainWindowGeometry.y();

    const double newDeltaX = deltaX * widthFactor;
    const double newDeltaY = deltaY * heightFactor;

    pt.setX(qCeil(savedMainWindowGeometry.x() + newDeltaX));
    pt.setY(qCeil(savedMainWindowGeometry.y() + newDeltaY));
}

void LayoutSaver::ScalingInfo::applyFactorsTo(QPoint &pt) const
{
    pt.setX(qCeil(pt.x() * widthFactor));
    pt.setY(qCeil(pt.y() * heightFactor));
}

void LayoutSaver::ScalingInfo::applyFactorsTo(QSize &sz) const
{
    sz.setWidth(int(widthFactor * sz.width()));
    sz.setHeight(int(heightFactor * sz.height()));
}

void LayoutSaver::ScalingInfo::applyFactorsTo(QRect &rect) const
{
    if (rect.isEmpty())
        return;

    QPoint pos = rect.topLeft();
    QSize size = rect.size();

    applyFactorsTo(/*by-ref*/size);
    applyFactorsTo(/*by-ref*/pos);

    rect.moveTopLeft(pos);
    rect.setSize(size);
}
