/*
 * IdealIRC - Internet Relay Chat client
 * Copyright (C) 2021  Tom-Andre Barstad.
 * This software is licensed under the Software Attribution License.
 * See LICENSE for more information.
*/

#include "ScriptDialog.h"
#include "Script/Dialog.h"
#include "Script/ScriptException.h"
#include "Script/ValueExtract.h"
#include "Script/Builtin/ListUtils.h" // Needed for LIST_END_MAGIC

#include "IdealIRC.h"

#include <QDialog>
#include <QLabel>
#include <QPushButton>
#include <QLineEdit>
#include <QPlainTextEdit>
#include <QSpinBox>
#include <QListWidget>
#include <QComboBox>
#include <QTableWidget>
#include <QHeaderView>
#include <QTreeWidget>
#include <QGroupBox>
#include <QRadioButton>
#include <QCheckBox>
#include <QScrollBar>
#include <unordered_map>
#include <string>
#include <stdexcept>

extern IdealIRC* IIRC_MainWindow_Ptr;

struct ScriptWidget
{
    explicit ScriptWidget(Script& script_) : script(script_) {}

    std::unordered_map<std::string, ValueHolder> customAttrs;
    QWidget* widget{ nullptr }; // Pointer owned by the QDialog
    const DialogWidget* tmpl{ nullptr };
    Script& script;
};

struct ScriptDialogPriv
{
    explicit ScriptDialogPriv(Script& script_)
        : dialog(new QDialog(nullptr))
        , script(script_)
    {
        dialog->setAttribute(Qt::WA_DeleteOnClose, false);
    }

    std::unordered_map<std::string, ValueHolder> customAttrs;
    std::unordered_map<std::string, ScriptWidget> widgets;
    std::unique_ptr<QDialog> dialog;
    const Dialog* tmpl{ nullptr };
    Script& script;
};

namespace {

template<DialogWidget::Type type, typename T>
void buildWidget(T* widget, const DialogWidget& tmpl)
{
    ValueHolder valX = tmpl.defaultAttr("x");
    ValueHolder valY = tmpl.defaultAttr("y");
    ValueHolder valWidth = tmpl.defaultAttr("width");
    ValueHolder valHeight = tmpl.defaultAttr("height");
    ValueHolder valFont = tmpl.defaultAttr("font");
    ValueHolder valFontsize = tmpl.defaultAttr("fontsize");
    ValueHolder valEnabled = tmpl.defaultAttr("enabled");
    ValueHolder valVisible = tmpl.defaultAttr("visible");
    ValueHolder valText = tmpl.defaultAttr("text");
    ValueHolder valTooltip = tmpl.defaultAttr("tooltip");

    auto rect = widget->geometry();

    if (valX.getType() != ValueHolder::Type::Void)
        rect.setX(ValueExtract(valX).toInt());
    if (valY.getType() != ValueHolder::Type::Void)
        rect.setY(ValueExtract(valY).toInt());
    if (valWidth.getType() != ValueHolder::Type::Void)
        rect.setWidth(ValueExtract(valWidth).toInt());
    if (valHeight.getType() != ValueHolder::Type::Void)
        rect.setHeight(ValueExtract(valHeight).toInt());

    widget->setGeometry(rect);

    auto font = widget->font();
    if (valFont.getType() != ValueHolder::Type::Void)
        font.setFamily(ValueExtract(valFont).toString().c_str());

    if (valFontsize.getType() != ValueHolder::Type::Void)
        font.setPixelSize(ValueExtract(valFontsize).toInt());

    widget->setFont(font);

    if (valEnabled.getType() != ValueHolder::Type::Void)
        widget->setEnabled(ValueExtract(valEnabled).toBool());

    if (valVisible.getType() != ValueHolder::Type::Void)
        widget->setVisible(ValueExtract(valVisible).toBool());

    if (valTooltip.getType() != ValueHolder::Type::Void)
        widget->setToolTip(ValueExtract(valHeight).toString().c_str());


    using WType = DialogWidget::Type;
    if constexpr (type == WType::Spinbox) {
        ValueHolder value = tmpl.defaultAttr("value");
        ValueHolder min = tmpl.defaultAttr("min");
        ValueHolder max = tmpl.defaultAttr("max");
        ValueHolder step  = tmpl.defaultAttr("step");
        if (min.getType() == ValueHolder::Type::Void)
            throw ScriptException("Attribute 'min' is required for Spinbox");
        if (max.getType() == ValueHolder::Type::Void)
            throw ScriptException("Attribute 'max' is required for Spinbox");


        if (min.getType() != ValueHolder::Type::Integer)
            throw ScriptException("Attribute 'min' for Spinbox must be a whole number");
        if (max.getType() != ValueHolder::Type::Integer)
            throw ScriptException("Attribute 'max' for Spinbox must be a whole number");

        if (value.getType() == ValueHolder::Type::Void)
            value = min;

        if (step.getType() == ValueHolder::Type::Void)
            step = 1;

        if (value.getType() != ValueHolder::Type::Integer)
            throw ScriptException("Attribute 'value' for Spinbox must be a whole number");
        if (step.getType() != ValueHolder::Type::Integer)
            throw ScriptException("Attribute 'step' for Spinbox must be a whole number");

        widget->setMinimum(ValueExtract(min).toInt());
        widget->setMaximum(ValueExtract(max).toInt());
        widget->setValue(ValueExtract(value).toInt());
        widget->setSingleStep(ValueExtract(step).toInt());
    }
    else if constexpr (type == WType::Textbox) {
        ValueHolder readOnly = tmpl.defaultAttr("readonly");
        if (readOnly.getType() != ValueHolder::Type::Void)
            widget->setEnabled(ValueExtract(readOnly).toBool());
        widget->setPlainText(ValueExtract(valText).toString().c_str());
    }
    else if constexpr (type == WType::Groupbox) {
        ValueHolder readOnly = tmpl.defaultAttr("readonly");
        if (readOnly.getType() != ValueHolder::Type::Void)
            widget->setEnabled(ValueExtract(readOnly).toBool());
        widget->setTitle(ValueExtract(valText).toString().c_str());
    }
    else if constexpr (type == WType::Button) {
        ValueHolder checkable = tmpl.defaultAttr("checkable");
        ValueHolder checked = tmpl.defaultAttr("checked");
        if (checkable.getType() != ValueHolder::Type::Void)
            widget->setCheckable(ValueExtract(checkable).toBool());
        if (checked.getType() != ValueHolder::Type::Void)
            widget->setChecked(ValueExtract(checked).toBool());
        widget->setText(ValueExtract(valText).toString().c_str());
    }
    else if constexpr (type == WType::Checkbox) {
        ValueHolder checked = tmpl.defaultAttr("checked");
        if (checked.getType() != ValueHolder::Type::Void)
            widget->setChecked(ValueExtract(checked).toBool());
        widget->setText(ValueExtract(valText).toString().c_str());
    }
    else if constexpr (type == WType::Radio) {
        ValueHolder checked = tmpl.defaultAttr("checked");
        if (checked.getType() != ValueHolder::Type::Void)
            widget->setChecked(ValueExtract(checked).toBool());
        widget->setText(ValueExtract(valText).toString().c_str());
    }
    else if constexpr (type == WType::Table) {

    }
    else if constexpr (type != WType::Vscroll && type != WType::Hscroll
                        && type != WType::Listbox && type != WType::Combobox
                        && type != WType::Tree) {
        widget->setText(ValueExtract(valText).toString().c_str());
    }
}

void widgetSetText(ScriptWidget& widget, ValueHolder& valh)
{
    std::string text = ValueExtract(valh).toString();

    using WType = DialogWidget::Type;
    if (widget.tmpl->type() == WType::Label) {
        auto* cw = dynamic_cast<QLabel*>(widget.widget);
        cw->setText(text.c_str());
    }
    else if (widget.tmpl->type() == WType::Groupbox) {
        auto* cw = dynamic_cast<QGroupBox*>(widget.widget);
        cw->setTitle(text.c_str());
    }
    else if (widget.tmpl->type() == WType::Textline) {
        auto* cw = dynamic_cast<QLineEdit*>(widget.widget);
        cw->setText(text.c_str());
    }
    else if (widget.tmpl->type() == WType::Textbox) {
        auto* cw = dynamic_cast<QPlainTextEdit*>(widget.widget);
        cw->setPlainText(text.c_str());
    }
    else if (widget.tmpl->type() == WType::Button) {
        auto* cw = dynamic_cast<QPushButton*>(widget.widget);
        cw->setText(text.c_str());
    }
    else if (widget.tmpl->type() == WType::Radio) {
        auto* cw = dynamic_cast<QRadioButton*>(widget.widget);
        cw->setText(text.c_str());
    }
    else if (widget.tmpl->type() == WType::Checkbox) {
        auto* cw = dynamic_cast<QCheckBox*>(widget.widget);
        cw->setText(text.c_str());
    }
    else {
        widget.customAttrs.insert_or_assign("text", valh);
    }
}

void widgetSetCheckable(ScriptWidget& widget, ValueHolder& valh)
{
    bool value = ValueExtract(valh).toBool();
    using WType = DialogWidget::Type;
    if (widget.tmpl->type() == WType::Button) {
        auto* cw = dynamic_cast<QPushButton*>(widget.widget);
        cw->setCheckable(value);
    }
    else {
        widget.customAttrs.insert_or_assign("checkable", valh);
    }
}

void widgetSetChecked(ScriptWidget& widget, ValueHolder& valh)
{
    bool value = ValueExtract(valh).toBool();
    using WType = DialogWidget::Type;
    if (widget.tmpl->type() == WType::Button) {
        auto* cw = dynamic_cast<QPushButton*>(widget.widget);
        cw->setChecked(value);
    }
    else if (widget.tmpl->type() == WType::Radio) {
        auto* cw = dynamic_cast<QRadioButton*>(widget.widget);
        cw->setChecked(value);
    }
    else if (widget.tmpl->type() == WType::Checkbox) {
        auto* cw = dynamic_cast<QCheckBox*>(widget.widget);
        cw->setChecked(value);
    }
    else {
        widget.customAttrs.insert_or_assign("checked", valh);
    }
}

void widgetSetReadOnly(ScriptWidget& widget, ValueHolder& valh)
{
    bool value = ValueExtract(valh).toBool();
    using WType = DialogWidget::Type;
    if (widget.tmpl->type() == WType::Textline) {
        auto* cw = dynamic_cast<QLineEdit*>(widget.widget);
        cw->setReadOnly(value);
    }
    else if (widget.tmpl->type() == WType::Textbox) {
        auto* cw = dynamic_cast<QPlainTextEdit*>(widget.widget);
        cw->setReadOnly(value);
    }
    else {
        widget.customAttrs.insert_or_assign("readonly", valh);
    }
}

void widgetSetValue(ScriptWidget& widget, ValueHolder& valh)
{
    int value = ValueExtract(valh).toInt();
    using WType = DialogWidget::Type;
    if (widget.tmpl->type() == WType::Spinbox) {
        auto* cw = dynamic_cast<QSpinBox*>(widget.widget);
        cw->setValue(value);
    }
    else {
        widget.customAttrs.insert_or_assign("value", valh);
    }
}

void widgetSetMin(ScriptWidget& widget, ValueHolder& valh)
{
    int value = ValueExtract(valh).toInt();
    using WType = DialogWidget::Type;
    if (widget.tmpl->type() == WType::Spinbox) {
        auto* cw = dynamic_cast<QSpinBox*>(widget.widget);
        cw->setMinimum(value);
    }
    else {
        widget.customAttrs.insert_or_assign("min", valh);
    }
}

void widgetSetMax(ScriptWidget& widget, ValueHolder& valh)
{
    int value = ValueExtract(valh).toInt();
    using WType = DialogWidget::Type;
    if (widget.tmpl->type() == WType::Spinbox) {
        auto* cw = dynamic_cast<QSpinBox*>(widget.widget);
        cw->setMaximum(value);
    }
    else {
        widget.customAttrs.insert_or_assign("max", valh);
    }
}

void widgetSetStep(ScriptWidget& widget, ValueHolder& valh)
{
    int value = ValueExtract(valh).toInt();
    using WType = DialogWidget::Type;
    if (widget.tmpl->type() == WType::Spinbox) {
        auto* cw = dynamic_cast<QSpinBox*>(widget.widget);
        cw->setSingleStep(value);
    }
    else {
        widget.customAttrs.insert_or_assign("step", valh);
    }
}

void widgetSetHeader(ScriptWidget& widget, ValueHolder& valh)
{
    ValueArray& list = ValueExtract(valh).toMap();
    using WType = DialogWidget::Type;
    if (widget.tmpl->type() == WType::Table) {
        auto* cw = dynamic_cast<QTableWidget*>(widget.widget);
        QStringList header;
        for (int i = 0; ; ++i) {
            std::string si = std::to_string(i);
            try {
                ValueHolder& val = *list.at(si).get();
                switch (val.getType()) {
                case ValueHolder::Type::Integer:
                    header.push_back(QString::number(ValueExtract(val).toInt()));
                    break;
                case ValueHolder::Type::Real:
                    header.push_back(QString::number(ValueExtract(val).toReal()));
                    break;
                case ValueHolder::Type::Bool:
                    header.push_back(ValueExtract(val).toBool() ? QStringLiteral("True") : QStringLiteral("False"));
                    break;
                case ValueHolder::Type::String:
                    header.push_back(ValueExtract(val).toString().c_str());
                    break;
                default:
                    header.push_back(QStringLiteral(""));
                }
            }  catch (const std::out_of_range&) {
                break;
            }
        }
        cw->setColumnCount(header.size());
        cw->setHorizontalHeaderLabels(header);
        cw->resizeColumnsToContents();
    }
    else {
        widget.customAttrs.insert_or_assign("step", valh);
    }
}

ValueHolder widgetGetText(ScriptWidget& widget)
{
    using WType = DialogWidget::Type;
    if (widget.tmpl->type() == WType::Label) {
        auto* cw = dynamic_cast<QLabel*>(widget.widget);
        return cw->text().toStdString();
    }
    else if (widget.tmpl->type() == WType::Groupbox) {
        auto* cw = dynamic_cast<QGroupBox*>(widget.widget);
        return cw->title().toStdString();
    }
    else if (widget.tmpl->type() == WType::Textline) {
        auto* cw = dynamic_cast<QLineEdit*>(widget.widget);
        return cw->text().toStdString();
    }
    else if (widget.tmpl->type() == WType::Textbox) {
        auto* cw = dynamic_cast<QPlainTextEdit*>(widget.widget);
        return cw->toPlainText().toStdString();
    }
    else if (widget.tmpl->type() == WType::Button) {
        auto* cw = dynamic_cast<QPushButton*>(widget.widget);
        return cw->text().toStdString();
    }
    else if (widget.tmpl->type() == WType::Radio) {
        auto* cw = dynamic_cast<QRadioButton*>(widget.widget);
        return cw->text().toStdString();
    }
    else if (widget.tmpl->type() == WType::Checkbox) {
        auto* cw = dynamic_cast<QCheckBox*>(widget.widget);
        return cw->text().toStdString();
    }
    else {
        ValueHolder val;
        try {
            val = widget.customAttrs.at("text");
        }
        catch (...) {}
        return val;
    }
}

ValueHolder widgetGetCheckable(ScriptWidget& widget)
{
    using WType = DialogWidget::Type;
    if (widget.tmpl->type() == WType::Button) {
        auto* cw = dynamic_cast<QPushButton*>(widget.widget);
        return cw->isCheckable();
    }
    else {
        ValueHolder val;
        try {
            val = widget.customAttrs.at("checkable");
        }
        catch (...) {}
        return val;
    }
}

ValueHolder widgetGetChecked(ScriptWidget& widget)
{
    using WType = DialogWidget::Type;
    if (widget.tmpl->type() == WType::Button) {
        auto* cw = dynamic_cast<QPushButton*>(widget.widget);
        return cw->isChecked();
    }
    else if (widget.tmpl->type() == WType::Radio) {
        auto* cw = dynamic_cast<QRadioButton*>(widget.widget);
        return cw->isChecked();
    }
    else if (widget.tmpl->type() == WType::Checkbox) {
        auto* cw = dynamic_cast<QCheckBox*>(widget.widget);
        return cw->isChecked();
    }
    else {
        ValueHolder val;
        try {
            val = widget.customAttrs.at("checked");
        }
        catch (...) {}
        return val;
    }
}

ValueHolder widgetGetReadOnly(ScriptWidget& widget)
{
    using WType = DialogWidget::Type;
    if (widget.tmpl->type() == WType::Textline) {
        auto* cw = dynamic_cast<QLineEdit*>(widget.widget);
        return cw->isReadOnly();
    }
    else if (widget.tmpl->type() == WType::Textbox) {
        auto* cw = dynamic_cast<QPlainTextEdit*>(widget.widget);
        return cw->isReadOnly();
    }
    else {
        ValueHolder val;
        try {
            val = widget.customAttrs.at("readonly");
        }
        catch (...) {}
        return val;
    }
}

ValueHolder widgetGetValue(ScriptWidget& widget)
{
    using WType = DialogWidget::Type;
    if (widget.tmpl->type() == WType::Spinbox) {
        auto* cw = dynamic_cast<QSpinBox*>(widget.widget);
        return cw->value();
    }
    else {
        ValueHolder val;
        try {
            val = widget.customAttrs.at("value");
        }
        catch (...) {}
        return val;
    }
}

ValueHolder widgetGetMin(ScriptWidget& widget)
{
    using WType = DialogWidget::Type;
    if (widget.tmpl->type() == WType::Spinbox) {
        auto* cw = dynamic_cast<QSpinBox*>(widget.widget);
        return cw->minimum();
    }
    else {
        ValueHolder val;
        try {
            val = widget.customAttrs.at("min");
        }
        catch (...) {}
        return val;
    }
}

ValueHolder widgetGetMax(ScriptWidget& widget)
{
    using WType = DialogWidget::Type;
    if (widget.tmpl->type() == WType::Spinbox) {
        auto* cw = dynamic_cast<QSpinBox*>(widget.widget);
        return cw->maximum();
    }
    else {
        ValueHolder val;
        try {
            val = widget.customAttrs.at("max");
        }
        catch (...) {}
        return val;
    }
}

ValueHolder widgetGetStep(ScriptWidget& widget)
{
    using WType = DialogWidget::Type;
    if (widget.tmpl->type() == WType::Spinbox) {
        auto* cw = dynamic_cast<QSpinBox*>(widget.widget);
        return cw->singleStep();
    }
    else {
        ValueHolder val;
        try {
            val = widget.customAttrs.at("step");
        }
        catch (...) {}
        return val;
    }
}

ValueHolder widgetGetHeader(ScriptWidget& widget)
{
    using WType = DialogWidget::Type;
    if (widget.tmpl->type() == WType::Table) {
        auto* cw = dynamic_cast<QTableWidget*>(widget.widget);
        ValueArray header;
        for (int i = 0; ; ++i) {
            auto* item = cw->horizontalHeaderItem(i);
            if (!item)
                break;
            header.emplace(std::to_string(i), new ValueHolder(item->text().toStdString()));
        }
        return ValueHolder(std::move(header));
    }
    else {
        ValueHolder val;
        try {
            val = widget.customAttrs.at("header");
        }
        catch (...) {}
        return val;
    }
}

} // anonymous namespace

ScriptDialog::ScriptDialog(const Dialog& dialog, Script& script, int handle)
    : mp(new ScriptDialogPriv(script))
{
    ValueHolder valX = dialog.defaultAttr("x");
    ValueHolder valY = dialog.defaultAttr("y");
    ValueHolder valWidth = dialog.defaultAttr("width");
    ValueHolder valHeight = dialog.defaultAttr("height");
    ValueHolder valTitle = dialog.defaultAttr("title");
    ValueHolder valFont = dialog.defaultAttr("font");
    ValueHolder valFontsize = dialog.defaultAttr("fontsize");
    ValueHolder valIcon = dialog.defaultAttr("icon");

    mp->tmpl = &dialog;

    ValueHolder voidValue;
    auto IIRC_rect = IIRC_MainWindow_Ptr->geometry();

    if (valWidth != voidValue)
        setAttr("width", valWidth);
    if (valHeight != voidValue)
        setAttr("height", valHeight);

    if (valX != voidValue)
        setAttr("x", valX);
    else {
        int width = ValueExtract(valWidth).toInt();
        ValueHolder x = (2 * IIRC_rect.x() + IIRC_rect.width() - width) / 2;
        setAttr("x", x);
    }

    if (valY != voidValue)
        setAttr("y", valY);
    else {
        int height = ValueExtract(valHeight).toInt();
        ValueHolder y = (2 * IIRC_rect.y() + IIRC_rect.height() - height) / 2;
        setAttr("y", y);
    }

    if (valTitle != voidValue)
        setAttr("title", valTitle);
    else {
        ValueHolder defaultTitle = std::string("Unnamed dialog");
        setAttr("title", defaultTitle);
    }

    if (valFont != voidValue)
        setAttr("font", valFont);

    if (valFontsize != voidValue)
        setAttr("fontsize", valFontsize);

    if (valIcon != voidValue)
        setAttr("icon", valIcon);

    for (auto wref : dialog.allWidgets()) {
        auto& widget = wref.get();

        mp->widgets.emplace(widget.name(), ScriptWidget(script));
        auto& qw = mp->widgets.at(widget.name());
        qw.tmpl = &widget;

        using WType = DialogWidget::Type;
        switch (widget.type()) {
        case WType::Label:
        {
            auto* w = new QLabel(mp->dialog.get());
            buildWidget<WType::Label, QLabel>(w, widget);
            qw.widget = w;
            break;
        }

        case WType::Button:
        {
            auto* w = new QPushButton(mp->dialog.get());
            buildWidget<WType::Button, QPushButton>(w, widget);
            qw.widget = w;

            const DialogWidget* widgetTemplate = qw.tmpl;
            Script* scr = &mp->script;
            connect(w, &QPushButton::clicked, [scr, widgetTemplate, handle]{
                auto hook = widgetTemplate->hook("clicked");
                if (!hook) return;
                SymbolScope sym;
                sym.set("handle", handle);
                if (!scr->runScopeWithSymbols(*hook, sym))
                    ScriptManager::printError(scr->lastError());
            });

            break;
        }

        case WType::Textline:
        {
            auto* w = new QLineEdit(mp->dialog.get());
            buildWidget<WType::Textline, QLineEdit>(w, widget);
            qw.widget = w;
            break;
        }

        case WType::Textbox:
        {
            auto* w = new QPlainTextEdit(mp->dialog.get());
            buildWidget<WType::Textbox, QPlainTextEdit>(w, widget);
            qw.widget = w;
            break;
        }

        case WType::Spinbox:
        {
            auto* w = new QSpinBox(mp->dialog.get());
            buildWidget<WType::Spinbox, QSpinBox>(w, widget);
            qw.widget = w;
            break;
        }

        case WType::Listbox:
        {
            auto* w = new QListWidget(mp->dialog.get());
            buildWidget<WType::Listbox, QListWidget>(w, widget);
            qw.widget = w;
            break;
        }

        case WType::Combobox:
        {
            auto* w = new QComboBox(mp->dialog.get());
            buildWidget<WType::Combobox, QComboBox>(w, widget);
            qw.widget = w;
            break;
        }

        case WType::Table:
        {
            auto* w = new QTableWidget(mp->dialog.get());
            buildWidget<WType::Table, QTableWidget>(w, widget);
            qw.widget = w;
            w->setSelectionMode(QAbstractItemView::SingleSelection);
            w->setSelectionBehavior(QAbstractItemView::SelectRows);
            break;
        }

        case WType::Tree:
        {
            auto* w = new QTreeWidget(mp->dialog.get());
            buildWidget<WType::Tree, QTreeWidget>(w, widget);
            qw.widget = w;
            break;
        }

        case WType::Groupbox:
        {
            auto* w = new QGroupBox(mp->dialog.get());
            buildWidget<WType::Groupbox, QGroupBox>(w, widget);
            qw.widget = w;
            break;
        }

        // TODO case DialogWidget::Type::Tab:
        //	break;
        case WType::Radio:
        {
            auto* w = new QRadioButton(mp->dialog.get());
            buildWidget<WType::Radio, QRadioButton>(w, widget);
            qw.widget = w;
            break;
        }

        case WType::Checkbox:
        {
            auto* w = new QCheckBox(mp->dialog.get());
            buildWidget<WType::Checkbox, QCheckBox>(w, widget);
            qw.widget = w;
            break;
        }

        case WType::Vscroll:
        {
            auto* w = new QScrollBar(mp->dialog.get());
            buildWidget<WType::Vscroll, QScrollBar>(w, widget);
            qw.widget = w;
            break;
        }

        case WType::Hscroll:
        {
            auto* w = new QScrollBar(mp->dialog.get());
            buildWidget<WType::Hscroll, QScrollBar>(w, widget);
            qw.widget = w;
            break;
        }

        // TODO case DialogWidget::Type::Canvas:
        //	break;
        }
    }

    show();
}

ScriptDialog::ScriptDialog(ScriptDialog&& other)
    : mp(std::move(other.mp))
{}

ScriptDialog::~ScriptDialog() = default;

void ScriptDialog::setAttr(const std::string& key, ValueHolder& value)
{
    if (key == "x") {
        auto rect = mp->dialog->geometry();
        rect.setX(ValueExtract(value).toInt());
        mp->dialog->setGeometry(rect);
    }
    else if (key == "y") {
        auto rect = mp->dialog->geometry();
        rect.setY(ValueExtract(value).toInt());
        mp->dialog->setGeometry(rect);
    }
    else if (key == "width") {
        auto rect = mp->dialog->geometry();
        int width = ValueExtract(value).toInt();
        rect.setWidth(width);
        mp->dialog->setGeometry(rect);
        mp->dialog->setMinimumWidth(width);
        mp->dialog->setMaximumWidth(width);
    }
    else if (key == "height") {
        auto rect = mp->dialog->geometry();
        int height = ValueExtract(value).toInt();
        rect.setHeight(height);
        mp->dialog->setGeometry(rect);
        mp->dialog->setMinimumHeight(height);
        mp->dialog->setMaximumHeight(height);
    }
    else if (key == "title") {
        mp->dialog->setWindowTitle(ValueExtract(value).toString().c_str());
    }
    else if (key == "font") {
        QFont font = mp->dialog->font();
        font.setFamily(ValueExtract(value).toString().c_str());
        mp->dialog->setFont(font);
    }
    else if (key == "fontsize") {
        QFont font = mp->dialog->font();
        font.setPixelSize(ValueExtract(value).toInt());
        mp->dialog->setFont(font);
    }
    else if (key == "icon") {
        mp->dialog->setWindowIcon(QIcon(ValueExtract(value).toString().c_str()));
    }
    else {
        mp->customAttrs.insert_or_assign(key, value);
    }
}

ValueHolder ScriptDialog::attr(const std::string& key) const
{
    if (key == "x") {
        auto rect = mp->dialog->geometry();
        return rect.x();
    }
    else if (key == "y") {
        auto rect = mp->dialog->geometry();
        return rect.y();
    }
    else if (key == "width") {
        auto rect = mp->dialog->geometry();
        return rect.width();
    }
    else if (key == "height") {
        auto rect = mp->dialog->geometry();
        return rect.height();
    }
    else if (key == "title") {
        return mp->dialog->windowTitle().toStdString();
    }
    else if (key == "font") {
        QFont font = mp->dialog->font();
        return font.family().toStdString();
    }
    else if (key == "fontsize") {
        QFont font = mp->dialog->font();
        return font.pixelSize();
    }
    else if (key == "icon") {
        return mp->dialog->windowIcon().name().toStdString();
    }
    else {
        auto iter = mp->customAttrs.find(key);
        if (iter == mp->customAttrs.end())
            return ValueHolder();
        return iter->second;
    }
}

void ScriptDialog::setWidgetAttr(const std::string& widgetName, const std::string& key, ValueHolder& value)
{
    try {
        ScriptWidget& widget = mp->widgets.at(widgetName);
        if (key == "x") {
            auto rect = widget.widget->geometry();
            rect.setX(ValueExtract(value).toInt());
            widget.widget->setGeometry(rect);
        }
        else if (key == "y") {
            auto rect = widget.widget->geometry();
            rect.setY(ValueExtract(value).toInt());
            widget.widget->setGeometry(rect);
        }
        else if (key == "width") {
            auto rect = widget.widget->geometry();
            rect.setWidth(ValueExtract(value).toInt());
            widget.widget->setGeometry(rect);
        }
        else if (key == "height") {
            auto rect = widget.widget->geometry();
            rect.setHeight(ValueExtract(value).toInt());
            widget.widget->setGeometry(rect);
        }
        else if (key == "font") {
            QFont font = widget.widget->font();
            font.setFamily(ValueExtract(value).toString().c_str());
            widget.widget->setFont(font);
        }
        else if (key == "fontsize") {
            QFont font = widget.widget->font();
            font.setPixelSize(ValueExtract(value).toInt());
            widget.widget->setFont(font);
        }
        else if (key == "enabled") {
            widget.widget->setEnabled(ValueExtract(value).toBool());
        }
        else if (key == "visible") {
            widget.widget->setVisible(ValueExtract(value).toBool());
        }
        else if (key == "text") {
            widgetSetText(widget, value);
        }
        else if (key == "checkable") {
            widgetSetCheckable(widget, value);
        }
        else if (key == "checked") {
            widgetSetChecked(widget, value);
        }
        else if (key == "readonly") {
            widgetSetReadOnly(widget, value);
        }
        else if (key == "tooltip") {
            widget.widget->setToolTip(ValueExtract(value).toString().c_str());
        }
        else if (key == "value") {
            widgetSetValue(widget, value);
        }
        else if (key == "min") {
            widgetSetMin(widget, value);
        }
        else if (key == "max") {
            widgetSetMax(widget, value);
        }
        else if (key == "step") {
            widgetSetStep(widget, value);
        }
        else if (key == "header") {
            widgetSetHeader(widget, value);
        }
        else {
            widget.customAttrs.insert_or_assign(key, value);
        }
    } catch (std::out_of_range&) {
        throw ScriptException("Widget not found: " + widgetName);
    }
}

ValueHolder ScriptDialog::widgetAttr(const std::string& widgetName, const std::string& key) const
{
    try {
        auto& widget = mp->widgets.at(widgetName);
        if (key == "x") {
            auto rect = widget.widget->geometry();
            return rect.x();
        }
        else if (key == "y") {
            auto rect = widget.widget->geometry();
            return rect.y();
        }
        else if (key == "width") {
            auto rect = widget.widget->geometry();
            return rect.width();
        }
        else if (key == "height") {
            auto rect = widget.widget->geometry();
            return rect.height();
        }
        else if (key == "font") {
            QFont font = widget.widget->font();
            return font.family().toStdString();
        }
        else if (key == "fontsize") {
            QFont font = widget.widget->font();
            return font.pixelSize();
        }
        else if (key == "enabled") {
            return widget.widget->isEnabled();
        }
        else if (key == "visible") {
            return widget.widget->isVisible();
        }
        else if (key == "text") {
            return widgetGetText(widget);
        }
        else if (key == "checkable") {
            return widgetGetCheckable(widget);
        }
        else if (key == "checked") {
            return widgetGetChecked(widget);
        }
        else if (key == "readonly") {
            return widgetGetReadOnly(widget);
        }
        else if (key == "tooltip") {
            return widget.widget->toolTip().toStdString();
        }
        else if (key == "value") {
            return widgetGetValue(widget);
        }
        else if (key == "min") {
            return widgetGetMin(widget);
        }
        else if (key == "max") {
            return widgetGetMax(widget);
        }
        else if (key == "step") {
            return widgetGetStep(widget);
        }
        else if (key == "header") {
            return widgetGetHeader(widget);
        }
        else {
            auto iter = widget.customAttrs.find(key);
            if (iter == widget.customAttrs.end())
                return ValueHolder();
            return iter->second;
        }
    } catch (std::out_of_range&) {
        throw ScriptException("Widget not found: " + widgetName);
    }
}

int ScriptDialog::tableWidgetCount(const std::string& widgetName)
{
    auto& baseWidget = mp->widgets.at(widgetName);
    auto* widget = dynamic_cast<QTableWidget*>(baseWidget.widget);
    if (!widget)
        throw ScriptException("Not a table widget");

    return widget->rowCount();
}

void ScriptDialog::tableWidgetInsert(const std::string& widgetName, const ValueArray& cols)
{
    auto& baseWidget = mp->widgets.at(widgetName);
    auto* widget = dynamic_cast<QTableWidget*>(baseWidget.widget);
    if (!widget)
        throw ScriptException("Not a table widget");

    const int rowCount = widget->rowCount() + 1;
    widget->setRowCount(rowCount);

    try {
        const auto& colCountHolder = cols.at(LIST_END_MAGIC);
        int colCount = ValueExtract(*(colCountHolder.get())).toInt();
        if (colCount > widget->columnCount())
            colCount = widget->columnCount();

        for (int i = 0; i < colCount; ++i) {
            auto& valHolder = *(cols.at(std::to_string(i)).get());
            QString value;
            switch (valHolder.getType()) {
            case ValueHolder::Type::Void:
                value = "[Void]";
                break;
            case ValueHolder::Type::Integer: {
                int n = ValueExtract(valHolder).toInt();
                value = QString::number(n);
                break;
            }
            case ValueHolder::Type::Real: {
                double n = ValueExtract(valHolder).toReal();
                value = QString::number(n);
                break;
            }
            case ValueHolder::Type::Bool: {
                bool n = ValueExtract(valHolder).toBool();
                value = QString::number(n);
                break;
            }
            case ValueHolder::Type::String:
                value = QString::fromStdString( ValueExtract(valHolder).toString() );
                break;
            case ValueHolder::Type::Map:
                value = "[Map]";
                break;
            }

            auto* item = new QTableWidgetItem(value);
            widget->setItem(rowCount - 1, i, item);
        }

        widget->resizeColumnsToContents();

    } catch (const std::out_of_range&) {
        throw ScriptException("Not a list");
    }
}

void ScriptDialog::tableWidgetRemove(const std::string& widgetName, int index)
{
    auto& baseWidget = mp->widgets.at(widgetName);
    auto* widget = dynamic_cast<QTableWidget*>(baseWidget.widget);
    if (!widget)
        throw ScriptException("Not a table widget");

    if (index < 0 || index >= widget->rowCount())
        return;

    widget->removeRow(index);
}

int ScriptDialog::tableWidgetSelected(const std::string& widgetName)
{
    auto& baseWidget = mp->widgets.at(widgetName);
    auto* widget = dynamic_cast<QTableWidget*>(baseWidget.widget);
    if (!widget)
        throw ScriptException("Not a table widget");

    auto* selection = widget->selectionModel();
    auto idx = selection->currentIndex();
    return idx.isValid() ? idx.row() : -1;
}

ValueArray ScriptDialog::tableWidgetRow(const std::string& widgetName, int index)
{
    auto& baseWidget = mp->widgets.at(widgetName);
    auto* widget = dynamic_cast<QTableWidget*>(baseWidget.widget);
    if (!widget)
        throw ScriptException("Not a table widget");

    ValueArray ret;

    if (index < 0 || index >= widget->rowCount()) {
        ret.emplace(LIST_END_MAGIC, std::make_unique<ValueHolder>(0));
        return ret;
    }

    const int colCount = widget->columnCount();
    int i = 0;
    for (; i < colCount; ++i) {
        auto* item = widget->item(index, i);
        const auto text = item->text().toStdString();
        ret.emplace(std::to_string(i), std::make_unique<ValueHolder>(text));
    }
    ret.emplace(LIST_END_MAGIC, std::make_unique<ValueHolder>(i));
    return ret;
}

const std::string& ScriptDialog::name() const
{
    return mp->tmpl->name();
}

void ScriptDialog::close()
{
    mp->dialog->close();
    mp->dialog->deleteLater();
    mp->dialog.release();
}

void ScriptDialog::hide()
{
    mp->dialog->hide();
}

void ScriptDialog::show()
{
    mp->dialog->show();
}