#include "panes/propertiesview.h"
#include "common.h"
#include "datatypes/base.h"
#include "datatypes/variant.h"
#include "datatypes/primitives.h"
#include "error/data.h"
#include "mainwindow.h"
#include "objects/base/member.h"
#include "undohistory.h"

#include <QColorDialog>
#include <QComboBox>
#include <QLineEdit>
#include <QSpinBox>
#include <QStyledItemDelegate>
#include <QPainter>
#include <QTime>
#include <cmath>
#include <functional>
#include <qapplication.h>
#include <qcombobox.h>
#include <qevent.h>
#include <qnamespace.h>
#include <qtreewidget.h>

QDoubleSpinBox* makeDoubleSpinBox(QWidget* parent = nullptr) {
    QDoubleSpinBox* spinBox = new QDoubleSpinBox(parent);
    spinBox->setMaximum(INFINITY);
    spinBox->setMinimum(-INFINITY);
    spinBox->setDecimals(4);
    return spinBox;
}

class PropertiesItemDelegate : public QStyledItemDelegate {
    PropertiesView* view;
public:
    PropertiesItemDelegate(PropertiesView* parent) : QStyledItemDelegate(parent), view(parent) {}

    void initStyleOption(QStyleOptionViewItem *option, const QModelIndex &index) const override {
        // https://stackoverflow.com/a/76645757/16255372
        // https://stackoverflow.com/a/70078448/16255372

        QStyledItemDelegate::initStyleOption(option, index);

        if (!index.parent().isValid()) {
            option->state &= ~QStyle::State_Selected;

            option->backgroundBrush = view->palette().dark();
        }
    };

    QWidget* createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const override {
        if (index.column() == 0) return nullptr;
    
        if (!index.parent().isValid() || view->currentInstance.expired()) return nullptr;
        std::shared_ptr<Instance> inst = view->currentInstance.lock();

        // If the property is deeper than 1 layer, then it is considered composite
        // Handle specially

        bool isComposite = index.parent().parent().isValid();
        std::string componentName = isComposite ? view->itemFromIndex(index)->data(0, Qt::DisplayRole).toString().toStdString() : "";

        std::string propertyName = !isComposite ? view->itemFromIndex(index)->data(0, Qt::DisplayRole).toString().toStdString()
            : view->itemFromIndex(index.parent())->data(0, Qt::DisplayRole).toString().toStdString();
        PropertyMeta meta = inst->GetPropertyMeta(propertyName).expect();
        Variant currentValue = inst->GetProperty(propertyName).expect();

        if (isComposite) {
            if (meta.type.descriptor == &Vector3::TYPE) {
                Vector3 vector = currentValue.get<Vector3>();
                float value = componentName == "X" ? vector.X() : componentName == "Y" ? vector.Y() : componentName == "Z" ? vector.Z() : 0;

                QDoubleSpinBox* spinBox = makeDoubleSpinBox(parent);
                spinBox->setValue(value);

                return spinBox;
            }

            return nullptr;
        }

        if (meta.type.descriptor == &FLOAT_TYPE) {
            QDoubleSpinBox* spinBox = new QDoubleSpinBox(parent);
            spinBox->setMinimum(-INFINITY);
            spinBox->setMaximum(INFINITY);
            spinBox->setValue(currentValue.get<float>());

            if (meta.flags & PROP_UNIT_FLOAT) {
                spinBox->setMinimum(0);
                spinBox->setMaximum(1);
                spinBox->setSingleStep(0.1);
            }

            return spinBox;
        } else if (meta.type.descriptor == &INT_TYPE) {
            QSpinBox* spinBox = new QSpinBox(parent);
            spinBox->setMinimum(INT_MIN);
            spinBox->setMaximum(INT_MAX);
            spinBox->setValue(currentValue.get<int>());

            return spinBox;
        } else if (meta.type.descriptor == &STRING_TYPE) {
            QLineEdit* lineEdit = new QLineEdit(parent);
            lineEdit->setText(QString::fromStdString(currentValue.get<std::string>()));

            return lineEdit;
        } else if (meta.type.descriptor == &Color3::TYPE) {
            QColorDialog* colorDialog = new QColorDialog(parent->window());

            Color3 color = currentValue.get<Color3>();
            colorDialog->setCurrentColor(QColor::fromRgbF(color.R(), color.G(), color.B()));

            return colorDialog;
        } else if (meta.type.descriptor == &EnumItem::TYPE) {
            QComboBox* comboBox = new QComboBox(parent);

            EnumItem enumItem = currentValue.get<EnumItem>();
            std::vector<EnumItem> siblingItems = meta.type.enum_->GetEnumItems();
            for (size_t i = 0; i < siblingItems.size(); i++) {
                comboBox->addItem(QString::fromStdString(siblingItems[i].Name()));
                if (siblingItems[i].Value() == enumItem.Value())
                    comboBox->setCurrentIndex(i);
            }

            // If a selection is made
            // https://forum.qt.io/post/426902
            connect(comboBox, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), [this, comboBox, index]() {
                setModelData(comboBox, view->model(), index);
                view->closeEditor(comboBox, QAbstractItemDelegate::EndEditHint::NoHint);
            });

            return comboBox;
        } else if (meta.type.descriptor->fromString) {
            QLineEdit* lineEdit = new QLineEdit(parent);
            lineEdit->setText(QString::fromStdString(currentValue.ToString()));

            return lineEdit;
        }

        return nullptr;
    }

    void setEditorData(QWidget *editor, const QModelIndex &index) const override {
        if (index.column() == 0) return;
    
        if (!index.parent().isValid() || view->currentInstance.expired()) return;
        std::shared_ptr<Instance> inst = view->currentInstance.lock();

        bool isComposite = index.parent().parent().isValid();
        std::string componentName = isComposite ? view->itemFromIndex(index)->data(0, Qt::DisplayRole).toString().toStdString() : "";

        std::string propertyName = !index.parent().parent().isValid() ? view->itemFromIndex(index)->data(0, Qt::DisplayRole).toString().toStdString()
            : view->itemFromIndex(index.parent())->data(0, Qt::DisplayRole).toString().toStdString();
        PropertyMeta meta = inst->GetPropertyMeta(propertyName).expect();
        Variant currentValue = inst->GetProperty(propertyName).expect();
        
        if (isComposite) {
            if (meta.type.descriptor == &Vector3::TYPE) {
                Vector3 vector = currentValue.get<Vector3>();
                float value = componentName == "X" ? vector.X() : componentName == "Y" ? vector.Y() : componentName == "Z" ? vector.Z() : 0;

                QDoubleSpinBox* spinBox = dynamic_cast<QDoubleSpinBox*>(editor);
                spinBox->setValue(value);

                return;
            }
            
            return;
        }

        if (meta.type.descriptor == &FLOAT_TYPE) {
            QDoubleSpinBox* spinBox = dynamic_cast<QDoubleSpinBox*>(editor);

            spinBox->setValue(currentValue.get<float>());
        } else if (meta.type.descriptor == &INT_TYPE) {
            QSpinBox* spinBox = dynamic_cast<QSpinBox*>(editor);

            spinBox->setValue(currentValue.get<int>());
        } else if (meta.type.descriptor == &STRING_TYPE) {
            QLineEdit* lineEdit = dynamic_cast<QLineEdit*>(editor);

            lineEdit->setText(QString::fromStdString((std::string)currentValue.get<std::string>()));
        } else if (meta.type.descriptor == &Color3::TYPE) {
            QColorDialog* colorDialog = dynamic_cast<QColorDialog*>(editor);

            Color3 color = currentValue.get<Color3>();
            colorDialog->setCurrentColor(QColor::fromRgbF(color.R(), color.G(), color.B()));
        } else if (meta.type.descriptor == &EnumItem::TYPE) {
            QComboBox* comboBox = dynamic_cast<QComboBox*>(editor);

            EnumItem enumItem = currentValue.get<EnumItem>();
            std::vector<EnumItem> siblingItems = meta.type.enum_->GetEnumItems();
            for (size_t i = 0; i < siblingItems.size(); i++) {
                if (siblingItems[i].Value() == enumItem.Value())
                    comboBox->setCurrentIndex(i);
            }
        } else if (meta.type.descriptor->fromString) {
            QLineEdit* lineEdit = dynamic_cast<QLineEdit*>(editor);

            lineEdit->setText(QString::fromStdString((std::string)currentValue.ToString()));
        }
    }

    void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override {
        if (index.column() == 0) return;
    
        if (!index.parent().isValid() || view->currentInstance.expired()) return;
        std::shared_ptr<Instance> inst = view->currentInstance.lock();

        bool isComposite = index.parent().parent().isValid();
        std::string componentName = isComposite ? view->itemFromIndex(index)->data(0, Qt::DisplayRole).toString().toStdString() : "";

        std::string propertyName = !index.parent().parent().isValid() ? view->itemFromIndex(index)->data(0, Qt::DisplayRole).toString().toStdString()
            : view->itemFromIndex(index.parent())->data(0, Qt::DisplayRole).toString().toStdString();
        PropertyMeta meta = inst->GetPropertyMeta(propertyName).expect();

        Variant oldValue = inst->GetProperty(propertyName).expect();

        if (isComposite) {
            if (meta.type.descriptor == &Vector3::TYPE) {
                QDoubleSpinBox* spinBox = dynamic_cast<QDoubleSpinBox*>(editor);
                float value = spinBox->value();

                Vector3 prev = inst->GetProperty(propertyName).expect().get<Vector3>();
                Vector3 newVector = componentName == "X" ? Vector3(value, prev.Y(), prev.Z())
                : componentName == "Y" ? Vector3(prev.X(), value, prev.Z())
                : componentName == "Z" ? Vector3(prev.X(), prev.Y(), value) : prev;

                inst->SetProperty(propertyName, newVector).expect();
                // SetProperty above already causes the composite to be rebuilt. So we get rid of it here to prevent errors
                // view->rebuildCompositeProperty(view->itemFromIndex(index.parent()), &Vector3::TYPE, newVector);
                return;
            }

            return;
        }

        if (meta.type.descriptor == &FLOAT_TYPE) {
            QDoubleSpinBox* spinBox = dynamic_cast<QDoubleSpinBox*>(editor);

            inst->SetProperty(propertyName, (float)spinBox->value()).expect();
            model->setData(index, spinBox->value());
        } else if (meta.type.descriptor == &INT_TYPE) {
            QSpinBox* spinBox = dynamic_cast<QSpinBox*>(editor);

            inst->SetProperty(propertyName, (int)spinBox->value()).expect();
            model->setData(index, spinBox->value());
        } else if (meta.type.descriptor == &STRING_TYPE) {
            QLineEdit* lineEdit = dynamic_cast<QLineEdit*>(editor);

            inst->SetProperty(propertyName, lineEdit->text().toStdString()).expect();
            model->setData(index, lineEdit->text());
        } else if (meta.type.descriptor == &Color3::TYPE) {
            QColorDialog* colorDialog = dynamic_cast<QColorDialog*>(editor);

            QColor color = colorDialog->currentColor();
            Color3 color3(color.redF(), color.greenF(), color.blueF());
            inst->SetProperty(propertyName, color3).expect();
            model->setData(index, QString::fromStdString(color3.ToString()), Qt::DisplayRole);
            model->setData(index, color, Qt::DecorationRole);
        } else if (meta.type.descriptor == &EnumItem::TYPE) {
            QComboBox* comboBox = dynamic_cast<QComboBox*>(editor);

            std::vector<EnumItem> siblingItems = meta.type.enum_->GetEnumItems();
            EnumItem newItem = siblingItems[comboBox->currentIndex()];
            inst->SetProperty(propertyName, newItem).expect("Failed to set enum value in properties pane");
            model->setData(index, QString::fromStdString(newItem.Name()), Qt::DisplayRole);
        } else if (meta.type.descriptor->fromString) {
            QLineEdit* lineEdit = dynamic_cast<QLineEdit*>(editor);

            result<Variant, DataParseError> parsedResult = meta.type.descriptor->fromString(lineEdit->text().toStdString(), meta.type);
            if (!parsedResult) return;
            Variant parsedValue = parsedResult.expect();
            inst->SetProperty(propertyName, parsedValue).expect();
            model->setData(index, QString::fromStdString(parsedValue.ToString()));
            view->rebuildCompositeProperty(view->itemFromIndex(index), meta.type.descriptor, parsedValue);
        }

        Variant newValue = inst->GetProperty(propertyName).expect();
        view->undoManager->PushState({ UndoStatePropertyChanged { inst, propertyName, oldValue, newValue } });
    }
};

PropertiesView::PropertiesView(QWidget* parent):
    QTreeWidget(parent) {

    clear();
    setHeaderHidden(true);
    setColumnCount(2);
    setAlternatingRowColors(true);
    setItemDelegate(new PropertiesItemDelegate(this));

    connect(this, &QTreeWidget::itemChanged, this, &PropertiesView::propertyChanged);
    connect(this, &QTreeWidget::itemActivated, this, [&](auto* item, int column) {
        // Prevent editing the first column
        if (column == 0)
            item->setFlags(item->flags() & ~Qt::ItemIsEditable);
        else if (item->parent())
            item->setFlags(item->flags() | Qt::ItemIsEditable);
    });

    addPropertyUpdateListener(std::bind(&PropertiesView::onPropertyUpdated, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
}

PropertiesView::~PropertiesView() {
}

QStringList PROPERTY_CATEGORY_NAMES {
    "Appearence",
    "Data",
    "Behavior",
    "Part",
    "Surface",
    "Surface Inputs",
};

void PropertiesView::init() {
    undoManager = &dynamic_cast<MainWindow*>(window())->undoManager;
}

QModelIndex PropertiesView::indexAt(const QPoint &point) const {
    return QTreeWidget::indexAt(point + QPoint(indentation(), 0));
}

void PropertiesView::drawBranches(QPainter *painter, const QRect &rect, const QModelIndex &index) const {
    // https://codebrowser.dev/qt5/qtbase/src/widgets/itemviews/qtreeview.cpp.html#312opt

    // Draw background for headings
    if (!index.parent().isValid())
        painter->fillRect(rect, palette().dark());

    QTreeWidget::drawBranches(painter, rect, index);
}

void PropertiesView::setSelected(nullable std::shared_ptr<Instance> instance) {
    clear();
    currentInstance = {};
    if (!instance) return;
    std::shared_ptr<Instance> inst = instance;
    currentInstance = inst;

    std::map<PropertyCategory, QTreeWidgetItem*> propertyCategories;

    for (int i = 0; i <= PROPERTY_CATEGORY_MAX; i++) {
        QTreeWidgetItem* item = new QTreeWidgetItem;

        QBrush brush;
        brush.setColor(QPalette::Midlight);
        brush.setStyle(Qt::SolidPattern);

        item->setData(0, Qt::DisplayRole, PROPERTY_CATEGORY_NAMES[i]);
        item->setFirstColumnSpanned(true);

        propertyCategories[(PropertyCategory)i] = item;
        addTopLevelItem(item);
    }

    std::vector<std::string> properties = inst->GetProperties();
    std::sort(properties.begin(),properties.end(), [&](auto a, auto b) { return a < b; });

    for (std::string property : properties) {
        PropertyMeta meta = inst->GetPropertyMeta(property).expect();
        Variant currentValue = inst->GetProperty(property).expect();

        if (meta.type.descriptor == &CFrame::TYPE || meta.flags & PROP_HIDDEN) continue;

        QTreeWidgetItem* item = new QTreeWidgetItem;
        item->setFlags(item->flags() | Qt::ItemIsEditable | Qt::ItemIsSelectable);
        item->setData(0, Qt::DisplayRole, QString::fromStdString(property));
        
        if (meta.type.descriptor == &BOOL_TYPE) {
            item->setCheckState(1, (bool)currentValue.get<bool>() ? Qt::CheckState::Checked : Qt::CheckState::Unchecked);
        } else if (meta.type.descriptor == &Color3::TYPE) {
            Color3 color = currentValue.get<Color3>();
            item->setData(1, Qt::DecorationRole, QColor::fromRgbF(color.R(), color.G(), color.B()));
            item->setData(1, Qt::DisplayRole, QString::fromStdString(currentValue.ToString()));
        } else if (meta.type.descriptor == &Vector3::TYPE) {
            Vector3 vector = currentValue.get<Vector3>();
            item->setData(1, Qt::DisplayRole, QString::fromStdString(currentValue.ToString()));
        // } else if (meta.type.descriptor == &CFrame::TYPE) {
        //     Vector3 vector = currentValue.get<CFrame>().Position();
        //     item->setData(1, Qt::DisplayRole, QString::fromStdString(currentValue.ToString()));
        } else if (meta.type.descriptor == &EnumItem::TYPE) {
            item->setData(1, Qt::DisplayRole, QString::fromStdString(currentValue.get<EnumItem>().Name()));
        }  else {
            item->setData(1, Qt::DisplayRole, QString::fromStdString(currentValue.ToString()));
        }

        if (meta.type.descriptor != &Color3::TYPE && (!meta.type.descriptor->fromString || meta.flags & PROP_READONLY)) {
            item->setDisabled(true);
        }

        rebuildCompositeProperty(item, meta.type.descriptor, currentValue);

        propertyCategories[meta.category]->addChild(item);
        propertyCategories[meta.category]->setExpanded(true);
    }

    // Remove child-less categories
    for (int i = 0; i <= PROPERTY_CATEGORY_MAX; i++) {
        if (i == PROP_CATEGORY_SURFACE_INPUT)
            propertyCategories[(PropertyCategory)i]->setExpanded(false);

        if (propertyCategories[(PropertyCategory)i]->childCount() > 0) continue;
        int idx = indexOfTopLevelItem(propertyCategories[(PropertyCategory)i]);
        delete takeTopLevelItem(idx);
    }

    resizeColumnToContents(0);
}

void PropertiesView::propertyChanged(QTreeWidgetItem *item, int column) {
    // Necessary because otherwise this will catch setCheckState from onPropertyUpdated
    if (ignorePropertyUpdates) return;
    if (!item->parent() || (item->parent() && item->parent()->parent()) || currentInstance.expired()) return;
    std::shared_ptr<Instance> inst = currentInstance.lock();

    std::string propertyName = item->data(0, Qt::DisplayRole).toString().toStdString();
    PropertyMeta meta = inst->GetPropertyMeta(propertyName).expect();

    if (meta.type.descriptor == &BOOL_TYPE) {
        inst->SetProperty(propertyName, item->checkState(1) == Qt::Checked).expect();
        undoManager->PushState({ UndoStatePropertyChanged { inst, propertyName, item->checkState(1) != Qt::Checked, item->checkState(1) == Qt::Checked } });
    }
}

void PropertiesView::rebuildCompositeProperty(QTreeWidgetItem *item, const TypeDesc* type, Variant value) {
    if (type == &Vector3::TYPE) {
        // https://forum.qt.io/post/266837
        foreach(auto i, item->takeChildren()) delete i;

        Vector3 vector = value.get<Vector3>();
        item->setData(1, Qt::DisplayRole, QString::fromStdString(value.ToString()));

        QTreeWidgetItem* xItem = new QTreeWidgetItem;
        xItem->setData(0, Qt::DisplayRole, "X");
        xItem->setData(1, Qt::DisplayRole, vector.X());

        QTreeWidgetItem* yItem = new QTreeWidgetItem;
        yItem->setData(0, Qt::DisplayRole, "Y");
        yItem->setData(1, Qt::DisplayRole, vector.Y());

        QTreeWidgetItem* zItem = new QTreeWidgetItem;
        zItem->setData(0, Qt::DisplayRole, "Z");
        zItem->setData(1, Qt::DisplayRole, vector.Z());

        item->addChild(xItem);
        item->addChild(yItem);
        item->addChild(zItem);
    }
}

// static auto lastUpdateTime = std::chrono::steady_clock::now();
void PropertiesView::onPropertyUpdated(std::shared_ptr<Instance> inst, std::string property, Variant newValue) {
    // if (!currentInstance || currentInstance->expired() || instance != currentInstance->lock()) return;
    // if (std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - lastUpdateTime).count() < 1000) return;
    // lastUpdateTime = std::chrono::steady_clock::now();

    PropertyMeta meta = inst->GetPropertyMeta(property).expect();
    Variant currentValue = inst->GetProperty(property).expect();
    
    if (meta.type.descriptor == &CFrame::TYPE) return;

    for (int categoryItemIdx = 0; categoryItemIdx < topLevelItemCount(); categoryItemIdx++) {
        QTreeWidgetItem* categoryItem = topLevelItem(categoryItemIdx);
        for (int itemIdx = 0; itemIdx < categoryItem->childCount(); itemIdx++) {
            QTreeWidgetItem* item = categoryItem->child(itemIdx);

            if (item->data(0, Qt::DisplayRole).toString().toStdString() != property) continue;

            if (meta.type.descriptor == &BOOL_TYPE) {
                // This is done because otherwise propertyChanged will catch this change erroneously
                ignorePropertyUpdates = true;
                item->setCheckState(1, (bool)currentValue.get<bool>() ? Qt::CheckState::Checked : Qt::CheckState::Unchecked);
                ignorePropertyUpdates = false;
            } else if (meta.type.descriptor == &Color3::TYPE) {
                Color3 color = currentValue.get<Color3>();
                item->setData(1, Qt::DecorationRole, QColor::fromRgbF(color.R(), color.G(), color.B()));
                item->setData(1, Qt::DisplayRole, QString::fromStdString(currentValue.ToString()));
            } else if (meta.type.descriptor == &Vector3::TYPE) {
                Vector3 vector = currentValue.get<Vector3>();
                item->setData(1, Qt::DisplayRole, QString::fromStdString(currentValue.ToString()));
            } else {
                item->setData(1, Qt::DisplayRole, QString::fromStdString(currentValue.ToString()));
            }

            if (meta.type.descriptor != &Color3::TYPE && (!meta.type.descriptor->fromString || meta.flags & PROP_READONLY)) {
                item->setDisabled(true);
            }

            rebuildCompositeProperty(item, meta.type.descriptor, currentValue);

            return;
        }
    }
}