// 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:
    QString m_display;
    QColor m_decoration;
    QString m_toolTip;
};

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 e) 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)
    {
        if (!m_children)
            m_children.emplace(Tree{});
        TreeRow &res = m_children->emplace_back(args...);
        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:
    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);
    }
};

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>())); };
}


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

public:
    Object(int x, QObject *parent = nullptr)
        : m_display(QString::number(x))
    {}
    QString display() const { return m_display; }
    void setDisplay(const QString &d) { m_display = d; displayChanged(); }

Q_SIGNALS:
    void displayChanged();

private:
    QString m_display;
};

template <>
struct QRangeModel::RowOptions<Object>
{
    static constexpr auto rowCategory = QRangeModel::RowCategory::MultiRoleItem;
};

class ModelFactory : public QObject
{
    Q_OBJECT

    std::vector<int> numbers = {1, 2, 3, 4, 5};
    QList<QString> strings = {u"one"_s, u"two"_s, u"three"_s};
    std::array<int, 1000000> largeArray = {};
    std::array<Object *, 10000> objects = {};

    void updateAllObjects()
    {
        for (auto *object : objects)
            object->setDisplay(QTime::currentTime().toString());
    }

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

    QRangeModel *makeLargeArray()
    {
        return new QRangeModel(&largeArray);
    }


    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 *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 *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>, 3> data = {
            std::make_unique<Object>(1),
            std::make_unique<Object>(2),
            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()
    {
        static TreeRow root[] = {{"Germany", "Berlin"},
                                 {"France", "Paris"},
                                 {"Austria", "Vienna"}
                                };

        static 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");

        return new QRangeModel(std::ref(europe));
    }

    QRangeModel *makeAutoConnectedObjects()
    {
        QRangeModel *model = new QRangeModel(std::ref(objects));
        QTimer *updater = new QTimer(model);
        connect(updater, &QTimer::timeout, this, &ModelFactory::updateAllObjects);

        for (int i = 0; i < objects.size(); ++i)
            objects[i] = new Object(i, model);
        updater->start(1000);
        model->setAutoConnectPolicy(QRangeModel::AutoConnectPolicy::OnRead);
        return model;
    }

    QRangeModel *makeAutoConnectedConstObjects()
    {
        QRangeModel *model = new QRangeModel(&std::as_const(objects));
        QTimer *updater = new QTimer(model);
        connect(updater, &QTimer::timeout, this, &ModelFactory::updateAllObjects);

        for (int i = 0; i < objects.size(); ++i)
            objects[i] = new Object(i, model);
        updater->start(1000);
        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;

        treeview = new QTreeView;
        treeview->setUniformRowHeights(true);
        splitter->addWidget(treeview);

#ifdef QUICK_UI
        quickWidget = new QQuickWidget;
        quickWidget->loadFromModule("Main", "Main");
        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 *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();

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

        connectionOptions = new QActionGroup(this);
        connectionOptions->setExclusive(true);
        connectionOptions->setExclusionPolicy(QActionGroup::ExclusionPolicy::ExclusiveOptional);
        connectionOptions->addAction(fullAutoConnectAction);
        connectionOptions->addAction(onReadAutoConnectAction);

        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);
    }

private:
    void modelChanged(int index)
    {
        QAbstractItemModel *oldModel = model;

        const QMetaObject &mo = ModelFactory::staticMetaObject;
        const QMetaMethod method = mo.method(index + mo.methodOffset());
        if (method.invoke(&factory, qReturnArg(model))) {
            treeview->setModel(model);
#ifdef QUICK_UI
            if (!quickWidget->rootObject())
                statusBar()->showMessage(tr("Failed to load QML"));
            else
                quickWidget->rootObject()->setProperty("model", QVariant::fromValue(model));

            QQmlContext *rootContext = quickWidget->rootContext();
            QQmlContext *UIContext = quickWidget->engine()->contextForObject(quickWidget->rootObject());
            qDebug() << "context objects" << rootContext->contextObject() << UIContext->contextObject();
            qDebug() << "context URLs" << rootContext->baseUrl() << UIContext->baseUrl();

            qDebug() << "name" << quickWidget->rootContext()->nameForObject(quickWidget->rootObject());
            qDebug() << "root object" << quickWidget->rootContext()->objectForName("root");
            qDebug() << "list object" << quickWidget->rootContext()->objectForName("root");

            qDebug() << "root object in context" << quickWidget->engine()->contextForObject(quickWidget->rootObject())->objectForName("root");
            qDebug() << "list object in context" << quickWidget->engine()->contextForObject(quickWidget->rootObject())->objectForName("list");
#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;
    QRangeModel *model;
    QTreeView *treeview;
#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"
