// Copyright (C) 2025 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only

#include <QtCore>
#include <QtWidgets>

#if __has_include(<QtQml>)
#define QUICK_UI
#include <QtQml/QQmlApplicationEngine>
#include <QtQml/QQmlContext>
#include <QQuickWidget>
#include <QQuickItem>
#elif defined(Q_CC_MSVC)
#pragma message "Building without Quick UI"
#else
#warning "Building without Quick UI"
#endif

#include <list>
#include <ranges>
#include <vector>

using namespace Qt::StringLiterals;

class Gadget
{
    Q_GADGET
    Q_PROPERTY(QString display READ display WRITE setDisplay)
    Q_PROPERTY(QColor decoration READ decoration WRITE setDecoration)
    Q_PROPERTY(QString toolTip READ toolTip WRITE setToolTip)
    Q_PROPERTY(QString user READ display WRITE setDisplay)
public:
    Gadget() = default;

    Gadget(const QString &display, QColor decoration, const QString &toolTip)
        : m_display(display), m_decoration(decoration), m_toolTip(toolTip)
    {
    }

    QString display() const { return m_display; }
    void setDisplay(const QString &display) { m_display = display; }
    QColor decoration() const { return m_decoration; }
    void setDecoration(QColor decoration) { m_decoration = decoration; }
    QString toolTip() const { return m_toolTip.isEmpty() ? m_display : m_toolTip; }
    void setToolTip(const QString &toolTip) { m_toolTip = toolTip; }

private:
    friend QDebug operator<<(QDebug dbg, const Gadget &gadget)
    {
        dbg << "Gadget(" << gadget.m_display << gadget.m_decoration << gadget.m_toolTip << ")";
        return dbg;
    }
    QString m_display;
    QColor m_decoration;
    QString m_toolTip;
};

template <>
struct QRangeModel::RowOptions<Gadget>
{
    static Qt::ItemFlags flags(const Gadget &)
    {
        return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemNeverHasChildren
             | Qt::ItemIsEditable
             | Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled;
    }

    static QStringList mimeTypes()
    {
        return QStringList{u"text/plain"_s};
    }

    template <typename Range>
    static QMimeData *mimeData(const Range &gadgets)
    {
        QByteArray data;
        QTextStream stream(&data, QIODevice::WriteOnly);
        for (const auto &row : gadgets) {
            if (!row.isValid()) {
                qCritical("Skipping invalid row");
                continue;
            }
            const auto &[gadget, index] = row;
            stream << Qt::flush;
            if (index.column() < 0) {
                stream << gadget.display() << ',' << gadget.decoration().name() << ','
                       << gadget.toolTip() << Qt::endl;
            } else {
                switch (index.column()) {
                case 0:
                    stream << gadget.display() << ",,";
                    break;
                case 1:
                    stream << ',' << gadget.decoration().name() << u',';
                    break;
                case 2:
                    stream << ",," << gadget.toolTip();
                    break;
                }
                stream << Qt::endl;
            }
        }
        if (data.isEmpty())
            return nullptr;
        QMimeData *mimeData = new QMimeData;
        mimeData->setData(mimeTypes().first(), data);
        return mimeData;
    }

    static QRangeModel::DropOperation dropMimeData(const QMimeData *mimeData, auto &&inserter)
    {
        if (!mimeData->hasFormat(mimeTypes().first()))
            return QRangeModel::DropOperation::DontDrop;
        QByteArray data = mimeData->data(mimeTypes().first());
        if (data.isEmpty())
            return QRangeModel::DropOperation::DontDrop;
        QTextStream stream(&data, QIODevice::ReadOnly);
        int newRow = 0;
        while (!stream.atEnd()) {
            const auto line = stream.readLine().split(",");
            if (line.isEmpty())
                continue;
            inserter = {Gadget(line.size() > 0 ? line[0] : QString(),
                              QColor::fromString(line.size() > 1 ? line[1] : QString()),
                              line.size() > 2 ? line[2] : QString()),
                        newRow};
            ++newRow; // += 2 would leave a gap
        }
        return QRangeModel::DropOperation::Automatic;
    }

    // simplified version, only called when the default implementation
    // accepts the drop, so action and mime type check already done
    static bool canDropMimeData(const QMimeData *)
    {
        return true;
    }

    // a full implementation to only support drops in between items
    // static bool canDropMimeData(const QMimeData *data, Qt::DropAction, int row, int column, const QModelIndex &)
    // {
    //     if (!data->hasFormat(mimeTypes().first()))
    //         return false;
    //     return row > -1 && column > -1;
    // }

    static QVariant headerData(int section, int role)
    {
        if (role == Qt::DisplayRole) {
            switch (section) {
            case 0: return QCoreApplication::translate("Gadget", "Display");
            case 1: return QCoreApplication::translate("Gadget", "Decoration");
            case 2: return QCoreApplication::translate("Gadget", "ToolTip");
            default:
                break;
            }
        }
        return {};
    }
};

template <>
struct QRangeModel::ItemAccess<Gadget>
{
    using RowOptions = typename QRangeModel::RowOptions<Gadget>;
    static QMimeData *mimeData(auto &&gadgets)
    {
        if (gadgets.isEmpty())
            return nullptr;

        const QModelIndex topLeft = gadgets.first().index();
        const QModelIndex bottomRight = gadgets.last().index();
        QRect span = {QPoint(topLeft.column(), topLeft.row()),
                      QPoint(bottomRight.column(), bottomRight.row())};
        span.moveTo(0, 0);

        QByteArray data;
        QTextStream stream(&data, QIODevice::WriteOnly);

        QModelIndex lastIndex;
        for (const auto &[gadget, index] : gadgets) {
            stream << Qt::flush;
            if (index.row() != lastIndex.row() && stream.pos()) {
                stream.seek(stream.pos() - 1);
                stream << Qt::endl;
            }
            stream << gadget.display() << ','
                   << gadget.decoration().name()
                   << ',' << gadget.toolTip()
                   << ';';
            lastIndex = index;
        }
        stream << Qt::endl;
        if (data.isEmpty())
            return nullptr;
        QMimeData *mimeData = new QMimeData;
        mimeData->setData(RowOptions::mimeTypes().first(), data);
        return mimeData;
    }

    static bool dropMimeData(const QMimeData *mimeData, auto inserter)
    {
        if (!mimeData->hasFormat(RowOptions::mimeTypes().first()))
            return false;
        QByteArray data = mimeData->data(RowOptions::mimeTypes().first());
        if (data.isEmpty())
            return false;

        QTextStream stream(&data, QIODevice::ReadOnly);
        while (!stream.atEnd()) {
            const QString row = stream.readLine();
            if (row.isEmpty())
                continue;
            const QStringList cells = row.split(u';', Qt::SkipEmptyParts);
            for (const auto &cell : cells) {
                const QStringList values = cell.split(u',');
                inserter = Gadget(
                    values.size() > 0 ? values[0] : QString(),
                    QColor::fromString(values.size() > 1 ? values[1] : QString()),
                    values.size() > 2 ? values[2] : QString()
                );
            }
        }
        return true;
    }
};

static_assert(QRangeModelDetails::hasHeaderData<Gadget>);
static_assert(QRangeModelDetails::hasRowFlags<Gadget>);
static_assert(QRangeModelDetails::hasMimeDataRowSpan<Gadget>);
static_assert(QRangeModelDetails::hasDropMimeData<Gadget>);
static_assert(QRangeModelDetails::item_access<Gadget>::hasMimeData);
static_assert(QRangeModelDetails::item_access<Gadget>::hasDropMimeData);

struct QMetaEnumerator
{
    struct iterator
    {
        using difference_type = int;
        using size_type = int;
        using pointer = void;
        using iterator_category = std::input_iterator_tag;
        using value_type = std::tuple<int, QByteArray, int>;
        using reference = value_type;
        using const_reference = const value_type;

        friend constexpr iterator &operator++(iterator &that)
        { ++that.m_index; return that; }
        friend constexpr iterator operator++(iterator &that, int)
        { auto copy = that; ++that.m_index; return copy; }
        friend constexpr iterator &operator+=(iterator &that, int n)
        { that.m_index += n; return that; }

        friend constexpr bool comparesEqual(const iterator &lhs, const iterator &rhs) noexcept
        {
            return lhs.m_index == rhs.m_index && lhs.m_enum == rhs.m_enum;
        }

        friend constexpr Qt::strong_ordering compareThreeWay(const iterator &lhs,
                                                             const iterator &rhs) noexcept
        {
            return Qt::compareThreeWay(lhs.m_index, rhs.m_index);
        }
        Q_DECLARE_STRONGLY_ORDERED(iterator)

        const_reference operator*() const
        { return {m_index, m_enum->key(m_index), m_enum->value(m_index)}; }

        const QMetaEnum *m_enum = nullptr;
        int m_index = 0;
    };

    static_assert(std::input_iterator<iterator>);

    using size_type = iterator::size_type;
    using value_type = iterator::value_type;
    using const_iterator = iterator;

    template <typename Enum>
    explicit QMetaEnumerator(Enum) noexcept
        : m_enum(QMetaEnum::fromType<Enum>())
    {}

    const_iterator begin() const { return iterator{&m_enum, 0}; }
    const_iterator end() const { return iterator{&m_enum, size()}; }
    size_type size() const { return m_enum.keyCount(); }

private:
    const QMetaEnum m_enum;
};

struct TreeRow;
using Tree = std::vector<TreeRow>;

struct TreeRow
{
public:
    TreeRow() = default;

    TreeRow(const QString &name, const QString &title)
        : m_name(name), m_title(title)
    {}

    ~TreeRow() = default;
    TreeRow(TreeRow &&other) = default;
    TreeRow &operator=(TreeRow &&other) = default;

    template <typename ...Args>
    TreeRow &addChild(Args&& ...args)
    {
        TreeRow *oldData = nullptr;
        if (!m_children) {
            m_children.emplace(Tree{});
            m_children->reserve(10);
        } else {
            oldData = m_children->data();
        }
        TreeRow &res = m_children->emplace_back(args...);
        // this is why trees of values in an iterator-invalidating container
        // are a bad idea.
        if (oldData && oldData != m_children->data())
            qWarning() << "Reallocated!";
        res.m_parent = this;
        return res;
    }

    // tree traversal protocol implementation
    TreeRow *parentRow() const { return m_parent; }
    void setParentRow(TreeRow *parent) { m_parent = parent; }
    const std::optional<Tree> &childRows() const { return m_children; }
    std::optional<Tree> &childRows() { return m_children; }

private:
    friend struct QRangeModel::RowOptions<TreeRow>;
    QString m_name;
    QString m_title;

    TreeRow *m_parent = nullptr;
    std::optional<Tree> m_children = std::nullopt;

    template<size_t I, typename Row,
        std::enable_if_t<std::is_same_v<q20::remove_cvref_t<Row>, TreeRow>, bool> = true>
    friend inline decltype(auto) get(Row &&row)
    {
        if constexpr (I == 0)
            return q23::forward_like<Row>(row.m_name);
        else if constexpr (I == 1)
            return q23::forward_like<Row>(row.m_title);
    }

    friend QDebug operator<<(QDebug dbg, const TreeRow &row)
    {
        dbg << "TreeRow(" << row.m_name << row.m_title << ")";
        return dbg;
    }
};

namespace std {
    template <> struct tuple_size<TreeRow> : std::integral_constant<size_t, 2> {};
    template <size_t I> struct tuple_element<I, TreeRow>
    { using type = decltype(get<I>(std::declval<TreeRow>())); };
}

template <>
struct QRangeModel::RowOptions<TreeRow>
{
    static QStringList mimeTypes() { return {u"application/json"_s}; }

    using Path = QVarLengthArray<int, 32>;
    static Path makePath(const QModelIndex &index) {
        // we call with col0 indexes and a parent can never be at another column
        Q_ASSERT(index.column() == 0);
        Path path{index.row()};
        QModelIndex parent = index.parent();
        while (parent.isValid()) {
            path.append(parent.row());
            parent = parent.parent();
        }
        std::reverse(path.begin(), path.end());
        return path;
    };

    static QJsonArray makeArray(const auto &rows)
    {
        QList<std::pair<QVarLengthArray<int, 32>, QJsonObject>> builtObjects;

        std::for_each(rows.rbegin(), rows.rend(), [&](const auto &entry) {
            const auto &[row, index] = entry;
            QJsonObject rowObject;
            if (index.column() == -1) { // full row selected
                rowObject.insert("name", get<0>(row));
                rowObject.insert("title", get<1>(row));
            } else {
                switch (index.column()) {
                case 0:
                    rowObject.insert("name", get<0>(row));
                    break;
                case 1:
                    rowObject.insert("title", get<1>(row));
                    break;
                }
            }
            Path rowPath = makePath(index.siblingAtColumn(0));

            QJsonArray children;
            for (auto entry = builtObjects.cbegin(); entry != builtObjects.cend();) {
                const auto &[builtPath, builtObject] = *entry;
                if (rowPath.size() < builtPath.size()) {
                    int i = 0;
                    while (i < std::min(builtPath.size(), rowPath.size()) && builtPath[i] == rowPath[i])
                        ++i;
                    if (i) {
                        children.append(builtObject);
                        entry = builtObjects.erase(entry);
                        continue;
                    }
                }
                ++entry;
            }
            if (!children.isEmpty())
                rowObject.insert("children", children);
            builtObjects.prepend({rowPath, rowObject});
        });

        QJsonArray result;
        for (const auto &[_, object] : builtObjects)
            result.append(object);
        return result;
    }

    static QMimeData *mimeData(const auto &rows)
    {
        if (rows.empty())
            return nullptr;

        QJsonDocument document(makeArray(rows));
        QByteArray data = document.toJson();

        QMimeData *mimeData = new QMimeData;
        mimeData->setData(mimeTypes().first(), data);
        return mimeData;
    }

    static Tree generateTree(const QJsonArray &array)
    {
        Tree tree;
        for (const auto &value : array) {
            if (!value.isObject())
                continue;
            QJsonObject object = value.toObject();
            TreeRow treeRow{
                object["name"].toString(),
                object["title"].toString()
            };
            if (QJsonValue maybeChildren = object["children"]; maybeChildren.isArray())
                treeRow.m_children = generateTree(maybeChildren.toArray());
            tree.emplace_back(std::move(treeRow));
        }
        return tree;
    }

    static bool dropMimeData(const QMimeData *data, auto inserter)
    {
        QByteArray json = data->data(mimeTypes().first());
        if (json.isEmpty())
            return false;
        QJsonParseError error;
        QJsonDocument document = QJsonDocument::fromJson(json, &error);
        if (error.error) {
            qWarning("Invalid JSON: %s at offset %d", qPrintable(error.errorString()), error.offset);
            return false;
        }
        QJsonArray topObjects = document.array();
        Tree newTree = generateTree(topObjects);
        for (auto &newRow : newTree)
            inserter = std::move(newRow);
        return true;
    }
};

class Object : public QObject
{
    Q_OBJECT
    Q_PROPERTY(QString display READ display WRITE setDisplay NOTIFY displayChanged)
    Q_PROPERTY(QColor decoration READ decoration WRITE setDecoration NOTIFY decorationChanged)

public:
    Object(int x, QObject *parent = nullptr)
        : QObject(parent)
        , m_display(QString::number(x))
        , m_decoration(QColor::fromHsv(x % 255, 255, 255))
    {}
    QString display() const { return m_display; }
    void setDisplay(const QString &display)
    {
        if (m_display == display)
            return;
        m_display = display;
        emit displayChanged();
    }

    QColor decoration() const { return m_decoration; }
    void setDecoration(QColor color)
    {
        if (m_decoration == color)
            return;
        m_decoration = color;
        emit decorationChanged();
    }

Q_SIGNALS:
    void displayChanged();
    void decorationChanged();

private:
    QString m_display;
    QColor m_decoration;
};

class ModelFactory : public QObject
{
    Q_OBJECT

    std::vector<int> numbers = {1, 10, 3, 8, 5, 6, 7, 4, 9, 2, 11};
    QList<QString> strings = {u"one"_s, u"two"_s, u"three"_s};
    std::array<int, 1000000> largeArray = {};
    QTimer updater;

public:
    void reset()
    {
        updater.stop();
        updater.disconnect();
    }

public slots:
    QRangeModel *makeNumbers()
    {
        return new QRangeModel(&numbers);
    }

    QRangeModel *makeLargeArray()
    {
        for (int i = 0; i < int(largeArray.size()); ++i)
            largeArray[i] = i;
        return new QRangeModel(&largeArray);
    }

    QRangeModel *makeVectorWithAdapter()
    {
        QRangeModelAdapter adapter(std::ref(numbers));
        auto *model = adapter.model();
        qDebug() << "Data from index" << adapter.index(0).data();
        qDebug() << "Data from adapter" << adapter.data(0);
        qDebug() << "Data from operator[]" << adapter[0];

        connect(&updater, &QTimer::timeout, model, [adapter] mutable {
            adapter[0] = adapter[0] + 1;
        });

        updater.start(1000);
        return model;
    }

    QRangeModel *makeStrings()
    {
        return new QRangeModel(std::ref(strings));
    }

    QRangeModel *makeJson()
    {
        QJsonDocument json = QJsonDocument::fromJson(R"(
            [ "one", "two", 12345 ]
        )");
        Q_ASSERT(json.isArray());
        return new QRangeModel(json.array());
    }

    QRangeModel *makeListOfTuples()
    {
        std::list<std::tuple<int, QString>> data = {
            { 1, "eins"},
            { 2, "zwei"},
            { 3, "drei"},
            { 4, "vier"},
            { 5, "fünf"},
        };

        return new QRangeModel(data);
    }

    QRangeModel *makeListOfArrays()
    {
        QList<std::array<double, 2000>> data = {
            {0.0},
            {1.1},
            {2.2},
        };

        return new QRangeModel(std::move(data));
    }

    QRangeModel *makeCustomFromEnum()
    {
        return new QRangeModel(QMetaEnumerator(Qt::ItemDataRole{}));
    }

    QRangeModel *makeBoundedIota()
    {
        return new QRangeModel(std::views::iota(1, 10000));
    }

    QRangeModel *makeUnboundedIota()
    {
        auto view = std::views::iota(1);
        return new QRangeModel(view);
    }

    QRangeModel *makeZipView()
    {
        static auto x = QList<int>{1, 2, 3, 4, 5};
        static auto y = std::list<QString>{"α", "β", "γ", "δ", "ε"};
        static auto z = std::array<QChar, 6>{u'A', u'B', u'C', u'D', u'E', u'F'};

        return new QRangeModel(std::views::zip(x, y, z));
    }

    QRangeModel *makeGadgetList()
    {
        QList<Gadget> gadgetList = {
            {"1/1", Qt::red, "red"},
            {"1/2", Qt::black, "black"},
            {"2/1", Qt::blue, "blue"},
            {"2/2", Qt::green, "green"},
        };
        return new QRangeModel(gadgetList);
    }

    QRangeModel *makeGadgetPointerList()
    {
        std::vector<std::unique_ptr<Gadget>> gadgetList;
        std::array gadgets = {
            std::make_unique<Gadget>("1/1", Qt::red, "red"),
            std::make_unique<Gadget>("1/2", Qt::black, "black"),
            std::unique_ptr<Gadget>(),
            std::make_unique<Gadget>("3/1", Qt::blue, "blue"),
            std::make_unique<Gadget>("3/2", Qt::green, "green"),
        };
        std::copy(std::move_iterator(gadgets.begin()), std::move_iterator(gadgets.end()),
                  std::back_inserter(gadgetList));
        return new QRangeModel(std::move(gadgetList));
    }

    QRangeModel *makeGadgetTable()
    {
        QList<QList<Gadget>> gadgetTable = {
            {{"1/1", Qt::red, "red"}, {"1/2", Qt::black, "black"}},
            {{"2/1", Qt::blue, "blue"}, {"2/2", Qt::green, "green"}},
        };
        return new QRangeModel(gadgetTable);
    }

   QRangeModel *makeGadgetPointerTable()
    {
        QList<std::vector<std::shared_ptr<Gadget>>> gadgetTable = {
            {
                std::make_shared<Gadget>("1/1", Qt::red, "red"),
                std::make_shared<Gadget>("1/2", Qt::black, "black")
            },
            {
                std::shared_ptr<Gadget>(), std::make_shared<Gadget>("2/1", Qt::blue, "blue"),
            },
            {
                std::make_shared<Gadget>("3/1", Qt::blue, "blue"),
                std::make_shared<Gadget>("3/2", Qt::green, "green")
            },
        };
        return new QRangeModel(gadgetTable);
    }

    QRangeModel *makeQtMap()
    {
        return new QRangeModel(QMap<QString, QString>{
            {"one", "eins"},
            {"two", "zwei"},
            {"three", "drei"},
            {"four", "vier"},
        });
    }

    QRangeModel *makeStdMap()
    {
        return new QRangeModel(std::map<int, double>{
            {1, 0.1},
            {2, 0.2},
            {3, 0.3},
            {4, 0.4},
        });
    }

    QRangeModel *makeFilterView()
    {
        const QDate today = QDate::currentDate();
        auto view = std::views::iota(today.addYears(-100), today.addYears(100))
                  | std::views::filter([](QDate date){
                        return date.dayOfWeek() < 6;
                    });

        return new QRangeModel(view);
    }

    QRangeModel *makeMultiRoleMap()
    {
        using ColorEntry = QMap<Qt::ItemDataRole, QVariant>;

        const QStringList colorNames = QColor::colorNames();
        QList<ColorEntry> colors;
        colors.reserve(colorNames.size());
        for (const QString &name : QColor::colorNames()) {
            const QColor color = QColor::fromString(name);
            colors << ColorEntry{{Qt::DisplayRole, name},
                                {Qt::DecorationRole, color},
                                {Qt::ToolTipRole, color.name()}};
        }
        return new QRangeModel(colors);
    }

    QRangeModel *makeUniqueObjects()
    {
        std::array<std::unique_ptr<Object>, 4> data = {
            std::make_unique<Object>(1),
            std::make_unique<Object>(2),
            std::unique_ptr<Object>(),
            std::make_unique<Object>(3),
        };
        return new QRangeModel(std::move(data));
    }

    QRangeModel *makeUniquePtrArray()
    {
        // not possible, values need to be copyable
        // std::array<std::unique_ptr<QString>, 3> data = {
        //     std::make_unique<QString>("A"),
        //     std::make_unique<QString>("B"),
        //     std::make_unique<QString>("C"),
        // };
        // return new QRangeModel(std::move(data));
        return new QRangeModel(QList{"Nothing to see here"});
    }

    QRangeModel *makeUniqueRows()
    {
        std::array<std::unique_ptr<std::vector<QString>>, 3> data = {
            std::make_unique<std::vector<QString>>(std::vector<QString>{u"A"_s, u"B"_s, u"C"_s}),
            std::make_unique<std::vector<QString>>(std::vector<QString>{u"D"_s, u"E"_s, u"F"_s}),
            std::make_unique<std::vector<QString>>(std::vector<QString>{u"G"_s, u"H"_s, u"I"_s}),
        };
        return new QRangeModel(std::move(data));
    }

    QRangeModel *makeTree()
    {
        TreeRow root[] = {{"Germany", "Berlin"},
                          {"France", "Paris"},
                          {"Austria", "Vienna"}
                         };

        Tree europe{std::make_move_iterator(std::begin(root)),
                    std::make_move_iterator(std::end(root))};
        TreeRow &bavaria = europe[0].addChild("Bavaria", "Munich");
        bavaria.addChild("Upper Bavaria", "München");
        bavaria.addChild("Lower Bavaria", "Landshut");
        bavaria.addChild("Upper Palatinate", "Regensburg");
        bavaria.addChild("Swabia", "Augsburg");
        bavaria.addChild("Franconia", "Nürnberg");
        bavaria.addChild("Upper Franconia", "Bayreuth");
        bavaria.addChild("Middle Franconia", "Ansbach");
        bavaria.addChild("Lower Franconia", "Würzburg");

        TreeRow &hessia = europe[0].addChild("Hessia", "Wiesbaden");
        hessia.addChild("Upper Hesse", "Giessen");
        hessia.addChild("Lower Hesse", "Darmstadt");
        hessia.addChild("North Hesse", "Kassel");

        europe[1].addChild("Île-de-France", "Paris");
        europe[1].addChild("Provence-Alpes-Côte d'Azur", "Marseille");
        europe[1].addChild("Auvergne-Rhône-Alpes", "Lyon");
        europe[1].addChild("Nouvelle-Aquitaine", "Bordeaux");

        europe[2].addChild("Vienna", "Vienna");
        europe[2].addChild("Lower Austria", "St. Pölten");
        europe[2].addChild("Upper Austria", "Linz");
        europe[2].addChild("Styria", "Graz");
        europe[2].addChild("Carinthia", "Klagenfurt");
        europe[2].addChild("Salzburg", "Salzburg");
        europe[2].addChild("Tyrol", "Innsbruck");
        europe[2].addChild("Vorarlberg", "Bregenz");
        europe[2].addChild("Burgenland", "Eisenstadt");

        QRangeModelAdapter adapter(std::move(europe));
        const QList<int> path = {1, 0};
        QRangeModel *model = adapter.model();
        connect(&updater, &QTimer::timeout, model, [adapter] mutable {
            for (auto row : adapter)
                qDebug() << row[0] << row[1];
        });
        updater.start(1000);

        return model;
    }

    QRangeModel *makeAutoConnectedObjects()
    {
        std::array<std::pair<Object *, Object *>, 500> table;
        for (size_t i = 0; i < table.size(); ++i) {
            table[i].first = new Object(i);
            table[i].second = new Object(i);
        }

        QRangeModelAdapter adapter(std::move(table));
        QRangeModel *model = adapter.model();
        connect(&updater, &QTimer::timeout, this, [adapter = std::move(adapter)] {
            for (auto row : adapter) {
                for (auto column : row) {
                    const_cast<Object *>(column)->setDisplay(QTime::currentTime().toString());
                }
            }
        });

        updater.start(1000);
        model->setAutoConnectPolicy(QRangeModel::AutoConnectPolicy::OnRead);
        return model;
    }

    QRangeModel *makeAutoConnectedConstObjects()
    {
        std::array<std::pair<Object *, Object *>, 500> table;
        for (size_t i = 0; i < table.size(); ++i) {
            table[i].first = new Object(i);
            table[i].second = new Object(i);
        }

        QRangeModelAdapter adapter(std::move(std::as_const(table)));
        QRangeModel *model = adapter.model();
        connect(&updater, &QTimer::timeout, this, [adapter = std::move(adapter)] {
            for (auto row : adapter) {
                for (auto column : row) {
                    auto object = const_cast<Object *>(column);
                    object->setDisplay(QTime::currentTime().toString());
                    int hue = column->decoration().hue() + 1;
                    object->setDecoration(QColor::fromHsv(hue % 255, 255, 255));
                }
            }
        });

        updater.start(1000);
        model->setAutoConnectPolicy(QRangeModel::AutoConnectPolicy::Full);
        return model;
    }

    QRangeModel *makeAutoConnectedRows()
    {
        QList<Object *> list;
        for (int i = 0; i < 100; ++i)
            list.append(new Object(i));

        QRangeModelAdapter adapter(std::move(list));
        QRangeModel *model = adapter.model();
        connect(&updater, &QTimer::timeout, this, [adapter = std::move(adapter)] {
            for (auto row : adapter) {
                auto object = const_cast<Object *>(row.get());
                object->setDisplay(QTime::currentTime().toString());
                int hue = row->decoration().hue() + 1;
                object->setDecoration(QColor::fromHsv(hue % 255, 255, 255));
            }
        });

        updater.start(100);
        model->setAutoConnectPolicy(QRangeModel::AutoConnectPolicy::Full);
        return model;
    }
};

struct QMetaMethodEnumerator
{
    struct iterator
    {
        using difference_type = int;
        using size_type = int;
        using pointer = void;
        using iterator_category = std::forward_iterator_tag;
        using value_type = std::tuple<int, QByteArray, QMetaMethod>;
        using reference = value_type;
        using const_reference = const value_type;

        friend constexpr iterator &operator++(iterator &that)
        { ++that.m_index; return that; }
        friend constexpr iterator operator++(iterator &that, int)
        { auto copy = that; ++that.m_index; return copy; }
        friend constexpr iterator &operator+=(iterator &that, int n)
        { that.m_index += n; return that; }

        friend constexpr bool comparesEqual(const iterator &lhs, const iterator &rhs) noexcept
        {
            return lhs.m_index == rhs.m_index && lhs.m_metaobject == rhs.m_metaobject;
        }

        friend constexpr Qt::strong_ordering compareThreeWay(const iterator &lhs,
                                                             const iterator &rhs) noexcept
        {
            return Qt::compareThreeWay(lhs.m_index, rhs.m_index);
        }
        Q_DECLARE_STRONGLY_ORDERED(iterator)

        const_reference operator*() const
        { return {m_index,
                  m_metaobject->method(m_index).name().slice(4),
                  m_metaobject->method(m_index)}; }

        const QMetaObject *m_metaobject = nullptr;
        int m_index = 0;
    };

    static_assert(std::input_iterator<iterator>);

    using size_type = iterator::size_type;
    using value_type = iterator::value_type;
    using const_iterator = iterator;

    const_iterator begin() const { return iterator{&m_metaobject, m_metaobject.methodOffset()}; }
    const_iterator end() const { return iterator{&m_metaobject, size()}; }
    size_type size() const { return m_metaobject.methodCount() - m_metaobject.methodOffset(); }

    explicit QMetaMethodEnumerator(const QMetaObject &mo) noexcept
        : m_metaobject(mo)
    {}

    template <typename Class>
    static QMetaMethodEnumerator fromType() noexcept
    {
        return QMetaMethodEnumerator{Class::staticMetaObject};
    }

private:
    const QMetaObject &m_metaobject;
};

class MainWindow : public QMainWindow
{
    Q_OBJECT
public:
    MainWindow(QWidget *parent = nullptr)
        : QMainWindow(parent), model(nullptr)
    {
        QSplitter *splitter = new QSplitter;
        QSplitter *viewSplitter = new QSplitter(Qt::Vertical);

        treeview = new QTreeView;
        treeview->setSelectionMode(QAbstractItemView::ExtendedSelection);
        treeview->setUniformRowHeights(true);
        treeview->setDragEnabled(true);
        treeview->setDropIndicatorShown(true);
        treeview->setAcceptDrops(true);
        treeview->viewport()->setAcceptDrops(true);
        treeview->setDragDropMode(QAbstractItemView::DragDrop);
        viewSplitter->addWidget(treeview);

        tableview = new QTableView;
        tableview->setDragEnabled(true);
        tableview->setDropIndicatorShown(true);
        tableview->setAcceptDrops(true);
        tableview->viewport()->setAcceptDrops(true);
        tableview->setDragDropOverwriteMode(true);
        tableview->setDragDropMode(QAbstractItemView::DragDrop);
        viewSplitter->addWidget(tableview);

        splitter->addWidget(viewSplitter);

#ifdef QUICK_UI
        quickWidget = new QQuickWidget;
        connect(quickWidget, &QQuickWidget::statusChanged, this, [](QQuickWidget::Status status){
            qDebug() << "Quick UI status" << status;
        });
        quickWidget->loadFromModule("Main", "Main");
        quickWidget->setResizeMode(QQuickWidget::SizeRootObjectToView);
        splitter->addWidget(quickWidget);
#endif

        setCentralWidget(splitter);

        QComboBox *modelPicker = new QComboBox;
        connect(modelPicker, &QComboBox::currentIndexChanged, this, &MainWindow::modelChanged);

        QToolBar *toolBar = addToolBar(tr("Model Operations"));
        toolBar->addWidget(modelPicker);

        toolBar->addSeparator();

        QAction *sortAction = toolBar->addAction(tr("Sort"));
        sortAction->setCheckable(true);
        connect(sortAction, &QAction::toggled, treeview, &QTreeView::setSortingEnabled);
        toolBar->addSeparator();

        QAction *addAction = toolBar->addAction(tr("Add"), this, &MainWindow::onAdd);
        addAction->setIcon(QIcon::fromTheme(QIcon::ThemeIcon::ListAdd));
        QAction *removeAction = toolBar->addAction(tr("Remove"), this, &MainWindow::onRemove);
        removeAction->setIcon(QIcon::fromTheme(QIcon::ThemeIcon::ListRemove));
        QAction *upAction = toolBar->addAction(tr("Move up"), this, &MainWindow::onUp);
        upAction->setIcon(QIcon::fromTheme(QIcon::ThemeIcon::GoUp));
        QAction *downAction = toolBar->addAction(tr("Move down"), this, &MainWindow::onDown);
        downAction->setIcon(QIcon::fromTheme(QIcon::ThemeIcon::GoDown));
        QAction *indentAction = toolBar->addAction(tr("Move in"), this, &MainWindow::onIn);
        indentAction->setIcon(QIcon::fromTheme(QIcon::ThemeIcon::FormatIndentMore));
        QAction *dedentAction = toolBar->addAction(tr("Move out"), this, &MainWindow::onOut);
        dedentAction->setIcon(QIcon::fromTheme(QIcon::ThemeIcon::FormatIndentLess));

        toolBar->addSeparator();

        connectionOptions = new QActionGroup(this);
        QAction *fullAutoConnectAction = connectionOptions->addAction(tr("Full"));
        fullAutoConnectAction->setCheckable(true);
        fullAutoConnectAction->setData(QVariant::fromValue(QRangeModel::AutoConnectPolicy::Full));
        QAction *onReadAutoConnectAction = connectionOptions->addAction(tr("On Read"));
        onReadAutoConnectAction->setCheckable(true);
        onReadAutoConnectAction->setData(QVariant::fromValue(QRangeModel::AutoConnectPolicy::OnRead));

        connectionOptions->setExclusive(true);
        connectionOptions->setExclusionPolicy(QActionGroup::ExclusionPolicy::ExclusiveOptional);

        toolBar->addAction(fullAutoConnectAction);
        toolBar->addAction(onReadAutoConnectAction);

        connect(connectionOptions, &QActionGroup::triggered, this, [this](){
            QAction *checkedAction = connectionOptions->checkedAction();
            const auto policy = checkedAction ? checkedAction->data().value<QRangeModel::AutoConnectPolicy>()
                                              : QRangeModel::AutoConnectPolicy::None;

            qDebug() << "Switching to policy" << policy;
            model->setAutoConnectPolicy(policy);
        });

        // this will implicitly run modelChanged() and update the UI
        modelPicker->setModel(new QRangeModel(QMetaMethodEnumerator::fromType<ModelFactory>(),
                                              modelPicker));
        modelPicker->setModelColumn(1);
    }

    ~MainWindow()
    {
        factory.reset();
    }

private:
    void modelChanged(int index)
    {
        QPointer<QRangeModel> oldModel = model;

        factory.reset();

        QRangeModel *newModel = nullptr;
        const QMetaObject &mo = ModelFactory::staticMetaObject;
        const QMetaMethod method = mo.method(index + mo.methodOffset());
        if (method.invoke(&factory, qReturnArg(newModel))) {
            model = newModel;
            newModel->setParent(this);
            newModel->setObjectName(QString::fromUtf8(method.name()).slice(4));
            treeview->setModel(newModel);
            tableview->setModel(newModel);
            newModel->setSupportedDragActions(Qt::CopyAction | Qt::MoveAction);
            newModel->setSupportedDropActions(Qt::CopyAction | Qt::MoveAction);
#ifdef QUICK_UI
            if (!quickWidget->rootObject())
                statusBar()->showMessage(tr("Failed to load QML"));
            else
                quickWidget->rootObject()->setProperty("model", QVariant::fromValue(newModel));
#endif
            for (auto *action : connectionOptions->actions()) {
                action->setChecked(action->data().value<QRangeModel::AutoConnectPolicy>()
                                == model->autoConnectPolicy());
            }
        }

        delete oldModel;
    }

    void onAdd()
    {
        const auto current = treeview->currentIndex();
        showMessage(tr("Inserting after '%1'").arg(current.data().toString()));
        if (!model->insertRows(current.row() + 1, 1, current.parent())) {
            showMessage(tr("Insertion failed"));
        } else {
            const auto newIndex = model->index(current.row() + 1, 0, current.parent());
            static int counter = 0;
            model->setData(newIndex, u"New Value %1"_s.arg(++counter));
        }
    }

    void onRemove()
    {
        const auto current = treeview->currentIndex();
        showMessage(tr("Removing '%1'").arg(current.data().toString()));
        if (!model->removeRows(current.row(), 1, model->parent(current)))
            showMessage(tr("Removal failed"));
    }

    void onUp()
    {
        const auto current = treeview->currentIndex();
        showMessage(tr("Moving '%1' up").arg(current.data().toString()));
        const auto currentParent = current.parent();
        if (!model->moveRows(currentParent, current.row(), 1, currentParent, current.row() - 1))
            showMessage(tr("Failed to move up"));
    }

    void onDown()
    {
        const auto current = treeview->currentIndex();
        showMessage(tr("Moving '%1' down").arg(current.data().toString()));
        const auto currentParent = current.parent();
        if (!model->moveRows(currentParent, current.row(), 1, currentParent, current.row() + 2))
            showMessage(tr("Failed to move down"));
    }

    void onIn()
    {
        const auto current = treeview->currentIndex();
        showMessage(tr("Moving '%1' in").arg(current.data().toString()));
        const auto currentParent = current.parent();
        const auto newParent = current.sibling(current.row() - 1, 0);
        // move the selected row under it's top-most sibling

        if (!model->moveRows(currentParent, current.row(), 1, newParent, model->rowCount(newParent)))
            showMessage(tr("Indentation failed"));
    }

    void onOut()
    {
        const auto current = treeview->currentIndex();
        showMessage(tr("Moving '%1' out").arg(current.data().toString()));
        const auto currentParent = current.parent();
        const auto grandParent = currentParent.parent();
        // move the selected row under it's grandparent
        if (!model->moveRows(currentParent, current.row(), 1, grandParent, currentParent.row()))
            showMessage(tr("Dedentation failed"));
    }

    void showMessage(const QString &message, int timeout = 2000)
    {
        statusBar()->showMessage(message, timeout);
    }

    ModelFactory factory;
    QPointer<QRangeModel> model; // might be owned by an adapter
    QTreeView *treeview;
    QTableView *tableview;
#ifdef QUICK_UI
    QQuickWidget *quickWidget;
#endif
    QActionGroup *connectionOptions;
};

int main(int argc, char *argv[])
{
    QApplication app(argc, argv);

    auto mainWindow = std::make_unique<MainWindow>();
    mainWindow->show();

    return app.exec();
}

#include "main.moc"
