#include "TypesWidget.h"
#include "common/Configuration.h"
#include "common/Helpers.h"
#include "common/ShortcutManager.h"
#include "common/TempConfig.h"
#include "core/MainWindow.h"
#include "dialogs/LinkTypeDialog.h"
#include "dialogs/TypesInteractionDialog.h"
#include "ui_TypesWidget.h"

#include <QFileDialog>
#include <QIcon>
#include <QMenu>
#include <QScrollBar>
#include <QShortcut>

TypesModel::TypesModel(QList<TypeDescription> *types, QObject *parent)
    : QAbstractListModel(parent)
    , types(types)
{}

int TypesModel::rowCount(const QModelIndex &) const
{
    return types->count();
}

int TypesModel::columnCount(const QModelIndex &) const
{
    return Columns::COUNT;
}

QVariant TypesModel::data(const QModelIndex &index, int role) const
{
    if (index.row() >= types->count())
        return QVariant();

    const TypeDescription &exp = types->at(index.row());

    switch (role) {
    case Qt::DisplayRole:
        switch (index.column()) {
        case TYPE:
            return exp.type;
        case SIZE:
            return exp.size ? exp.size : QVariant();
        case FORMAT:
            return exp.format;
        case CATEGORY:
            return exp.category;
        default:
            return QVariant();
        }
    case TypeDescriptionRole:
        return QVariant::fromValue(exp);
    default:
        return QVariant();
    }
}

QVariant TypesModel::headerData(int section, Qt::Orientation, int role) const
{
    switch (role) {
    case Qt::DisplayRole:
        switch (section) {
        case TYPE:
            return tr("Type / Name");
        case SIZE:
            return tr("Size");
        case FORMAT:
            return tr("Format");
        case CATEGORY:
            return tr("Category");
        default:
            return QVariant();
        }
    default:
        return QVariant();
    }
}

bool TypesModel::removeRows(int row, int count, const QModelIndex &parent)
{
    if (row < 0 || count <= 0 || row + count > types->size()) {
        return false;
    }
    Core()->cmdRaw("t-" + types->at(row).type);
    beginRemoveRows(parent, row, row + count - 1);
    while (count--) {
        types->removeAt(row);
    }
    endRemoveRows();
    return true;
}

TypesSortFilterProxyModel::TypesSortFilterProxyModel(TypesModel *source_model, QObject *parent)
    : QSortFilterProxyModel(parent)
{
    setSourceModel(source_model);
}

bool TypesSortFilterProxyModel::filterAcceptsRow(int row, const QModelIndex &parent) const
{
    QModelIndex index = sourceModel()->index(row, 0, parent);
    TypeDescription exp = index.data(TypesModel::TypeDescriptionRole).value<TypeDescription>();
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
    if (selectedCategory.isEmpty()) {
        return exp.type.contains(this->filterRegularExpression());
    } else {
        return selectedCategory == exp.category
               && exp.type.contains(this->filterRegularExpression());
    }
#else
    if (selectedCategory.isEmpty()) {
        return exp.type.contains(filterRegExp());
    } else {
        return selectedCategory == exp.category && exp.type.contains(filterRegExp());
    }
#endif
}

bool TypesSortFilterProxyModel::lessThan(const QModelIndex &left, const QModelIndex &right) const
{
    TypeDescription left_exp = left.data(TypesModel::TypeDescriptionRole).value<TypeDescription>();
    TypeDescription right_exp = right.data(TypesModel::TypeDescriptionRole).value<TypeDescription>();

    switch (left.column()) {
    case TypesModel::TYPE:
        return left_exp.type < right_exp.type;
    case TypesModel::SIZE:
        return left_exp.size < right_exp.size;
    case TypesModel::FORMAT:
        return left_exp.format < right_exp.format;
    case TypesModel::CATEGORY:
        return left_exp.category < right_exp.category;
    default:
        break;
    }

    return left_exp.size < right_exp.size;
}

TypesWidget::TypesWidget(MainWindow *main)
    : IaitoDockWidget(main)
    , ui(new Ui::TypesWidget)
    , tree(new IaitoTreeWidget(this))
{
    ui->setupUi(this);
    ui->quickFilterView->setLabelText(tr("Category"));

    // Add status bar which displays the count
    tree->addStatusBar(ui->verticalLayout);

    // Set single select mode
    ui->typesTreeView->setSelectionMode(QAbstractItemView::SingleSelection);

    // Setup up the model and the proxy model
    types_model = new TypesModel(&types, this);
    types_proxy_model = new TypesSortFilterProxyModel(types_model, this);
    ui->typesTreeView->setModel(types_proxy_model);
    ui->typesTreeView->sortByColumn(TypesModel::TYPE, Qt::AscendingOrder);

    setScrollMode();

    // Setup custom context menu
    connect(
        ui->typesTreeView,
        &QWidget::customContextMenuRequested,
        this,
        &TypesWidget::showTypesContextMenu);

    connect(
        ui->quickFilterView,
        &ComboQuickFilterView::filterTextChanged,
        types_proxy_model,
        &QSortFilterProxyModel::setFilterWildcard);

    connect(ui->quickFilterView, &ComboQuickFilterView::filterTextChanged, this, [this] {
        tree->showItemsNumber(types_proxy_model->rowCount());
    });

    QShortcut *searchShortcut = ShortcutMgr()->registerShortcut("list.showFilter", this);
    connect(
        searchShortcut,
        &QShortcut::activated,
        ui->quickFilterView,
        &ComboQuickFilterView::showFilter);
    searchShortcut->setContext(Qt::WidgetWithChildrenShortcut);

    QShortcut *clearShortcut = ShortcutMgr()->registerShortcut("list.clearFilter", this);
    connect(clearShortcut, &QShortcut::activated, this, [this]() {
        ui->quickFilterView->clearFilter();
        ui->typesTreeView->clearSelection();
        ui->typesTreeView->setCurrentIndex(QModelIndex());
        updateTypeDetail();
    });
    clearShortcut->setContext(Qt::WidgetWithChildrenShortcut);

    connect(Core(), &IaitoCore::refreshAll, this, &TypesWidget::refreshTypes);

    connect(ui->quickFilterView->comboBox(), &QComboBox::currentTextChanged, this, [this]() {
        types_proxy_model->selectedCategory
            = ui->quickFilterView->comboBox()->currentData().toString();
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
//            types_proxy_model->setFilterRegExp(types_proxy_model->filterRegularExpression());
#else
            types_proxy_model->setFilterRegExp(types_proxy_model->filterRegExp());
#endif
        tree->showItemsNumber(types_proxy_model->rowCount());
    });

    actionViewType = new QAction(tr("View Type"), this);
    actionEditType = new QAction(tr("Edit Type"), this);

    connect(actionViewType, &QAction::triggered, [this]() { viewType(true); });
    connect(actionEditType, &QAction::triggered, [this]() { viewType(false); });
    connect(ui->typesTreeView, &QTreeView::doubleClicked, this, &TypesWidget::typeItemDoubleClicked);

    connect(ui->typesTreeView, &QTreeView::clicked, this, &TypesWidget::onTypeClicked);

    ui->splitter->setSizes(QList<int>() << 300 << 300);

    setupDetailFont();
    setupDetailColors();
    connect(Config(), &Configuration::fontsUpdated, this, &TypesWidget::setupDetailFont);
    connect(Config(), &Configuration::colorsUpdated, this, &TypesWidget::setupDetailColors);
    connect(Config(), &Configuration::colorsUpdated, this, &TypesWidget::updateTypeDetail);
}

TypesWidget::~TypesWidget()
{
    // Explicitly delete models first because they hold a pointer to the
    // member 'types'. Members are destroyed after this destructor body
    // but QObject children are deleted during the QWidget/QObject base
    // destructor which runs later — that would lead to the models being
    // deleted after 'types' is destroyed and cause use-after-free.
    delete types_proxy_model;
    delete types_model;
}

void TypesWidget::refreshTypes()
{
    types_model->beginResetModel();
    types = Core()->getAllTypes();
    types_model->endResetModel();

    QStringList categories;
    for (TypeDescription exp : types) {
        categories << exp.category;
    }
    categories.removeDuplicates();
    refreshCategoryCombo(categories);

    qhelpers::adjustColumns(ui->typesTreeView, 4, 0);

    updateTypeDetail();
}

void TypesWidget::refreshCategoryCombo(const QStringList &categories)
{
    Q_UNUSED(categories);
    QComboBox *combo = ui->quickFilterView->comboBox();

    combo->clear();
    combo->addItem(tr("(All)"));

    QStringList allCategories = {tr("Primitive"), "Struct", "Union", "Enum", "Typedef", "Function"};
    for (const QString &category : allCategories) {
        combo->addItem(category, category);
    }

    types_proxy_model->selectedCategory.clear();
}

void TypesWidget::setScrollMode()
{
    qhelpers::setVerticalScrollMode(ui->typesTreeView);
}

void TypesWidget::showTypesContextMenu(const QPoint &pt)
{
    QModelIndex index = ui->typesTreeView->indexAt(pt);

    QMenu menu(ui->typesTreeView);
    menu.addAction(ui->actionLoad_New_Types);

    if (index.isValid()) {
        TypeDescription t = index.data(TypesModel::TypeDescriptionRole).value<TypeDescription>();
        if (t.category != "Primitive") {
            // Add "Link To Address" option
            menu.addAction(actionViewType);
            menu.addAction(actionEditType);
            if (t.category == "Struct") {
                menu.addAction(ui->actionLink_Type_To_Address);
            }
        }
    }

    menu.addAction(ui->actionExport_Types);

    if (index.isValid()) {
        TypeDescription t = index.data(TypesModel::TypeDescriptionRole).value<TypeDescription>();
        if (t.category != "Primitive") {
            menu.addSeparator();
            menu.addAction(ui->actionDelete_Type);
        }
    }

    menu.exec(ui->typesTreeView->mapToGlobal(pt));
}

void TypesWidget::on_actionExport_Types_triggered()
{
    QString filename
        = QFileDialog::getSaveFileName(this, tr("Save File"), Config()->getRecentFolder());
    if (filename.isEmpty()) {
        return;
    }
    Config()->setRecentFolder(QFileInfo(filename).absolutePath());
    QFile file(filename);
    if (!file.open(QIODevice::WriteOnly)) {
        QMessageBox::critical(this, tr("Error"), file.errorString());
        on_actionExport_Types_triggered();
        return;
    }
    QTextStream fileOut(&file);
    fileOut << Core()->cmdRaw("tc");
    file.close();
}

void TypesWidget::on_actionLoad_New_Types_triggered()
{
    TypesInteractionDialog dialog(this);
    connect(&dialog, &TypesInteractionDialog::newTypesLoaded, this, &TypesWidget::refreshTypes);
    dialog.setWindowTitle(tr("Load New Types"));
    dialog.exec();
}

void TypesWidget::viewType(bool readOnly)
{
    QModelIndex index = ui->typesTreeView->currentIndex();

    if (!index.isValid()) {
        return;
    }

    TypesInteractionDialog dialog(this, readOnly);
    TypeDescription t = index.data(TypesModel::TypeDescriptionRole).value<TypeDescription>();
    if (!readOnly) {
        dialog.setWindowTitle(tr("Edit Type: ") + t.type);
        dialog.setEditingType(t.type);
        connect(&dialog, &TypesInteractionDialog::newTypesLoaded, this, &TypesWidget::refreshTypes);
    } else {
        dialog.setWindowTitle(tr("View Type: ") + t.type + tr(" (Read Only)"));
    }
    dialog.fillTextArea(Core()->getTypeAsC(t.type, t.category));
    dialog.exec();
}

void TypesWidget::on_actionDelete_Type_triggered()
{
    QModelIndex proxyIndex = ui->typesTreeView->currentIndex();
    if (!proxyIndex.isValid()) {
        return;
    }
    QModelIndex index = types_proxy_model->mapToSource(proxyIndex);
    if (!index.isValid()) {
        return;
    }

    TypeDescription exp = index.data(TypesModel::TypeDescriptionRole).value<TypeDescription>();
    QMessageBox::StandardButton reply = QMessageBox::question(
        this, tr("iaito"), tr("Are you sure you want to delete \"%1\"?").arg(exp.type));
    if (reply == QMessageBox::Yes) {
        types_model->removeRow(index.row());
    }
}

void TypesWidget::on_actionLink_Type_To_Address_triggered()
{
    LinkTypeDialog dialog(this);

    QModelIndex index = ui->typesTreeView->currentIndex();
    if (index.isValid()) {
        TypeDescription t = index.data(TypesModel::TypeDescriptionRole).value<TypeDescription>();
        dialog.setDefaultType(t.type);
        dialog.setDefaultAddress(RAddressString(Core()->getOffset()));
        dialog.exec();
    }
}

void TypesWidget::typeItemDoubleClicked(const QModelIndex &index)
{
    if (!index.isValid()) {
        return;
    }

    TypeDescription t = index.data(TypesModel::TypeDescriptionRole).value<TypeDescription>();
    if (t.category == "Primitive") {
        return;
    }
    TypesInteractionDialog dialog(this, false);
    dialog.setWindowTitle(tr("Edit Type: ") + t.type);
    dialog.setEditingType(t.type);
    connect(&dialog, &TypesInteractionDialog::newTypesLoaded, this, &TypesWidget::refreshTypes);
    dialog.fillTextArea(Core()->getTypeAsC(t.type, t.category));
    dialog.exec();
}

void TypesWidget::setupDetailFont()
{
    ui->typeDetailTextEdit->setFont(Config()->getFont());
}

void TypesWidget::setupDetailColors()
{
    ui->typeDetailTextEdit->setStyleSheet(
        QStringLiteral("QTextEdit { background-color: %1; color: %2; }")
            .arg(ConfigColor("gui.background").name())
            .arg(ConfigColor("btext").name()));
}

void TypesWidget::onTypeClicked(const QModelIndex &)
{
    updateTypeDetail();
}

void TypesWidget::updateTypeDetail()
{
    if (!Core()) {
        return;
    }

    // Defer the expensive setHtml() until the dock is actually visible.
    // refreshTypes() fires from finalizeOpen() and colorsUpdated on every
    // widget — the "tv" blob parsed into QTextDocument costs ~20MB of peak
    // heap and is wasted on a hidden dock.
    if (!isVisible()) {
        detailDirty = true;
        return;
    }
    detailDirty = false;

    TempConfig tempConfig;
    tempConfig.set("scr.html", true);
    tempConfig.set("scr.color", COLOR_MODE_16M);

    QString typeDetail;
    QModelIndex index = ui->typesTreeView->currentIndex();
    if (!index.isValid()) {
        typeDetail = Core()->cmd("tv");
    } else {
        TypeDescription t = index.data(TypesModel::TypeDescriptionRole).value<TypeDescription>();
        if (t.category == "Primitive") {
            typeDetail = Core()->cmd("tv");
        } else {
            typeDetail = Core()->cmd("'tv " + t.type);
        }
    }
    ui->typeDetailTextEdit->document()->setHtml(typeDetail);
    ui->typeDetailTextEdit->verticalScrollBar()->setValue(0);
    ui->typeDetailTextEdit->horizontalScrollBar()->setValue(0);
}

void TypesWidget::showEvent(QShowEvent *event)
{
    IaitoDockWidget::showEvent(event);
    if (detailDirty) {
        updateTypeDetail();
    }
}
