#include "MainWindow.h"
#include "ProjectManager.h"

#include <QApplication>
#include <QMenuBar>
#include <QMenu>
#include <QAction>
#include <QActionGroup>
#include <QStatusBar>
#include <QDockWidget>
#include <QToolBar>
#include <QMessageBox>
#include <QFileDialog>
#include <QInputDialog>
#include <QDialog>
#include <QDialogButtonBox>
#include <QFormLayout>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QSplitter>
#include <QMdiArea>
#include <QMdiSubWindow>
#include <QTabWidget>
#include <QTreeWidget>
#include <QTreeWidgetItem>
#include <QTableWidget>
#include <QTableWidgetItem>
#include <QHeaderView>
#include <QAbstractItemView>
#include <QStyledItemDelegate>
#include <QLabel>
#include <QLineEdit>
#include <QComboBox>
#include <QSpinBox>
#include <QPushButton>
#include <QPlainTextEdit>
#include <QFont>
#include <QFile>
#include <QDir>
#include <QFileInfo>
#include <QProcess>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QDataStream>
#include <QMenu>
#include <QTimer>
#include <QDomDocument>
#include <QCoreApplication>
#include <QUndoStack>
#include <QFrame>
#include <QEvent>
#include <QGroupBox>
#include <QApplication>
#include <QColor>
#include <QDrag>
#include <QMetaType>
#include <QMimeData>
#include <QSettings>

#include <utility>

#include "../editor/scene/LadderScene.h"
#include "../editor/scene/LadderView.h"
#include "../editor/scene/PlcOpenViewer.h"
#include "../utils/StHighlighter.h"
#include "../utils/TreeBranchStyle.h"
#include "../core/compiler/CodeGenerator.h"
#include "../core/compiler/StGenerator.h"
#include "../sim/smartsim.h"
#include "BlockPropertiesDialog.h"
#include "../comm/DownloadDialog.h"

// PlcOpenViewer 兼作所有图形语言（LD/FBD/SFC）的统一编辑器

// ============================================================
namespace {
constexpr int kMaxRecentProjects = 10;
constexpr const char* kRecentProjectsKey = "recentProjects";

class ComboBoxDelegate final : public QStyledItemDelegate {
public:
    explicit ComboBoxDelegate(QStringList values, QObject* parent = nullptr)
        : QStyledItemDelegate(parent)
        , m_values(std::move(values))
    {}

    QWidget* createEditor(QWidget* parent,
                          const QStyleOptionViewItem&,
                          const QModelIndex&) const override
    {
        auto* combo = new QComboBox(parent);
        combo->addItems(m_values);
        combo->setSizeAdjustPolicy(QComboBox::AdjustToMinimumContentsLengthWithIcon);
        combo->setMinimumContentsLength(1);
        return combo;
    }

    void setEditorData(QWidget* editor, const QModelIndex& index) const override
    {
        auto* combo = qobject_cast<QComboBox*>(editor);
        if (!combo)
            return;

        const QString current = index.data(Qt::EditRole).toString();
        if (!current.isEmpty() && combo->findText(current) < 0)
            combo->addItem(current);
        combo->setCurrentText(current);
    }

    void setModelData(QWidget* editor,
                      QAbstractItemModel* model,
                      const QModelIndex& index) const override
    {
        auto* combo = qobject_cast<QComboBox*>(editor);
        if (!combo)
            return;

        model->setData(index, combo->currentText(), Qt::EditRole);
    }

private:
    QStringList m_values;
};
}

// ============================================================
// ============================================================

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
{
    menuBar()->setNativeMenuBar(false);
    m_simDiVars = QStringList(16);
    m_simDoVars = QStringList(16);
    m_simAiVars = QStringList(4);
    m_simAoVars = QStringList(4);

    // 初始化 UI 框架
    setupMenuBar();
    setupToolBar();
    setupProjectPanel();
    setupLibraryPanel();
    setupConsolePanel();
    setupCentralArea();
    setupStatusBar();

    // 加载样式表
    applyTheme(":/light_theme.qss");   // 默认浅色主题

    // ── ProjectManager：负责项目的新建 / 打开 / 保存 ──────────────
    m_projectManager = new ProjectManager(this, this);
    m_projectManager->setSceneMap(&m_sceneMap);

    // ProjectManager 创建/替换项目时，MainWindow 负责 UI 切换
    connect(m_projectManager, &ProjectManager::projectCreated,
            this, [this](ProjectModel* newProj) {
        closeAllPouTabs();
        m_sceneMap.clear();
        if (m_projPropSubWin) m_projPropSubWin->close();

        delete m_project;
        m_project = newProj;
        m_project->setParent(this);
        connect(m_project, &ProjectModel::changed,
                this, &MainWindow::updateWindowTitle);
        rebuildProjectTree();
    });
    connect(m_projectManager, &ProjectManager::firstPouReady,
            this, &MainWindow::openPouTab);
    connect(m_projectManager, &ProjectManager::titleUpdateNeeded,
            this, &MainWindow::updateWindowTitle);
    connect(m_projectManager, &ProjectManager::statusMessage,
            this, [this](const QString& msg, int ms){
        statusBar()->showMessage(msg, ms);
    });

    // 创建启动示例项目（触发上面的信号链）
    m_projectManager->buildDefaultProject();

    resize(1400, 900);
    updateWindowTitle();
}

MainWindow::~MainWindow()
{
    // ── 析构顺序问题修复 ────────────────────────────────────────
    // C++ 析构顺序：① 析构体执行 → ② 成员变量析构 → ③ 基类析构
    // Qt 在 ③（~QObject）中才删除子对象（deleteChildren）。
    //
    // 问题：m_subWinPouMap（值类型 QMap，成员变量）在 ② 被销毁，
    // 但 ③ 删除 MDI 子窗口时触发 destroyed 信号，lambda 仍试图
    // 访问 m_subWinPouMap.remove(sw) → use-after-destruction → SIGSEGV。
    //
    // 修复：在 ① 中提前断开所有子窗口的 destroyed 信号，让 ② 之后
    // 的子窗口删除不再触发 lambda。
    if (m_mdiArea) {
        for (QMdiSubWindow* sw : m_mdiArea->subWindowList())
            sw->disconnect(this);
    }
}

// ============================================================

// ============================================================
// 菜单栏
// ============================================================
void MainWindow::setupMenuBar()
{
    QMenu* fileMenu = menuBar()->addMenu("File(&F)");
    fileMenu->addAction("New Project",    this, &MainWindow::newProject);
    fileMenu->addAction("Open Project...", this, &MainWindow::openProject);
    m_recentProjectsMenu = fileMenu->addMenu("Open Recent");
    updateRecentProjectsMenu();
    fileMenu->addAction("Save",           this, &MainWindow::saveProject);
    fileMenu->addAction("Save As...",     this, &MainWindow::saveProjectAs);
    fileMenu->addSeparator();
    fileMenu->addAction(QIcon(":/images/Properties.png"),
                        "Project Settings...", this, &MainWindow::openProjectProperties);
    fileMenu->addSeparator();
    fileMenu->addAction("Exit", this, &QWidget::close);

    QMenu* editMenu = menuBar()->addMenu("Edit(&E)");
    editMenu->addAction(makeLdIcon("undo"),  "Undo",  this, []{
        if (auto* w = qobject_cast<QPlainTextEdit*>(QApplication::focusWidget())) w->undo();
    }, QKeySequence::Undo);
    editMenu->addAction(makeLdIcon("redo"),  "Redo",  this, []{
        if (auto* w = qobject_cast<QPlainTextEdit*>(QApplication::focusWidget())) w->redo();
    }, QKeySequence::Redo);
    editMenu->addSeparator();
    editMenu->addAction(makeLdIcon("cut"),   "Cut",   this, []{
        if (auto* w = qobject_cast<QPlainTextEdit*>(QApplication::focusWidget())) w->cut();
    }, QKeySequence::Cut);
    editMenu->addAction(makeLdIcon("copy"),  "Copy",  this, []{
        if (auto* w = qobject_cast<QPlainTextEdit*>(QApplication::focusWidget())) w->copy();
    }, QKeySequence::Copy);
    editMenu->addAction(makeLdIcon("paste"), "Paste", this, []{
        if (auto* w = qobject_cast<QPlainTextEdit*>(QApplication::focusWidget())) w->paste();
    }, QKeySequence::Paste);

    // ── PLC 菜单 ─────────────────────────────────────────────
    QMenu* plcMenu = menuBar()->addMenu("PLC(&P)");

    // 编译组
    auto* aBuildActive = plcMenu->addAction(
        QIcon(":/images/Build.png"), "Build Active Resource\tCtrl+B");
    aBuildActive->setShortcut(QKeySequence("Ctrl+B"));
    connect(aBuildActive, &QAction::triggered, this, &MainWindow::buildProject);

    auto* aRebuild = plcMenu->addAction(
        QIcon(":/images/Clean.png"), "Rebuild Active Resource");
    connect(aRebuild, &QAction::triggered, this, [this]{
        m_consoleEdit->clear();
        buildProject();
    });

    plcMenu->addSeparator();

    // 连接组
    auto* aConn = plcMenu->addAction(
        QIcon(":/images/Connect.png"), "Connections...");
    connect(aConn, &QAction::triggered, this, &MainWindow::connectToPlc);

    auto* aOnline = plcMenu->addAction(
        QIcon(":/images/Connect.png"), "Online");
    aOnline->setCheckable(true);
    connect(aOnline, &QAction::triggered, this, &MainWindow::connectToPlc);

    plcMenu->addSeparator();

    // 传输/控制组
    auto* aDownload = plcMenu->addAction(
        QIcon(":/images/Transfer.png"), "Download...");
    connect(aDownload, &QAction::triggered, this, &MainWindow::downloadProject);

    auto* aColdStart = plcMenu->addAction("Cold Start");
    connect(aColdStart, &QAction::triggered, this, [this]{
        if (m_connState != PlcConnState::Connected) {
            QMessageBox::warning(this, "Not Connected", "Please connect to a PLC first.");
            return;
        }
        setPlcRunState(PlcRunState::Stopped);
        statusBar()->showMessage("Cold start requested.", 3000);
    });

    auto* aHotStart = plcMenu->addAction("Hot Start");
    connect(aHotStart, &QAction::triggered, this, [this]{
        if (m_connState != PlcConnState::Connected) {
            QMessageBox::warning(this, "Not Connected", "Please connect to a PLC first.");
            return;
        }
        setPlcRunState(PlcRunState::Running);
        statusBar()->showMessage("Hot start requested.", 3000);
    });

    auto* aPlcStop = plcMenu->addAction(
        QIcon(":/images/Stop.png"), "Stop");
    connect(aPlcStop, &QAction::triggered, this, [this]{
        if (m_connState != PlcConnState::Connected) {
            QMessageBox::warning(this, "Not Connected", "Please connect to a PLC first.");
            return;
        }
        setPlcRunState(PlcRunState::Stopped);
        statusBar()->showMessage("PLC stopped.", 3000);
    });

    plcMenu->addSeparator();

    auto* aSmartSim = plcMenu->addAction(
        QIcon(":/images/Connect.png"), "SmartSim...");
    connect(aSmartSim, &QAction::triggered, this, &MainWindow::openSmartSim);

    plcMenu->addSeparator();

    // 调试组
    auto* aMonitor = plcMenu->addAction(
        QIcon(":/images/Debug.png"), "Monitor / Edit");
    aMonitor->setCheckable(true);
    connect(aMonitor, &QAction::triggered, this, [this](bool checked){
        if (m_connState != PlcConnState::Connected) {
            QMessageBox::warning(this, "Not Connected", "Please connect to a PLC first.");
            return;
        }
        statusBar()->showMessage(
            checked ? "Monitor mode enabled." : "Monitor mode disabled.", 3000);
    });

    auto* aBrowser = plcMenu->addAction(
        QIcon(":/images/IO_VARIABLE.png"), "Browser");
    connect(aBrowser, &QAction::triggered, this, [this]{
        QDialog dlg(this);
        dlg.setWindowTitle("Variable Browser");
        dlg.resize(760, 420);

        auto* layout = new QVBoxLayout(&dlg);
        auto* table = new QTableWidget(&dlg);
        table->setColumnCount(7);
        table->setHorizontalHeaderLabels({
            "POU", "POU Type", "Language", "Class", "Name", "Type", "Initial Value"
        });
        table->setEditTriggers(QAbstractItemView::NoEditTriggers);
        table->setSelectionBehavior(QAbstractItemView::SelectRows);
        table->setAlternatingRowColors(true);

        int row = 0;
        if (m_project) {
            for (const PouModel* pou : m_project->pous)
                row += pou->variables.size();
        }
        table->setRowCount(row);

        row = 0;
        if (m_project) {
            for (const PouModel* pou : m_project->pous) {
                for (const VariableDecl& v : pou->variables) {
                    const QStringList values = {
                        pou->name,
                        PouModel::typeToString(pou->pouType),
                        PouModel::langToString(pou->language),
                        v.varClass,
                        v.name,
                        v.type,
                        v.initValue
                    };
                    for (int col = 0; col < values.size(); ++col)
                        table->setItem(row, col, new QTableWidgetItem(values[col]));
                    ++row;
                }
            }
        }

        table->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
        table->horizontalHeader()->setStretchLastSection(true);
        layout->addWidget(table);

        auto* buttons = new QDialogButtonBox(QDialogButtonBox::Close, &dlg);
        connect(buttons, &QDialogButtonBox::rejected, &dlg, &QDialog::reject);
        layout->addWidget(buttons);
        dlg.exec();
    });

    plcMenu->addSeparator();

    auto* aPlcInfo = plcMenu->addAction(
        QIcon(":/images/LOG_INFO.png"), "PLC Info...");
    connect(aPlcInfo, &QAction::triggered, this, [this]{
        if (m_connState != PlcConnState::Connected) {
            QMessageBox::information(this, "PLC Info",
                "Not connected to any PLC.\n"
                "Use Connections... to establish a connection first.");
            return;
        }
        QMessageBox::information(this, "PLC Info",
            QString("URI: %1\nStatus: Connected\nRuntime: OpenPLC Runtime")
                .arg(m_plcUri));
    });

    // ── Extras 菜单 ──────────────────────────────────────────
    QMenu* extrasMenu = menuBar()->addMenu("Extras(&X)");

    // Tools 子菜单
    QMenu* toolsMenu = extrasMenu->addMenu("Tools");
    auto* aDriverInstall = toolsMenu->addAction("Driver Install...");
    connect(aDriverInstall, &QAction::triggered, this, [this]{
        const QString path = QFileDialog::getOpenFileName(
            this, "Select Driver Package", QString(),
            "TiZi Driver Packages (*.cab);;All Files (*)");
        if (!path.isEmpty())
            installDriverCab(path);
    });

    auto* aLicenseEditor = extrasMenu->addAction("License Editor");
    connect(aLicenseEditor, &QAction::triggered, this, [this]{
        QDialog dlg(this);
        dlg.setWindowTitle("License Information");
        dlg.resize(640, 420);

        auto* layout = new QVBoxLayout(&dlg);
        auto* text = new QPlainTextEdit(&dlg);
        text->setReadOnly(true);

        QString info;
        const QString licensePath = QDir::cleanPath(QCoreApplication::applicationDirPath()
                                                    + "/../LICENSE");
        const QString sourceLicensePath = QDir::cleanPath(QDir::currentPath() + "/LICENSE");
        QString resolvedLicensePath;
        if (QFileInfo::exists(licensePath))
            resolvedLicensePath = licensePath;
        else if (QFileInfo::exists(sourceLicensePath))
            resolvedLicensePath = sourceLicensePath;

        info += "Project: TiZi / OpenPLC Editor\n";
        if (m_project) {
            info += "PLC Project: " + m_project->projectName + "\n";
            info += "Author: " + m_project->author + "\n";
            info += "Company: " + m_project->companyName + "\n";
            info += "Product Version: " + m_project->productVersion + "\n";
        }
        info += "\n";
        if (resolvedLicensePath.isEmpty()) {
            info += "Repository license file was not found next to the application or in the current working directory.\n";
        } else {
            info += "License File: " + resolvedLicensePath + "\n\n";
            QFile f(resolvedLicensePath);
            if (f.open(QFile::ReadOnly | QFile::Text))
                info += QString::fromUtf8(f.readAll());
            else
                info += "Unable to read license file.\n";
        }

        text->setPlainText(info);
        layout->addWidget(text);

        auto* buttons = new QDialogButtonBox(QDialogButtonBox::Close, &dlg);
        connect(buttons, &QDialogButtonBox::rejected, &dlg, &QDialog::reject);
        layout->addWidget(buttons);
        dlg.exec();
    });

    extrasMenu->addSeparator();

    auto* aOptions = extrasMenu->addAction(
        QIcon(":/images/CONFIGURATION.png"), "Options...");
    connect(aOptions, &QAction::triggered, this, [this]{
        QDialog dlg(this);
        dlg.setWindowTitle("Options");
        dlg.setMinimumWidth(400);

        QFormLayout form(&dlg);
        form.setContentsMargins(12, 12, 12, 8);
        form.setSpacing(8);

        // 编译器路径
        auto* compEdit = new QLineEdit(
            m_project ? m_project->compiler : "gcc");
        form.addRow("Compiler:", compEdit);

        auto* linkerEdit = new QLineEdit(
            m_project ? m_project->linker : "gcc");
        form.addRow("Linker:", linkerEdit);

        // 编辑器字体
        auto* fontSizeSpin = new QSpinBox();
        fontSizeSpin->setRange(7, 24);
        fontSizeSpin->setValue(
            m_consoleEdit ? m_consoleEdit->font().pointSize() : 10);
        form.addRow("Editor Font Size:", fontSizeSpin);

        auto* btns = new QDialogButtonBox(
            QDialogButtonBox::Ok | QDialogButtonBox::Cancel,
            Qt::Horizontal, &dlg);
        form.addRow(btns);
        connect(btns, &QDialogButtonBox::accepted, &dlg, &QDialog::accept);
        connect(btns, &QDialogButtonBox::rejected, &dlg, &QDialog::reject);

        if (dlg.exec() == QDialog::Accepted && m_project) {
            m_project->compiler = compEdit->text().trimmed();
            m_project->linker   = linkerEdit->text().trimmed();
            m_project->markDirty();
            // 应用字体到所有代码编辑器
            const int sz = fontSizeSpin->value();
            QFont f("Courier New", sz);
            if (m_consoleEdit) m_consoleEdit->setFont(f);
            statusBar()->showMessage("Options saved.", 3000);
        }
    });

    // ── Display 菜单 ─────────────────────────────────────────
    QMenu* displayMenu = menuBar()->addMenu("Display(&D)");
    displayMenu->addAction(makeLdIcon("zoom_in"),  "Zoom In",
                           this, &MainWindow::zoomIn,  QKeySequence(Qt::CTRL | Qt::Key_Equal));
    displayMenu->addAction(makeLdIcon("zoom_out"), "Zoom Out",
                           this, &MainWindow::zoomOut, QKeySequence(Qt::CTRL | Qt::Key_Minus));
    displayMenu->addAction(makeLdIcon("fit"),      "Fit to Window",
                           this, &MainWindow::zoomFit, QKeySequence(Qt::CTRL | Qt::Key_0));

    displayMenu->addSeparator();

    // ── 主题切换 ──────────────────────────────────────────────
    QMenu* themeMenu = displayMenu->addMenu("Theme");

    auto* aLightTheme = themeMenu->addAction("Light");
    aLightTheme->setCheckable(true);
    aLightTheme->setChecked(true);

    auto* aDarkTheme = themeMenu->addAction("Dark");
    aDarkTheme->setCheckable(true);
    aDarkTheme->setChecked(false);

    // 互斥：切到浅色
    connect(aLightTheme, &QAction::triggered, this, [this, aLightTheme, aDarkTheme]() {
        applyTheme(":/light_theme.qss");
        aLightTheme->setChecked(true);
        aDarkTheme->setChecked(false);
    });

    // 互斥：切到深色
    connect(aDarkTheme, &QAction::triggered, this, [this, aLightTheme, aDarkTheme]() {
        applyTheme(":/dark_theme.qss");
        aDarkTheme->setChecked(true);
        aLightTheme->setChecked(false);
    });

    QMenu* helpMenu = menuBar()->addMenu("Help(&H)");
    helpMenu->addAction("About", this, [this](){
        QMessageBox::about(this, "TiZi PLC Editor",
            "TiZi PLC Editor v0.1.0\n\nAn OpenPLC IDE inspired by Beremiz");
    });
}

// ============================================================
// 工具栏图标：优先加载 QRC 图片，无对应图片则用 QPainter 绘制
// ============================================================
QIcon MainWindow::makeLdIcon(const QString& type, int sz)
{
    // 直接从资源文件加载的图标（Beremiz 原版图标）
    static const QMap<QString, QString> s_pngMap = {
        // 文件操作
        { "new",        ":/images/new.png"         },
        { "open",       ":/images/open.png"        },
        { "save",       ":/images/save.png"        },
        { "saveas",     ":/images/saveas.png"      },
        // 编辑
        { "undo",       ":/images/undo.png"        },
        { "redo",       ":/images/redo.png"        },
        { "cut",        ":/images/cut.png"         },
        { "copy",       ":/images/copy.png"        },
        { "paste",      ":/images/paste.png"       },
        // 编译
        { "build",      ":/images/Build.png"       },
        { "clean",      ":/images/Clean.png"       },
        // PLC 连接/控制
        { "connect",    ":/images/Connect.png"     },
        { "disconnect", ":/images/Disconnect.png"  },
        { "download",   ":/images/Transfer.png"    },
        { "run",        ":/images/Run.png"         },
        { "stop",       ":/images/Stop.png"        },
        // LD 编辑元件
        { "select",     ":/images/select.png"      },
        { "no",         ":/images/add_contact.png" },
        { "coil",       ":/images/add_coil.png"    },
        { "fb",         ":/images/add_block.png"   },
        { "wire",       ":/images/add_wire.png"    },
        // 缩放
        { "zoom_in",    ":/images/zoom_in.png"     },
        { "zoom_out",   ":/images/zoom_out.png"    },
        { "fit",        ":/images/zoom_fit.png"    },
    };
    if (s_pngMap.contains(type))
        return QIcon(s_pngMap[type]);

    // QPainter 绘制：NC / P / N 触点，Set / Reset 线圈
    QPixmap pm(sz, sz);
    pm.fill(Qt::transparent);
    QPainter p(&pm);
    p.setRenderHint(QPainter::Antialiasing);

    const int cy = sz / 2;
    const int bl = sz / 4;
    const int br = sz * 3 / 4;
    const int bt = cy - sz / 4;
    const int bb = cy + sz / 4;

    QPen pen(QColor("#2A2A2A"), 1.5f);
    p.setPen(pen);

    if (type == "nc") {
        p.drawLine(2, cy, bl, cy);
        p.drawLine(br, cy, sz-2, cy);
        p.drawLine(bl, bt, bl, bb);
        p.drawLine(br, bt, br, bb);
        p.drawLine(bl+1, bb-1, br-1, bt+1);

    } else if (type == "pe" || type == "ne") {
        p.drawLine(2, cy, bl, cy);
        p.drawLine(br, cy, sz-2, cy);
        p.drawLine(bl, bt, bl, bb);
        p.drawLine(br, bt, br, bb);
        QFont f; f.setPixelSize(sz/3); f.setBold(true); p.setFont(f);
        p.drawText(QRectF(bl+1, bt, br-bl-2, bb-bt),
                   Qt::AlignCenter, type == "pe" ? "P" : "N");

    } else if (type == "set" || type == "rst") {
        const int al = bl - 2, ar = br + 2;
        p.drawLine(2, cy, al, cy);
        p.drawLine(ar, cy, sz-2, cy);
        p.drawArc(al, bt, 8, bb-bt+1,  90*16,  180*16);
        p.drawArc(ar-8, bt, 8, bb-bt+1, 90*16, -180*16);
        QFont f; f.setPixelSize(sz/3); f.setBold(true); p.setFont(f);
        p.drawText(QRectF(al+4, bt, ar-al-8, bb-bt),
                   Qt::AlignCenter, type == "set" ? "S" : "R");

    } else if (type == "zoom_in" || type == "zoom_out") {
        // 放大镜圆圈
        const int r = sz * 5 / 14;
        const int ox = sz * 4 / 10, oy = sz * 4 / 10;
        p.setPen(QPen(QColor("#2A2A2A"), 1.8));
        p.drawEllipse(QPoint(ox, oy), r, r);
        // 手柄
        int hx1 = ox + r * 7/10, hy1 = oy + r * 7/10;
        p.drawLine(hx1, hy1, sz-3, sz-3);
        // + 或 -
        int cross = r * 5 / 8;
        p.drawLine(ox - cross, oy, ox + cross, oy);
        if (type == "zoom_in")
            p.drawLine(ox, oy - cross, ox, oy + cross);

    } else if (type == "fit") {
        // 四角箭头：表示 Fit to Window
        p.setPen(QPen(QColor("#2A2A2A"), 1.5));
        int m = 3, a = 5;
        // 左上角
        p.drawLine(m, m+a, m, m); p.drawLine(m, m, m+a, m);
        // 右上角
        p.drawLine(sz-m, m+a, sz-m, m); p.drawLine(sz-m, m, sz-m-a, m);
        // 左下角
        p.drawLine(m, sz-m-a, m, sz-m); p.drawLine(m, sz-m, m+a, sz-m);
        // 右下角
        p.drawLine(sz-m, sz-m-a, sz-m, sz-m); p.drawLine(sz-m, sz-m, sz-m-a, sz-m);
    }

    return QIcon(pm);
}

// ============================================================
// 工具栏
// ============================================================
void MainWindow::setupToolBar()
{
    QToolBar* tb = addToolBar("Main");
    tb->setObjectName("mainToolBar");
    tb->setMovable(false);
    tb->setIconSize(QSize(24, 24));
    tb->setToolButtonStyle(Qt::ToolButtonIconOnly);

    // ══════════════════════════════════════════════════════════
    // 第1组：文件操作
    // ══════════════════════════════════════════════════════════
    auto* aNew    = tb->addAction(makeLdIcon("new"),    "New Project  [Ctrl+N]");
    auto* aOpen   = tb->addAction(makeLdIcon("open"),   "Open Project  [Ctrl+O]");
    auto* aSave   = tb->addAction(makeLdIcon("save"),   "Save  [Ctrl+S]");
    auto* aSaveAs = tb->addAction(makeLdIcon("saveas"), "Save As…");
    connect(aNew,    &QAction::triggered, this, &MainWindow::newProject);
    connect(aOpen,   &QAction::triggered, this, &MainWindow::openProject);
    connect(aSave,   &QAction::triggered, this, &MainWindow::saveProject);
    connect(aSaveAs, &QAction::triggered, this, &MainWindow::saveProjectAs);
    aNew->setShortcut(QKeySequence::New);
    aOpen->setShortcut(QKeySequence::Open);
    aSave->setShortcut(QKeySequence::Save);
    tb->addSeparator();

    // ══════════════════════════════════════════════════════════
    // 第2组：编辑（Undo / Redo）
    // ══════════════════════════════════════════════════════════
    m_aUndo = tb->addAction(makeLdIcon("undo"), "Undo  [Ctrl+Z]");
    m_aRedo = tb->addAction(makeLdIcon("redo"), "Redo  [Ctrl+Y]");
    m_aUndo->setShortcut(QKeySequence::Undo);
    m_aRedo->setShortcut(QKeySequence::Redo);
    m_aUndo->setEnabled(false);
    m_aRedo->setEnabled(false);
    // 优先委托图形场景 undo stack，无场景时转发给聚焦的文本控件
    connect(m_aUndo, &QAction::triggered, this, [this]{
        if (m_scene && m_scene->undoStack()->canUndo())
            m_scene->undoStack()->undo();
        else if (auto* w = qobject_cast<QPlainTextEdit*>(QApplication::focusWidget()))
            w->undo();
    });
    connect(m_aRedo, &QAction::triggered, this, [this]{
        if (m_scene && m_scene->undoStack()->canRedo())
            m_scene->undoStack()->redo();
        else if (auto* w = qobject_cast<QPlainTextEdit*>(QApplication::focusWidget()))
            w->redo();
    });
    tb->addSeparator();

    // ══════════════════════════════════════════════════════════
    // 第3组：剪贴板（Cut / Copy / Paste）
    // ══════════════════════════════════════════════════════════
    auto* aCut   = tb->addAction(makeLdIcon("cut"),   "Cut  [Ctrl+X]");
    auto* aCopy  = tb->addAction(makeLdIcon("copy"),  "Copy  [Ctrl+C]");
    auto* aPaste = tb->addAction(makeLdIcon("paste"), "Paste  [Ctrl+V]");
    aCut->setShortcut(QKeySequence::Cut);
    aCopy->setShortcut(QKeySequence::Copy);
    aPaste->setShortcut(QKeySequence::Paste);
    connect(aCut, &QAction::triggered, this, []{
        if (auto* w = qobject_cast<QPlainTextEdit*>(QApplication::focusWidget()))
            w->cut();
    });
    connect(aCopy, &QAction::triggered, this, []{
        if (auto* w = qobject_cast<QPlainTextEdit*>(QApplication::focusWidget()))
            w->copy();
    });
    connect(aPaste, &QAction::triggered, this, []{
        if (auto* w = qobject_cast<QPlainTextEdit*>(QApplication::focusWidget()))
            w->paste();
    });
    tb->addSeparator();

    // ══════════════════════════════════════════════════════════
    // 第4组：编译（Build / Clean）
    // ══════════════════════════════════════════════════════════
    auto* aBuild = tb->addAction(makeLdIcon("build"), "Build / Compile  [Ctrl+B]");
    auto* aClean = tb->addAction(makeLdIcon("clean"), "Clean Build");
    connect(aBuild, &QAction::triggered, this, &MainWindow::buildProject);
    connect(aClean, &QAction::triggered, this, [this]{
        m_consoleEdit->clear();
        m_consoleEdit->appendPlainText("[ Clean ] Build output cleared.");
        m_consoleTabs->setCurrentWidget(m_consoleEdit);
        statusBar()->showMessage("Cleaned.", 2000);
    });
    aBuild->setShortcut(QKeySequence("Ctrl+B"));
    tb->addSeparator();

    // ══════════════════════════════════════════════════════════
    // 第5组：PLC 连接与控制（Connect · Transfer · Run · Stop）
    // ══════════════════════════════════════════════════════════
    m_aConnect  = tb->addAction(makeLdIcon("connect"),  "Connect to PLC  [Ctrl+D]");
    m_aTransfer = tb->addAction(makeLdIcon("download"), "Download Program to PLC");
    m_aRun  = tb->addAction(makeLdIcon("run"),  "Run PLC");
    m_aStop = tb->addAction(makeLdIcon("stop"), "Stop PLC");

    connect(m_aConnect,  &QAction::triggered, this, &MainWindow::connectToPlc);
    connect(m_aTransfer, &QAction::triggered, this, &MainWindow::downloadProject);
    connect(m_aRun, &QAction::triggered, this, [this]{
        if (m_connState != PlcConnState::Connected) {
            statusBar()->showMessage("Not connected to PLC.", 3000); return;
        }
        if (m_plcUri == "SIM://SmartSim") {
            if (!m_smartSimWindow) {
                statusBar()->showMessage("SmartSim is not open.", 3000);
                return;
            }
            if (!m_smartSimWindow->runProgram()) {
                statusBar()->showMessage("SmartSim run failed. Download a program first.", 5000);
                return;
            }
            statusBar()->showMessage("SmartSim running.", 2000);
            return;
        }
        setPlcRunState(PlcRunState::Running);
        statusBar()->showMessage("PLC running.", 2000);
    });
    connect(m_aStop, &QAction::triggered, this, [this]{
        if (m_connState != PlcConnState::Connected) {
            statusBar()->showMessage("Not connected to PLC.", 3000); return;
        }
        if (m_plcUri == "SIM://SmartSim") {
            if (!m_smartSimWindow) {
                statusBar()->showMessage("SmartSim is not open.", 3000);
                return;
            }
            m_smartSimWindow->stopProgram();
            statusBar()->showMessage("SmartSim stopped.", 2000);
            return;
        }
        setPlcRunState(PlcRunState::Stopped);
        statusBar()->showMessage("PLC stopped.", 2000);
    });

    m_aConnect->setShortcut(QKeySequence("Ctrl+D"));
    // 初始状态：Run/Stop/Transfer 需要已连接
    m_aRun->setEnabled(false);
    m_aStop->setEnabled(false);
    m_aTransfer->setEnabled(false);
    tb->addSeparator();

    // ══════════════════════════════════════════════════════════
    // 第6组：LD / FBD 编辑元件（互斥模式按钮）
    // 注意：这组工具只在激活图形视图（LD/FBD/SFC）时可见
    // ══════════════════════════════════════════════════════════

    // 前置分隔符也归入 m_ldToolActions，一起随视图类型显示/隐藏
    m_ldToolActions << tb->addSeparator();

    QActionGroup* modeGroup = new QActionGroup(this);
    modeGroup->setExclusive(true);

    struct ModeEntry {
        QString    iconType;
        QString    tooltip;
        EditorMode mode;
        bool       checked;
    };
    const QList<ModeEntry> ldTools = {
        { "select", "Select / Move  [Esc]",            Mode_Select,        true  },
        { "no",     "Normal Open Contact  -| |-",       Mode_AddContact_NO, false },
        { "nc",     "Normal Closed Contact  -|/|-",     Mode_AddContact_NC, false },
        { "pe",     "Rising Edge Contact  -|P|-",       Mode_AddContact_P,  false },
        { "ne",     "Falling Edge Contact  -|N|-",      Mode_AddContact_N,  false },
        { "coil",   "Output Coil  -( )-",               Mode_AddCoil,       false },
        { "set",    "Set Coil  -(S)-",                  Mode_AddCoil_S,     false },
        { "rst",    "Reset Coil  -(R)-",                Mode_AddCoil_R,     false },
        { "fb",     "Function Block",                   Mode_AddFuncBlock,  false },
        { "wire",   "Wire Connection",                  Mode_AddWire,       false },
    };

    for (const auto& e : ldTools) {
        // 各子组之间插入分隔符（分隔符也要归入隐藏列表）
        if (e.mode == Mode_AddContact_N || e.mode == Mode_AddCoil_R ||
            e.mode == Mode_AddFuncBlock)
            m_ldToolActions << tb->addSeparator();

        QAction* act = tb->addAction(makeLdIcon(e.iconType), e.tooltip);
        act->setCheckable(true);
        act->setChecked(e.checked);
        modeGroup->addAction(act);
        m_ldModeActions[e.mode] = act;
        m_ldToolActions << act;
        connect(act, &QAction::triggered, [this, e](){
            if (m_scene) m_scene->setMode(e.mode);
        });
    }

    // 初始状态：没有任何视图激活，先隐藏全部 LD 工具
    for (QAction* a : m_ldToolActions) a->setVisible(false);
}

// ──────────────────────────────────────────────────────────────
// 工具栏状态同步（Escape 等触发 modeChanged 时调用）
// ──────────────────────────────────────────────────────────────
void MainWindow::onLdModeChanged(EditorMode mode)
{
    for (auto it = m_ldModeActions.begin(); it != m_ldModeActions.end(); ++it)
        it.value()->setChecked(it.key() == mode);
}

// ============================================================
// 左侧停靠栏：项目树
// ============================================================
void MainWindow::setupProjectPanel()
{
    QDockWidget* dock = new QDockWidget("Project", this);
    dock->setObjectName("projectDock");
    dock->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea);
    dock->setFeatures(QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetClosable);

    m_projectTree = new QTreeWidget();
    m_projectTree->setObjectName("projectTree");
    m_projectTree->setHeaderHidden(true);
    m_projectTree->setMinimumWidth(190);
    m_projectTree->setContextMenuPolicy(Qt::CustomContextMenu);

    connect(m_projectTree, &QTreeWidget::itemDoubleClicked,
            this, &MainWindow::onTreeDoubleClicked);
    connect(m_projectTree, &QTreeWidget::customContextMenuRequested,
            this, &MainWindow::onTreeContextMenu);

    m_projectTree->setStyle(new TreeBranchStyle());
    dock->setWidget(m_projectTree);
    addDockWidget(Qt::LeftDockWidgetArea, dock);
}

// ============================================================
// 右侧停靠栏：函数库 + 调试器
// ============================================================

// LibraryTreeWidget —— 支持把 function/functionBlock 叶节点拖到画布
class LibraryTreeWidget : public QTreeWidget {
public:
    explicit LibraryTreeWidget(QWidget* parent = nullptr) : QTreeWidget(parent) {
        setDragEnabled(true);
        setDragDropMode(QAbstractItemView::DragOnly);
    }
protected:
    void startDrag(Qt::DropActions /*supportedActions*/) override {
        QTreeWidgetItem* item = currentItem();
        if (!item) return;
        const QString kind = item->data(0, Qt::UserRole).toString();
        if (kind != "function" && kind != "functionBlock") return;

        const QString typeName = item->text(0);
        auto* mime = new QMimeData();
        mime->setData("application/x-tizi-blocktype", typeName.toUtf8());

        auto* drag = new QDrag(this);
        drag->setMimeData(mime);
        drag->exec(Qt::CopyAction);
    }
};

// Helper: recursively filter tree items by name (case-insensitive).
// Returns true if this item or any descendant matches, so the parent stays visible.
static bool filterLibraryItem(QTreeWidgetItem* item, const QString& text)
{
    const bool isLeaf = item->childCount() == 0;
    if (isLeaf) {
        const bool match = text.isEmpty() ||
                           item->text(0).contains(text, Qt::CaseInsensitive);
        item->setHidden(!match);
        return match;
    }
    bool anyChildVisible = false;
    for (int i = 0; i < item->childCount(); ++i)
        if (filterLibraryItem(item->child(i), text))
            anyChildVisible = true;
    item->setHidden(!anyChildVisible);
    item->setExpanded(!text.isEmpty() && anyChildVisible);
    return anyChildVisible;
}

// Helper: recursively populate tree from <category>/<function>/<functionBlock> DOM
// 从 DOM 元素读取 input/output 端口信息，以 "name|name|..." 和 "type|type|..." 形式存入 item
static void storePortData(QTreeWidgetItem* node, const QDomElement& elem)
{
    QStringList inNames, inTypes, outNames, outTypes;
    for (QDomElement port = elem.firstChildElement();
         !port.isNull(); port = port.nextSiblingElement())
    {
        if (port.tagName() == "input") {
            inNames  << port.attribute("name");
            inTypes  << port.attribute("type");
        } else if (port.tagName() == "output") {
            outNames << port.attribute("name");
            outTypes << port.attribute("type");
        }
    }
    node->setData(0, Qt::UserRole + 1, elem.attribute("comment"));
    node->setData(0, Qt::UserRole + 2, inNames.join("|"));
    node->setData(0, Qt::UserRole + 3, inTypes.join("|"));
    node->setData(0, Qt::UserRole + 4, outNames.join("|"));
    node->setData(0, Qt::UserRole + 5, outTypes.join("|"));
}

static void populateLibraryNode(QTreeWidgetItem* parent,
                                 const QDomElement& elem,
                                 const QIcon& folderIcon,
                                 const QIcon& fnIcon,
                                 const QIcon& fbIcon)
{
    for (QDomElement child = elem.firstChildElement();
         !child.isNull(); child = child.nextSiblingElement())
    {
        const QString tag = child.tagName();
        if (tag == "category") {
            auto* node = new QTreeWidgetItem(parent, QStringList{child.attribute("name")});
            node->setIcon(0, folderIcon);
            populateLibraryNode(node, child, folderIcon, fnIcon, fbIcon);
        } else if (tag == "function") {
            auto* node = new QTreeWidgetItem(parent, QStringList{child.attribute("name")});
            node->setIcon(0, fnIcon);
            const QString comment = child.attribute("comment");
            if (!comment.isEmpty()) node->setToolTip(0, comment);
            node->setData(0, Qt::UserRole, "function");
            storePortData(node, child);
        } else if (tag == "functionBlock") {
            auto* node = new QTreeWidgetItem(parent, QStringList{child.attribute("name")});
            node->setIcon(0, fbIcon);
            const QString comment = child.attribute("comment");
            if (!comment.isEmpty()) node->setToolTip(0, comment);
            node->setData(0, Qt::UserRole, "functionBlock");
            storePortData(node, child);
        }
    }
}

void MainWindow::setupLibraryPanel()
{
    QDockWidget* dock = new QDockWidget("Library", this);
    dock->setObjectName("libraryDock");
    dock->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea);
    dock->setFeatures(QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetClosable);

    m_libraryTabs = new QTabWidget();
    m_libraryTabs->setObjectName("libraryTabs");

    // Library 标签页
    QWidget* libWidget = new QWidget();
    QVBoxLayout* libLay = new QVBoxLayout(libWidget);
    libLay->setContentsMargins(4, 4, 4, 4);
    libLay->setSpacing(4);

    QLineEdit* searchEdit = new QLineEdit();
    searchEdit->setPlaceholderText("Search...");
    libLay->addWidget(searchEdit);

    m_libraryTree = new LibraryTreeWidget();
    m_libraryTree->setObjectName("libraryTree");
    m_libraryTree->setHeaderHidden(true);
    m_libraryTree->setStyle(new TreeBranchStyle());

    const QIcon folderIcon(":/images/BLOCK.png");
    const QIcon fnIcon(":/images/BLOCK.png");
    const QIcon fbIcon(":/images/BLOCK.png");

    // Load library.xml from the application bundle or source conf directory
    // At runtime, look alongside the executable; fall back to source tree
    const QStringList searchPaths = {
        QCoreApplication::applicationDirPath() + "/conf/library.xml",
        QCoreApplication::applicationDirPath() + "/../Resources/conf/library.xml",
        QString(LIBRARY_XML_PATH)   // injected by CMake at compile time
    };

    QDomDocument libDoc;
    for (const QString& p : searchPaths) {
        QFile f(p);
        if (f.open(QFile::ReadOnly) && libDoc.setContent(&f))
            break;
        libDoc.clear();
    }

    if (!libDoc.isNull()) {
        QDomElement root = libDoc.documentElement(); // <library>
        for (QDomElement cat = root.firstChildElement("category");
             !cat.isNull(); cat = cat.nextSiblingElement("category"))
        {
            auto* topItem = new QTreeWidgetItem(m_libraryTree, QStringList{cat.attribute("name")});
            topItem->setIcon(0, folderIcon);
            populateLibraryNode(topItem, cat, folderIcon, fnIcon, fbIcon);
        }
    } else {
        // Fallback: minimal static list if XML not found
        const QStringList fallback = {
            "Standard Functions", "Standard Function Blocks", "Additional Function Blocks"
        };
        for (const QString& cat : fallback) {
            auto* item = new QTreeWidgetItem(m_libraryTree, QStringList{cat});
            item->setIcon(0, folderIcon);
        }
    }

    // User-defined POUs node (always present)
    {
        auto* userNode = new QTreeWidgetItem(m_libraryTree, QStringList{"User-defined POU"});
        userNode->setIcon(0, folderIcon);
    }

    // 搜索框过滤：输入时实时显示/隐藏匹配项
    connect(searchEdit, &QLineEdit::textChanged,
            m_libraryTree, [this](const QString& text) {
        for (int i = 0; i < m_libraryTree->topLevelItemCount(); ++i)
            filterLibraryItem(m_libraryTree->topLevelItem(i), text);
    });

    // 双击叶节点（function / functionBlock）弹出 Block Properties 对话框
    connect(m_libraryTree, &QTreeWidget::itemDoubleClicked,
            this, [this](QTreeWidgetItem* item, int) {
        const QString kind = item->data(0, Qt::UserRole).toString();
        if (kind != "function" && kind != "functionBlock") return;

        const QString name    = item->text(0);
        const QString comment = item->data(0, Qt::UserRole + 1).toString();
        const QStringList inNames  = item->data(0, Qt::UserRole + 2).toString().split("|", Qt::SkipEmptyParts);
        const QStringList inTypes  = item->data(0, Qt::UserRole + 3).toString().split("|", Qt::SkipEmptyParts);
        const QStringList outNames = item->data(0, Qt::UserRole + 4).toString().split("|", Qt::SkipEmptyParts);
        const QStringList outTypes = item->data(0, Qt::UserRole + 5).toString().split("|", Qt::SkipEmptyParts);

        BlockPropertiesDialog dlg(name, kind, comment,
                                   inNames, inTypes,
                                   outNames, outTypes, this);
        dlg.exec();
    });

    libLay->addWidget(m_libraryTree);
    m_libraryTabs->addTab(libWidget, "Library");

    auto* debuggerWidget = new QWidget;
    auto* debuggerLayout = new QVBoxLayout(debuggerWidget);
    debuggerLayout->setContentsMargins(6, 6, 6, 6);
    debuggerLayout->setSpacing(6);

    m_debuggerTable = new QTableWidget(0, 5, debuggerWidget);
    m_debuggerTable->setHorizontalHeaderLabels(QStringList() << "Name" << "Type" << "Value" << "Forced" << "Sim I/O");
    m_debuggerTable->setEditTriggers(QAbstractItemView::NoEditTriggers);
    m_debuggerTable->setSelectionBehavior(QAbstractItemView::SelectRows);
    m_debuggerTable->setSelectionMode(QAbstractItemView::SingleSelection);
    m_debuggerTable->setContextMenuPolicy(Qt::CustomContextMenu);
    m_debuggerTable->setAlternatingRowColors(true);
    m_debuggerTable->verticalHeader()->setVisible(false);
    m_debuggerTable->verticalHeader()->setDefaultSectionSize(24);
    m_debuggerTable->horizontalHeader()->setStretchLastSection(false);
    m_debuggerTable->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Stretch);
    m_debuggerTable->horizontalHeader()->setSectionResizeMode(1, QHeaderView::ResizeToContents);
    m_debuggerTable->horizontalHeader()->setSectionResizeMode(2, QHeaderView::ResizeToContents);
    m_debuggerTable->horizontalHeader()->setSectionResizeMode(3, QHeaderView::ResizeToContents);
    m_debuggerTable->horizontalHeader()->setSectionResizeMode(4, QHeaderView::ResizeToContents);
    connect(m_debuggerTable, &QTableWidget::customContextMenuRequested,
            this, &MainWindow::showDebuggerContextMenu);

    auto* debuggerActions = new QHBoxLayout;
    auto* forceButton = new QPushButton("Force");
    auto* releaseButton = new QPushButton("Release");
    connect(forceButton, &QPushButton::clicked, this, &MainWindow::forceDebuggerVariable);
    connect(releaseButton, &QPushButton::clicked, this, &MainWindow::releaseDebuggerVariable);
    debuggerActions->addWidget(forceButton);
    debuggerActions->addWidget(releaseButton);
    debuggerActions->addStretch(1);

    debuggerLayout->addWidget(m_debuggerTable, 1);
    debuggerLayout->addLayout(debuggerActions);
    m_libraryTabs->addTab(debuggerWidget, "Debugger");
    m_libraryTabs->tabBar()->setExpanding(false);  // 让标签页左对齐
    m_libraryTabs->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred);

    dock->setWidget(m_libraryTabs);
    dock->setMinimumWidth(190);
    addDockWidget(Qt::RightDockWidgetArea, dock);
    resizeDocks({dock}, {320}, Qt::Horizontal);
}

// ============================================================
// 底部停靠栏：Search | Console | PLC Log
// ============================================================
void MainWindow::setupConsolePanel()
{
    QDockWidget* dock = new QDockWidget("Console", this);
    dock->setObjectName("consoleDock");
    dock->setAllowedAreas(Qt::BottomDockWidgetArea | Qt::TopDockWidgetArea);
    dock->setFeatures(QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetClosable);

    m_consoleTabs = new QTabWidget();
    m_consoleTabs->setObjectName("consoleTabs");
    m_consoleTabs->tabBar()->setExpanding(false);  // 标签页左对齐，不拉伸填满

    // Search
    QWidget* searchWidget = new QWidget();
    QHBoxLayout* searchLay = new QHBoxLayout(searchWidget);
    searchLay->addWidget(new QLabel("Find:"));
    searchLay->addWidget(new QLineEdit(), 1);
    searchLay->addWidget(new QPushButton("Find"));
    searchLay->addStretch();
    m_consoleTabs->addTab(searchWidget, "Search");

    // Console
    m_consoleEdit = new QPlainTextEdit();
    m_consoleEdit->setObjectName("consoleEdit");
    m_consoleEdit->setReadOnly(true);
    m_consoleEdit->setFont(QFont("Courier New", 9));
    m_consoleTabs->addTab(m_consoleEdit, "Console");

    // PLC Log
    m_plcLogEdit = new QPlainTextEdit();
    m_plcLogEdit->setReadOnly(true);
    m_plcLogEdit->setFont(QFont("Courier New", 9));
    m_plcLogEdit->setMaximumBlockCount(500);
    m_consoleTabs->addTab(m_plcLogEdit, "PLC Log");

    dock->setWidget(m_consoleTabs);
    addDockWidget(Qt::BottomDockWidgetArea, dock);
    resizeDocks({dock}, {160}, Qt::Vertical);

    // 底部面板只占中央区域，不延伸到左/右停靠栏下方
    setCorner(Qt::BottomLeftCorner,  Qt::LeftDockWidgetArea);
    setCorner(Qt::BottomRightCorner, Qt::BottomDockWidgetArea);
}

// ============================================================
// 中央区域：MDI 多子窗口编辑区（标签页视图）
// ============================================================
void MainWindow::setupCentralArea()
{
    m_mdiArea = new QMdiArea();
    m_mdiArea->setObjectName("mdiArea");
    m_mdiArea->setViewMode(QMdiArea::TabbedView);
    m_mdiArea->setTabsClosable(true);
    m_mdiArea->setTabsMovable(true);
    m_mdiArea->setDocumentMode(true);  // 去掉子窗口边框，纯标签外观

    // 激活不同子窗口时：同步 m_scene，并根据视图类型显示/隐藏 LD 工具栏
    connect(m_mdiArea, &QMdiArea::subWindowActivated,
            this, [this](QMdiSubWindow* sw) {
        // 断开旧的 canUndo/canRedo 连接
        QObject::disconnect(m_undoConn);
        QObject::disconnect(m_redoConn);

        if (!sw) {
            m_scene = nullptr;
            for (QAction* a : m_ldToolActions) a->setVisible(false);
            m_aUndo->setEnabled(false);
            m_aRedo->setEnabled(false);
            return;
        }
        PouModel* pou = m_subWinPouMap.value(sw, nullptr);
        m_scene = pou ? m_sceneMap.value(pou, nullptr) : nullptr;

        // 图形语言（LD / FBD / SFC）显示 LD 工具栏，文本语言（ST / IL）隐藏
        const bool isGraphical = pou &&
            (pou->language == PouLanguage::LD  ||
             pou->language == PouLanguage::FBD ||
             pou->language == PouLanguage::SFC);
        for (QAction* a : m_ldToolActions) a->setVisible(isGraphical);

        // 根据视图类型连接 Undo/Redo 使能信号
        if (isGraphical && m_scene) {
            QUndoStack* us = m_scene->undoStack();
            m_aUndo->setEnabled(us->canUndo());
            m_aRedo->setEnabled(us->canRedo());
            m_undoConn = connect(us, &QUndoStack::canUndoChanged, m_aUndo, &QAction::setEnabled);
            m_redoConn = connect(us, &QUndoStack::canRedoChanged, m_aRedo, &QAction::setEnabled);
        } else {
            // 文本编辑器：始终启用（QPlainTextEdit 内建 undo/redo）
            m_aUndo->setEnabled(true);
            m_aRedo->setEnabled(true);
        }
    });

    setCentralWidget(m_mdiArea);
}

// ============================================================
// 重建项目树
// ============================================================
void MainWindow::rebuildProjectTree()
{
    m_projectTree->clear();

    if (!m_project) return;

    auto* root = new QTreeWidgetItem(m_projectTree,
                                      QStringList{m_project->projectName});
    root->setIcon(0, QIcon(":/images/PROJECT.png"));
    root->setData(0, Qt::UserRole, QString("project-root"));  // 标记根节点
    root->setExpanded(true);

    // 语言图标映射
    static const QMap<PouLanguage, QString> s_langIcon = {
        { PouLanguage::LD,  ":/images/LD.png"  },
        { PouLanguage::ST,  ":/images/ST.png"  },
        { PouLanguage::IL,  ":/images/IL.png"  },
        { PouLanguage::FBD, ":/images/FBD.png" },
        { PouLanguage::SFC, ":/images/SFC.png" },
    };

    auto addPouItem = [&](QTreeWidgetItem* parent, PouModel* pou) {
        auto* item = new QTreeWidgetItem(parent, QStringList{pou->name});
        item->setIcon(0, QIcon(s_langIcon.value(pou->language, ":/images/Unknown.png")));
        item->setData(0, Qt::UserRole, QVariant::fromValue(static_cast<void*>(pou)));
    };

    // 三次遍历，按 Beremiz 惯例排列：Function → Function Blocks（分组）→ Program
    // Pass 1: Functions（直接挂根节点）
    for (PouModel* pou : m_project->pous)
        if (pou->pouType == PouType::Function)
            addPouItem(root, pou);

    // Pass 2: Function Blocks（放入可折叠子分组）
    QTreeWidgetItem* fbGroup = nullptr;
    for (PouModel* pou : m_project->pous) {
        if (pou->pouType != PouType::FunctionBlock) continue;
        if (!fbGroup) {
            fbGroup = new QTreeWidgetItem(root, QStringList{"Function Blocks"});
            fbGroup->setIcon(0, QIcon(":/images/FOLDER.png"));
            fbGroup->setExpanded(true);
        }
        addPouItem(fbGroup, pou);
    }

    // Pass 3: Programs（直接挂根节点）
    for (PouModel* pou : m_project->pous)
        if (pou->pouType == PouType::Program)
            addPouItem(root, pou);

    m_projectTree->expandAll();
}

// ============================================================
// 双击树节点 → 打开对应标签页
// ============================================================
void MainWindow::onTreeDoubleClicked(QTreeWidgetItem* item, int /*column*/)
{
    if (!item) return;

    // 项目根节点 → 打开项目属性面板
    if (item->data(0, Qt::UserRole).toString() == "project-root") {
        openProjectProperties();
        return;
    }

    void* ptr = item->data(0, Qt::UserRole).value<void*>();
    if (!ptr) return;
    openPouTab(static_cast<PouModel*>(ptr));
}

// ============================================================
// 右键菜单 → 添加 POU
// ============================================================
void MainWindow::onTreeContextMenu(const QPoint& pos)
{
    QMenu menu(this);
    QAction* addAct = menu.addAction("Add POU...");
    if (menu.exec(m_projectTree->viewport()->mapToGlobal(pos)) != addAct)
        return;

    // 对话框：输入名称 / 选择类型 / 选择语言
    QDialog dlg(this);
    dlg.setWindowTitle("New POU");
    dlg.setFixedWidth(300);

    QFormLayout form(&dlg);

    QLineEdit nameEdit;
    nameEdit.setPlaceholderText("e.g. MyCounter");
    form.addRow("Name:", &nameEdit);

    QComboBox typeCombo;
    typeCombo.addItems({"Function Block", "Program", "Function"});
    form.addRow("Type:", &typeCombo);

    QComboBox langCombo;
    langCombo.addItems({"LD", "ST", "IL", "FBD", "SFC"});
    form.addRow("Language:", &langCombo);

    QDialogButtonBox buttons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel,
                              Qt::Horizontal, &dlg);
    form.addRow(&buttons);
    connect(&buttons, &QDialogButtonBox::accepted, &dlg, &QDialog::accept);
    connect(&buttons, &QDialogButtonBox::rejected, &dlg, &QDialog::reject);

    if (dlg.exec() != QDialog::Accepted) return;

    const QString name = nameEdit.text().trimmed();
    if (name.isEmpty()) return;
    if (m_project->pouNameExists(name)) {
        QMessageBox::warning(this, "Error",
            QString("A POU named \"%1\" already exists.").arg(name));
        return;
    }

    const QStringList typeStr = {"functionBlock", "program", "function"};
    PouType  type = PouModel::typeFromString(typeStr[typeCombo.currentIndex()]);
    PouLanguage lang = PouModel::langFromString(langCombo.currentText());

    PouModel* pou = m_project->addPou(name, type, lang);
    rebuildProjectTree();
    openPouTab(pou);
}

// ============================================================
// 打开 / 切换到某个 POU 的子窗口
// ============================================================
void MainWindow::openPouTab(PouModel* pou)
{
    if (!pou) return;

    // 如果已经打开，直接激活
    for (QMdiSubWindow* sw : m_mdiArea->subWindowList()) {
        if (m_subWinPouMap.value(sw) == pou) {
            m_mdiArea->setActiveSubWindow(sw);
            return;
        }
    }

    // 新建子窗口
    QWidget* editor = createPouEditorWidget(pou);

    QMdiSubWindow* sw = m_mdiArea->addSubWindow(editor);
    sw->setAttribute(Qt::WA_DeleteOnClose);
    sw->setWindowTitle(QString("[%1]  %2")
        .arg(PouModel::langToString(pou->language))
        .arg(pou->name));

    m_subWinPouMap[sw] = pou;

    // 子窗口销毁时清理映射
    connect(sw, &QObject::destroyed, this, [this, sw]() {
        m_subWinPouMap.remove(sw);
    });

    sw->show();
}

// ============================================================
// 关闭所有 POU 子窗口（新建 / 打开项目时调用）
// ============================================================
void MainWindow::closeAllPouTabs()
{
    // 先断开 destroyed 信号，避免在 closeAllSubWindows 过程中迭代器失效
    for (QMdiSubWindow* sw : m_mdiArea->subWindowList())
        sw->disconnect(this);

    m_subWinPouMap.clear();
    m_projPropSubWin = nullptr;
    if (m_smartSimWindow) {
        m_smartSimWindow->close();
        m_smartSimWindow = nullptr;
    }
    m_scene = nullptr;
    m_mdiArea->closeAllSubWindows();
}

// ============================================================
// PC 仿真面板（单例）
// ============================================================
void MainWindow::openSmartSim()
{
    if (!m_project) {
        QMessageBox::warning(this, "SmartSim", "Please create or open a project first.");
        return;
    }
    if (m_project->filePath.isEmpty()) {
        QMessageBox::warning(this, "SmartSim", "Please save the project before starting simulation.");
        return;
    }

    ProjectManager::syncScenesBeforeSave(m_sceneMap);
    m_project->saveToFile(m_project->filePath);

    if (m_smartSimWindow) {
        if (m_libraryTabs && m_debuggerTable)
            m_libraryTabs->setCurrentWidget(m_debuggerTable->parentWidget());
        if (m_consoleTabs && m_plcLogEdit)
            m_consoleTabs->setCurrentWidget(m_plcLogEdit);
        syncSimIoMappings();
        m_smartSimWindow->show();
        m_smartSimWindow->raise();
        m_smartSimWindow->activateWindow();
        return;
    }

    m_smartSimWindow = new SmartSimWidget(m_project);
    m_smartSimWindow->setAttribute(Qt::WA_DeleteOnClose);
    m_smartSimWindow->setWindowFlag(Qt::WindowMaximizeButtonHint, false);
    m_smartSimWindow->setWindowTitle("SmartSim - PC PLC Simulation");
    m_smartSimWindow->setFixedSize(820, 560);

    connect(m_smartSimWindow, &SmartSimWidget::debugValuesChanged,
            this, &MainWindow::updateDebuggerValues);
    connect(m_smartSimWindow, &SmartSimWidget::simulationEvent,
            this, &MainWindow::appendPlcLog);
    connect(m_smartSimWindow, &SmartSimWidget::simulationConnectionChanged,
            this, [this](bool connected) {
        if (connected) {
            m_plcUri = "SIM://SmartSim";
            setPlcConnState(PlcConnState::Connected);
            setPlcRunState(PlcRunState::Stopped);
            statusBar()->showMessage("Connected to SmartSim.", 3000);
        } else if (m_plcUri == "SIM://SmartSim") {
            m_plcUri.clear();
            setPlcConnState(PlcConnState::Disconnected);
            setPlcRunState(PlcRunState::Unknown);
            statusBar()->showMessage("SmartSim disconnected.", 3000);
        }
    });
    connect(m_smartSimWindow, &SmartSimWidget::simulationRunStateChanged,
            this, [this](const QString& state) {
        if (m_plcUri != "SIM://SmartSim")
            return;
        if (state == "Running")
            setPlcRunState(PlcRunState::Running);
        else if (state == "Paused")
            setPlcRunState(PlcRunState::Paused);
        else if (state == "Stopped")
            setPlcRunState(PlcRunState::Stopped);
    });

    connect(m_smartSimWindow, &QObject::destroyed, this, [this] {
        if (m_plcUri == "SIM://SmartSim") {
            m_plcUri.clear();
            setPlcConnState(PlcConnState::Disconnected);
            setPlcRunState(PlcRunState::Unknown);
        }
        m_smartSimWindow = nullptr;
    });

    if (m_libraryTabs && m_debuggerTable)
        m_libraryTabs->setCurrentWidget(m_debuggerTable->parentWidget());
    if (m_consoleTabs && m_plcLogEdit)
        m_consoleTabs->setCurrentWidget(m_plcLogEdit);
    syncSimIoMappings();

    m_smartSimWindow->show();
    m_smartSimWindow->raise();
    m_smartSimWindow->activateWindow();
}

void MainWindow::updateDebuggerValues(const QVector<SimDebugValue>& vars)
{
    if (!m_debuggerTable)
        return;

    m_lastDebugValues = vars;
    const QString selected = selectedDebuggerVariable();
    m_debuggerTable->setRowCount(vars.size());

    int selectedRow = -1;
    for (int row = 0; row < vars.size(); ++row) {
        const SimDebugValue& var = vars[row];
        auto* nameItem = new QTableWidgetItem(var.name);
        auto* typeItem = new QTableWidgetItem(var.type);
        auto* valueItem = new QTableWidgetItem(debugValueText(var.value));
        auto* forcedItem = new QTableWidgetItem(var.forced ? "Yes" : "No");
        auto* ioItem = new QTableWidgetItem(simIoRoleForVariable(var.name));

        const QList<QTableWidgetItem*> items{nameItem, typeItem, valueItem, forcedItem, ioItem};
        for (QTableWidgetItem* item : items) {
            item->setFlags(item->flags() & ~Qt::ItemIsEditable);
            item->setToolTip(item->text());
            if (var.forced)
                item->setBackground(QColor("#fff4bf"));
        }
        typeItem->setTextAlignment(Qt::AlignCenter);
        valueItem->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter);
        forcedItem->setTextAlignment(Qt::AlignCenter);
        ioItem->setTextAlignment(Qt::AlignCenter);

        m_debuggerTable->setItem(row, 0, nameItem);
        m_debuggerTable->setItem(row, 1, typeItem);
        m_debuggerTable->setItem(row, 2, valueItem);
        m_debuggerTable->setItem(row, 3, forcedItem);
        m_debuggerTable->setItem(row, 4, ioItem);

        if (var.name == selected)
            selectedRow = row;
    }

    if (selectedRow >= 0)
        m_debuggerTable->selectRow(selectedRow);
    else if (m_debuggerTable->rowCount() > 0 && m_debuggerTable->currentRow() < 0)
        m_debuggerTable->selectRow(0);
}

void MainWindow::appendPlcLog(const QString& message)
{
    if (!m_plcLogEdit)
        return;

    m_plcLogEdit->appendPlainText(message);
    m_plcLogEdit->ensureCursorVisible();
}

QString MainWindow::selectedDebuggerVariable() const
{
    if (!m_debuggerTable)
        return QString();

    const int row = m_debuggerTable->currentRow();
    if (row < 0)
        return QString();

    QTableWidgetItem* item = m_debuggerTable->item(row, 0);
    return item ? item->text() : QString();
}

void MainWindow::forceDebuggerVariable()
{
    if (!m_smartSimWindow || !m_debuggerTable)
        return;

    const int row = m_debuggerTable->currentRow();
    if (row < 0)
        return;

    QTableWidgetItem* nameItem = m_debuggerTable->item(row, 0);
    QTableWidgetItem* typeItem = m_debuggerTable->item(row, 1);
    QTableWidgetItem* valueItem = m_debuggerTable->item(row, 2);
    if (!nameItem || !typeItem || !valueItem)
        return;

    const QString selectedName = nameItem->text();
    const QString selectedType = typeItem->text();
    const QString selectedValue = valueItem->text();

    bool accepted = false;
    const QString text = QInputDialog::getText(
        this,
        "Force Variable",
        QString("Force %1 (%2)").arg(selectedName, selectedType),
        QLineEdit::Normal,
        selectedValue,
        &accepted);
    if (!accepted)
        return;

    bool parsed = false;
    const QVariant value = parseDebugForceValue(selectedType, text, &parsed);
    if (!parsed) {
        appendPlcLog(QString("Invalid force value for %1.").arg(selectedName));
        return;
    }

    const QString requestedName = selectedName;
    const QString targetName = debuggerForceTarget(requestedName);
    if (targetName != requestedName) {
        appendPlcLog(QString("Redirect force %1 -> %2.").arg(requestedName, targetName));
    }

    m_smartSimWindow->forceVariable(targetName, value);
}

void MainWindow::releaseDebuggerVariable()
{
    if (!m_smartSimWindow)
        return;

    const QString name = selectedDebuggerVariable();
    if (name.isEmpty())
        return;

    const QString targetName = debuggerForceTarget(name);
    if (targetName != name)
        appendPlcLog(QString("Redirect release %1 -> %2.").arg(name, targetName));

    m_smartSimWindow->releaseVariable(targetName);
}

QString MainWindow::debuggerForceTarget(const QString& name) const
{
    if (!m_debuggerTable || !name.startsWith("main.") || name.count('.') < 2)
        return name;

    const QString leaf = name.section('.', -1);
    const QString normalizedLeaf = QString(leaf).remove('_').toUpper();
    for (int row = 0; row < m_debuggerTable->rowCount(); ++row) {
        QTableWidgetItem* item = m_debuggerTable->item(row, 0);
        if (!item)
            continue;

        const QString candidate = item->text();
        if (!candidate.startsWith("main.") || candidate.count('.') != 1)
            continue;

        const QString candidateLeaf = candidate.section('.', -1);
        if (QString(candidateLeaf).remove('_').toUpper() == normalizedLeaf)
            return candidate;
    }

    return name;
}

void MainWindow::showDebuggerContextMenu(const QPoint& pos)
{
    if (!m_debuggerTable)
        return;

    const QModelIndex index = m_debuggerTable->indexAt(pos);
    if (!index.isValid())
        return;

    m_debuggerTable->selectRow(index.row());
    const QString name = selectedDebuggerVariable();
    if (name.isEmpty())
        return;

    QMenu menu(this);
    auto addChannelMenu = [this, &menu](const QString& role, int count) {
        QMenu* channelMenu = menu.addMenu(QString("Add to SmartSim %1").arg(role));
        for (int i = 0; i < count; ++i) {
            channelMenu->addAction(QString("%1%2").arg(role).arg(i), this, [this, role, i] {
                addDebuggerVariableToSimIo(role, i);
            });
        }
    };
    addChannelMenu("DI", 16);
    addChannelMenu("DO", 16);
    addChannelMenu("AI", 4);
    addChannelMenu("AO", 4);
    menu.addSeparator();
    menu.addAction("Remove from SmartSim I/O", this, &MainWindow::removeDebuggerVariableFromSimIo);
    menu.exec(m_debuggerTable->viewport()->mapToGlobal(pos));
}

void MainWindow::addDebuggerVariableToSimIo(const QString& role, int index)
{
    const QString name = selectedDebuggerVariable();
    if (name.isEmpty())
        return;

    auto removeFrom = [&name](QStringList& list) {
        for (QString& item : list) {
            if (item == name)
                item.clear();
        }
    };
    removeFrom(m_simDiVars);
    removeFrom(m_simDoVars);
    removeFrom(m_simAiVars);
    removeFrom(m_simAoVars);

    QStringList* target = nullptr;
    if (role == "DI")
        target = &m_simDiVars;
    else if (role == "DO")
        target = &m_simDoVars;
    else if (role == "AI")
        target = &m_simAiVars;
    else if (role == "AO")
        target = &m_simAoVars;

    if (!target || index < 0 || index >= target->size())
        return;

    (*target)[index] = name;

    appendPlcLog(QString("Mapped %1 to SmartSim %2%3.").arg(name, role).arg(index));
    syncSimIoMappings();
    updateDebuggerValues(m_lastDebugValues);
}

void MainWindow::removeDebuggerVariableFromSimIo()
{
    const QString name = selectedDebuggerVariable();
    if (name.isEmpty())
        return;

    auto removeFrom = [&name](QStringList& list) {
        for (QString& item : list) {
            if (item == name)
                item.clear();
        }
    };
    removeFrom(m_simDiVars);
    removeFrom(m_simDoVars);
    removeFrom(m_simAiVars);
    removeFrom(m_simAoVars);

    appendPlcLog(QString("Removed %1 from SmartSim I/O.").arg(name));
    syncSimIoMappings();
    updateDebuggerValues(m_lastDebugValues);
}

void MainWindow::syncSimIoMappings()
{
    if (!m_smartSimWindow)
        return;

    m_smartSimWindow->setIoMappings(m_simDiVars, m_simDoVars, m_simAiVars, m_simAoVars);
}

QString MainWindow::simIoRoleForVariable(const QString& name) const
{
    const int di = m_simDiVars.indexOf(name);
    if (di >= 0)
        return QString("DI%1").arg(di);
    const int dout = m_simDoVars.indexOf(name);
    if (dout >= 0)
        return QString("DO%1").arg(dout);
    const int ai = m_simAiVars.indexOf(name);
    if (ai >= 0)
        return QString("AI%1").arg(ai);
    const int ao = m_simAoVars.indexOf(name);
    if (ao >= 0)
        return QString("AO%1").arg(ao);
    return QString();
}

// ============================================================
// 项目属性面板（单例）
// ============================================================
void MainWindow::openProjectProperties()
{
    if (!m_project) return;

    // 若已打开则激活
    if (m_projPropSubWin) {
        m_mdiArea->setActiveSubWindow(m_projPropSubWin);
        return;
    }

    QWidget* w = createProjectPropertiesWidget();
    m_projPropSubWin = m_mdiArea->addSubWindow(w);
    m_projPropSubWin->setWindowTitle(
        QString("Project — %1").arg(m_project->projectName));
    m_projPropSubWin->setWindowIcon(QIcon(":/images/PROJECT.png"));
    m_projPropSubWin->setAttribute(Qt::WA_DeleteOnClose);
    m_projPropSubWin->resize(480, 560);
    m_projPropSubWin->show();

    // 关闭时清空指针
    connect(m_projPropSubWin, &QObject::destroyed, this,
            [this]{ m_projPropSubWin = nullptr; });
}

QWidget* MainWindow::createProjectPropertiesWidget()
{
    auto* w      = new QWidget();
    auto* topLay = new QVBoxLayout(w);
    topLay->setContentsMargins(12, 12, 12, 12);
    topLay->setSpacing(10);

    // ── Project Properties ────────────────────────────────────
    auto* projGroup = new QGroupBox("Project Properties");
    auto* projForm  = new QFormLayout(projGroup);
    projForm->setContentsMargins(8, 10, 8, 10);
    projForm->setSpacing(6);

    auto* nameEdit    = new QLineEdit(m_project->projectName);
    auto* authorEdit  = new QLineEdit(m_project->author);
    auto* compEdit    = new QLineEdit(m_project->companyName);
    auto* verEdit     = new QLineEdit(m_project->productVersion);
    auto* descEdit    = new QPlainTextEdit(m_project->description);
    descEdit->setFixedHeight(68);
    auto* createdLbl  = new QLabel(m_project->creationDateTime.isEmpty()
                                    ? "(unknown)" : m_project->creationDateTime);
    auto* modLbl      = new QLabel(m_project->modificationDateTime.isEmpty()
                                    ? "(unknown)" : m_project->modificationDateTime);
    createdLbl->setStyleSheet("color:#666;");
    modLbl->setStyleSheet("color:#666;");

    projForm->addRow("Project Name:",  nameEdit);
    projForm->addRow("Author:",        authorEdit);
    projForm->addRow("Company:",       compEdit);
    projForm->addRow("Version:",       verEdit);
    projForm->addRow("Description:",   descEdit);
    projForm->addRow("Created:",       createdLbl);
    projForm->addRow("Last Modified:", modLbl);

    // ── Build ────────────────────────────────────────────────
    auto* buildGroup = new QGroupBox("Build");
    auto* buildForm  = new QFormLayout(buildGroup);
    buildForm->setContentsMargins(8, 10, 8, 10);
    buildForm->setSpacing(6);

    // ── Driver 下拉列表（从 drivers 目录动态枚举）───────────────
    auto* driverCombo = new QComboBox();
    driverCombo->addItem("(auto — by Target Type)", QString());  // 空 = 自动
    {
        QStringList driverRoots = {
            QCoreApplication::applicationDirPath() + "/drivers",
            QString(DRIVERS_DIR),
        };
        QSet<QString> seen;
        for (const QString& root : driverRoots) {
            for (const QFileInfo& fi : QDir(root).entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot)) {
                if (seen.contains(fi.fileName())) continue;
                const QString driverJson = fi.absoluteFilePath() + "/driver.json";
                if (!QFileInfo::exists(driverJson)) continue;
                seen.insert(fi.fileName());
                // 读取 name 和 mode
                QFile f(driverJson);
                QString label = fi.fileName();
                if (f.open(QFile::ReadOnly)) {
                    QJsonObject obj = QJsonDocument::fromJson(f.readAll()).object();
                    QString dName = obj["name"].toString();
                    QString dMode = obj["mode"].toString("NCC");
                    if (!dName.isEmpty())
                        label = QString("%1  [%2]").arg(dName, dMode);
                }
                driverCombo->addItem(label, fi.fileName());
            }
        }
    }
    // 设置当前选项
    if (m_project->driver.isEmpty()) {
        driverCombo->setCurrentIndex(0);
    } else {
        int idx = driverCombo->findData(m_project->driver);
        if (idx >= 0) driverCombo->setCurrentIndex(idx);
    }

    // ── Mode 下拉（根据所选 driver 支持的模式动态更新）────────
    auto* modeCombo = new QComboBox();
    modeCombo->setEnabled(false);  // 默认禁用，由 driver 决定

    // 工具函数：从 driver.json 解析支持的 mode 列表
    auto loadDriverModes = [](const QString& driverName) -> QStringList {
        QStringList modes;
        QStringList roots = {
            QCoreApplication::applicationDirPath() + "/drivers",
            QString(DRIVERS_DIR),
        };
        for (const QString& root : roots) {
            const QString djPath = root + "/" + driverName + "/driver.json";
            if (!QFileInfo::exists(djPath)) continue;
            QFile f(djPath);
            if (!f.open(QFile::ReadOnly)) continue;
            QJsonObject obj = QJsonDocument::fromJson(f.readAll()).object();
            QJsonValue mv = obj["mode"];
            if (mv.isString()) {
                modes << mv.toString();
            } else if (mv.isArray()) {
                for (const QJsonValue& v : mv.toArray())
                    modes << v.toString();
            }
            break;
        }
        return modes;
    };

    // 刷新 modeCombo 的工具函数
    auto refreshModeCombo = [this, modeCombo, loadDriverModes](const QString& driverName) {
        modeCombo->clear();
        QStringList modes = loadDriverModes(driverName);
        if (modes.isEmpty()) {
            modeCombo->addItem("NCC");
            modeCombo->setEnabled(false);
        } else if (modes.size() == 1) {
            modeCombo->addItem(modes.first());
            modeCombo->setEnabled(false);  // 硬件只支持一种，不可选
        } else {
            modeCombo->addItems(modes);
            modeCombo->setEnabled(true);
        }
        int mi = modeCombo->findText(m_project->mode);
        if (mi >= 0) modeCombo->setCurrentIndex(mi);
    };

    // 初始化 modeCombo
    refreshModeCombo(m_project->driver);

    auto* targetCombo  = new QComboBox();
    targetCombo->addItems({"Linux", "Mac", "Windows", "Embedded"});
    targetCombo->setCurrentText(m_project->targetType);
    auto* compilerEdit = new QLineEdit(m_project->compiler);
    auto* cflagsEdit   = new QLineEdit(m_project->cflags);
    auto* linkerEdit   = new QLineEdit(m_project->linker);
    auto* ldflagsEdit  = new QLineEdit(m_project->ldflags);

    buildForm->addRow("Driver:", driverCombo);
    buildForm->addRow("Mode:",   modeCombo);
    buildForm->addRow("Target Type:", targetCombo);
    buildForm->addRow("Compiler:",    compilerEdit);
    buildForm->addRow("CFLAGS:",      cflagsEdit);
    buildForm->addRow("Linker:",      linkerEdit);
    buildForm->addRow("LDFLAGS:",     ldflagsEdit);

    topLay->addWidget(projGroup);
    topLay->addWidget(buildGroup);
    topLay->addStretch();

    // ── 连接：字段变化 → 更新 ProjectModel ───────────────────
    connect(nameEdit, &QLineEdit::textChanged, this, [this](const QString& v) {
        m_project->projectName = v;
        m_project->markDirty();
        updateWindowTitle();
        if (m_projPropSubWin)
            m_projPropSubWin->setWindowTitle(QString("Project — %1").arg(v));
        // 同步项目树根节点文字
        if (m_projectTree->topLevelItemCount() > 0)
            m_projectTree->topLevelItem(0)->setText(0, v);
    });
    connect(authorEdit,  &QLineEdit::textChanged, this,
            [this](const QString& v){ m_project->author = v; m_project->markDirty(); });
    connect(compEdit,    &QLineEdit::textChanged, this,
            [this](const QString& v){ m_project->companyName = v; m_project->markDirty(); });
    connect(verEdit,     &QLineEdit::textChanged, this,
            [this](const QString& v){ m_project->productVersion = v; m_project->markDirty(); });
    connect(descEdit, &QPlainTextEdit::textChanged, this, [this, descEdit]{
        m_project->description = descEdit->toPlainText();
        m_project->markDirty();
    });
    connect(driverCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
            [this, driverCombo, refreshModeCombo](int idx){
                m_project->driver = driverCombo->itemData(idx).toString();
                refreshModeCombo(m_project->driver);
                m_project->markDirty();
            });
    connect(modeCombo, &QComboBox::currentTextChanged, this,
            [this](const QString& v){ m_project->mode = v; m_project->markDirty(); });
    connect(targetCombo, &QComboBox::currentTextChanged, this,
            [this](const QString& v){ m_project->targetType = v; m_project->markDirty(); });
    connect(compilerEdit, &QLineEdit::textChanged, this,
            [this](const QString& v){ m_project->compiler = v; m_project->markDirty(); });
    connect(cflagsEdit,  &QLineEdit::textChanged, this,
            [this](const QString& v){ m_project->cflags = v; m_project->markDirty(); });
    connect(linkerEdit,  &QLineEdit::textChanged, this,
            [this](const QString& v){ m_project->linker = v; m_project->markDirty(); });
    connect(ldflagsEdit, &QLineEdit::textChanged, this,
            [this](const QString& v){ m_project->ldflags = v; m_project->markDirty(); });

    return w;
}

// ============================================================
// 创建 POU 编辑器控件
// ============================================================
QWidget* MainWindow::createPouEditorWidget(PouModel* pou)
{
    QWidget* w = new QWidget();
    QVBoxLayout* vlay = new QVBoxLayout(w);
    vlay->setContentsMargins(0, 0, 0, 0);
    vlay->setSpacing(0);

    QWidget* varDecl = createVarDeclWidget(pou);

    QWidget* editorArea = nullptr;

    const bool isGraphical = (pou->language == PouLanguage::LD  ||
                               pou->language == PouLanguage::FBD ||
                               pou->language == PouLanguage::SFC ||
                               !pou->graphicalXml.isEmpty());

    if (isGraphical) {
        // ── 统一图形编辑器（LD / FBD / SFC，含 PLCopen 导入）──────
        PlcOpenViewer* scene = m_sceneMap.value(pou, nullptr);
        if (!scene) {
            scene = new PlcOpenViewer(this);
            if (!pou->graphicalXml.isEmpty())
                scene->loadFromXmlString(pou->graphicalXml);
            else {
                // 传入语言字符串，scene 据此初始化正确的 body DOM
                QString langStr = "LD";
                if (pou->language == PouLanguage::FBD) langStr = "FBD";
                else if (pou->language == PouLanguage::SFC) langStr = "SFC";
                scene->initEmpty(langStr);
            }
            m_sceneMap[pou] = scene;
        }

        auto* view = new LadderView();
        view->setScene(scene);

        if (!m_scene) m_scene = scene;

        // 延迟 fitInView，等 MDI 子窗口完成布局后再缩放（0ms 有时不够）
        QTimer::singleShot(50, view, [view, scene](){
            QRectF r = scene->itemsBoundingRect().adjusted(-40, -40, 40, 40);
            if (r.isEmpty())
                r = QRectF(0, 0, 800, 600);
            // 确保 sceneRect 覆盖全部内容（防止大型图形被裁剪）
            if (!scene->sceneRect().contains(r))
                scene->setSceneRect(r.adjusted(-40, -40, 40, 40));
            view->fitInView(r, Qt::KeepAspectRatio);
        });

        connect(scene, &PlcOpenViewer::modeChanged,
                this,  &MainWindow::onLdModeChanged);
        connect(scene, &PlcOpenViewer::modeChanged,
                view,  &LadderView::onModeChanged);

        editorArea = view;

    } else if (pou->language == PouLanguage::ST ||
               pou->language == PouLanguage::IL) {
        // ── ST / IL 文本编辑器 ───────────────────────────────────
        auto* editor = new QPlainTextEdit();
        editor->setObjectName("stEditor");
        editor->setPlainText(pou->code);
        editor->setFont(QFont("Courier New", 11));

        new StHighlighter(editor->document());

        connect(editor, &QPlainTextEdit::textChanged, [pou, editor](){
            pou->code = editor->toPlainText();
        });
        editorArea = editor;

    } else {
        // ── 未知语言兜底 ──────────────────────────────────────────
        auto* fallback = new QLabel(
            QString("[ Unsupported POU language: %1 ]")
                .arg(PouModel::langToString(pou->language)));
        fallback->setAlignment(Qt::AlignCenter);
        editorArea = fallback;
    }

    QSplitter* splitter = new QSplitter(Qt::Vertical);
    splitter->addWidget(varDecl);
    splitter->addWidget(editorArea);
    splitter->setSizes({160, 600});
    splitter->setStretchFactor(0, 0);
    splitter->setStretchFactor(1, 1);

    vlay->addWidget(splitter);
    return w;
}

// ============================================================
// 变量声明表控件（由 PouModel 填充）
// ============================================================
QWidget* MainWindow::createVarDeclWidget(PouModel* pou)
{
    QWidget* w = new QWidget();
    w->setObjectName("varDeclWidget");
    QVBoxLayout* vlay = new QVBoxLayout(w);
    vlay->setContentsMargins(6, 4, 6, 0);
    vlay->setSpacing(4);

    // — 第一行：Description + Class Filter + 按钮 —
    QHBoxLayout* hlay = new QHBoxLayout();
    hlay->setSpacing(6);
    hlay->addWidget(new QLabel("Description:"));
    auto* descEdit = new QLineEdit(pou->description);
    hlay->addWidget(descEdit, 1);
    hlay->addSpacing(16);
    hlay->addWidget(new QLabel("Class Filter:"));
    auto* classFilter = new QComboBox();
    classFilter->addItems({"All", "Input", "Output", "InOut", "Local", "External"});
    classFilter->setFixedWidth(80);
    hlay->addWidget(classFilter);
    auto* btnAdd = new QPushButton("+");
    btnAdd->setObjectName("btnVarAdd");
    btnAdd->setFixedSize(22, 22);
    auto* btnDel = new QPushButton("-");
    btnDel->setObjectName("btnVarDel");
    btnDel->setFixedSize(22, 22);
    hlay->addWidget(btnAdd);
    hlay->addWidget(btnDel);
    vlay->addLayout(hlay);

    // — 变量表格 —
    auto* table = new QTableWidget();
    table->setObjectName("varTable");
    table->setColumnCount(6);
    table->setHorizontalHeaderLabels({"#", "Name", "Class", "Type", "Initial Value", "Comment"});
    table->verticalHeader()->setVisible(false);
    table->horizontalHeader()->setStretchLastSection(true);
    table->setAlternatingRowColors(true);
    table->setSelectionBehavior(QAbstractItemView::SelectRows);
    table->setEditTriggers(QAbstractItemView::DoubleClicked | QAbstractItemView::EditKeyPressed);
    table->setColumnWidth(0, 32);
    table->setColumnWidth(1, 110);
    table->setColumnWidth(2, 88);
    table->setColumnWidth(3, 110);
    table->setColumnWidth(4, 100);

    const QStringList varClasses = {"Input", "Output", "InOut", "Local", "External", "Global"};
    const QStringList varTypes = {
        "BOOL", "SINT", "INT", "DINT", "LINT",
        "USINT", "UINT", "UDINT", "ULINT",
        "REAL", "LREAL",
        "TIME", "DATE", "TIME_OF_DAY", "TOD", "DATE_AND_TIME", "DT",
        "STRING", "BYTE", "WORD", "DWORD", "LWORD"
    };

    table->setItemDelegateForColumn(2, new ComboBoxDelegate(varClasses, table));
    table->setItemDelegateForColumn(3, new ComboBoxDelegate(varTypes, table));

    // —— 辅助：将 pou->variables[row] 的数据写入 table 第 row 行 ——
    auto fillRow = [table](int row, const VariableDecl& v) {
        table->setItem(row, 0, new QTableWidgetItem(QString::number(row + 1)));
        table->setItem(row, 1, new QTableWidgetItem(v.name));
        table->setItem(row, 2, new QTableWidgetItem(v.varClass));
        table->setItem(row, 3, new QTableWidgetItem(v.type));
        table->setItem(row, 4, new QTableWidgetItem(v.initValue));
        table->setItem(row, 5, new QTableWidgetItem(v.comment));
        table->setRowHeight(row, 24);
        // 第0列（序号）不可编辑
        if (auto* it = table->item(row, 0))
            it->setFlags(it->flags() & ~Qt::ItemIsEditable);
    };

    // —— 辅助：刷新整列序号 ——
    auto refreshNumbers = [table]() {
        for (int r = 0; r < table->rowCount(); ++r) {
            if (!table->item(r, 0))
                table->setItem(r, 0, new QTableWidgetItem());
            table->item(r, 0)->setText(QString::number(r + 1));
        }
    };

    // 从 PouModel 填充（阻塞 cellChanged，避免在初始化时触发同步）
    table->blockSignals(true);
    const int rowCount = pou->variables.size();
    table->setRowCount(rowCount);
    for (int i = 0; i < rowCount; ++i)
        fillRow(i, pou->variables[i]);
    table->blockSignals(false);

    // —— + 按钮：追加一行空变量 ——
    connect(btnAdd, &QPushButton::clicked, table, [table, pou, fillRow, refreshNumbers]() {
        VariableDecl v;
        v.name     = QString("var%1").arg(pou->variables.size() + 1);
        v.varClass = "Local";
        v.type     = "BOOL";
        pou->variables.append(v);

        table->blockSignals(true);
        int row = table->rowCount();
        table->insertRow(row);
        fillRow(row, v);
        refreshNumbers();
        table->blockSignals(false);

        table->scrollToBottom();
        table->selectRow(row);
    });

    // —— - 按钮：删除选中行 ——
    connect(btnDel, &QPushButton::clicked, table, [table, pou, refreshNumbers]() {
        QList<int> rows;
        for (auto* sel : table->selectedItems())
            if (!rows.contains(sel->row())) rows.append(sel->row());
        std::sort(rows.rbegin(), rows.rend());   // 从大到小删，避免索引漂移

        table->blockSignals(true);
        for (int r : rows) {
            if (r >= 0 && r < pou->variables.size()) {
                pou->variables.removeAt(r);
                table->removeRow(r);
            }
        }
        refreshNumbers();
        table->blockSignals(false);
    });

    // —— cellChanged：将编辑同步回 PouModel ——
    connect(table, &QTableWidget::cellChanged, [table, pou](int row, int col) {
        if (row < 0 || row >= pou->variables.size()) return;
        auto* it = table->item(row, col);
        if (!it) return;
        VariableDecl& v = pou->variables[row];
        switch (col) {
        case 1: v.name      = it->text(); break;
        case 2: v.varClass  = it->text(); break;
        case 3: v.type      = it->text(); break;
        case 4: v.initValue = it->text(); break;
        case 5: v.comment   = it->text(); break;
        default: break;
        }
    });

    vlay->addWidget(table);
    return w;
}

// ============================================================
// 项目操作（委托给 ProjectManager）
// ============================================================
void MainWindow::newProject()    { m_projectManager->newProject(); }
void MainWindow::openProject()
{
    const QString before = m_project ? m_project->filePath : QString();
    m_projectManager->openProject();
    if (m_project && !m_project->filePath.isEmpty() && m_project->filePath != before)
        addRecentProject(m_project->filePath);
}

void MainWindow::openRecentProject(const QString& path)
{
    const QFileInfo fi(path);
    if (!fi.exists()) {
        QMessageBox::warning(this, "Open Recent",
                             QString("Project file no longer exists:\n%1").arg(path));
        removeRecentProject(path);
        return;
    }

    if (m_projectManager->openProject(fi.absoluteFilePath()))
        addRecentProject(fi.absoluteFilePath());
}

void MainWindow::saveProject()
{
    m_projectManager->saveProject();
    if (m_project && !m_project->filePath.isEmpty())
        addRecentProject(m_project->filePath);
}

void MainWindow::saveProjectAs()
{
    const QString before = m_project ? m_project->filePath : QString();
    m_projectManager->saveProjectAs();
    if (m_project && !m_project->filePath.isEmpty() && m_project->filePath != before)
        addRecentProject(m_project->filePath);
}

QStringList MainWindow::recentProjectPaths() const
{
    QSettings settings;
    return settings.value(kRecentProjectsKey).toStringList();
}

void MainWindow::addRecentProject(const QString& path)
{
    const QFileInfo fi(path);
    if (!fi.exists())
        return;

    const QString normalized = fi.absoluteFilePath();
    QStringList paths = recentProjectPaths();
    paths.removeAll(normalized);
    paths.prepend(normalized);
    while (paths.size() > kMaxRecentProjects)
        paths.removeLast();

    QSettings settings;
    settings.setValue(kRecentProjectsKey, paths);
    updateRecentProjectsMenu();
}

void MainWindow::removeRecentProject(const QString& path)
{
    QStringList paths = recentProjectPaths();
    paths.removeAll(QFileInfo(path).absoluteFilePath());
    paths.removeAll(path);

    QSettings settings;
    settings.setValue(kRecentProjectsKey, paths);
    updateRecentProjectsMenu();
}

void MainWindow::updateRecentProjectsMenu()
{
    if (!m_recentProjectsMenu)
        return;

    m_recentProjectsMenu->clear();
    const QStringList paths = recentProjectPaths();
    if (paths.isEmpty()) {
        QAction* emptyAction = m_recentProjectsMenu->addAction("(No Recent Projects)");
        emptyAction->setEnabled(false);
        return;
    }

    for (const QString& path : paths) {
        const QFileInfo fi(path);
        QAction* action = m_recentProjectsMenu->addAction(fi.fileName());
        action->setToolTip(path);
        action->setData(path);
        connect(action, &QAction::triggered, this, [this, path] {
            openRecentProject(path);
        });
    }

    m_recentProjectsMenu->addSeparator();
    m_recentProjectsMenu->addAction("Clear Recent Projects", this, [this] {
        QSettings settings;
        settings.remove(kRecentProjectsKey);
        updateRecentProjectsMenu();
    });
}

// ============================================================
// 编译：将整个项目 PLCopen XML 转换为 ST 中间代码，显示到控制台
// ============================================================
void MainWindow::buildProject()
{
    if (!m_project) {
        m_consoleEdit->appendPlainText("[ Build ] No project loaded.");
        m_consoleTabs->setCurrentWidget(m_consoleEdit);
        return;
    }

    m_lastBuildOutput.clear();
    m_consoleEdit->clear();
    m_consoleTabs->setCurrentWidget(m_consoleEdit);

    // ── 若项目尚未保存，提示先保存 ─────────────────────────────────
    if (m_project->filePath.isEmpty()) {
        m_consoleEdit->appendPlainText(
            "[ Build ] Project not saved. Please save first (Ctrl+S).");
        statusBar()->showMessage("Build failed: unsaved project.", 4000);
        return;
    }

    m_consoleEdit->appendPlainText(
        QString("[ Build ] Building \"%1\" ...").arg(m_project->projectName));

    // ── 自动同步并保存 ──────────────────────────────────────────────
    ProjectManager::syncScenesBeforeSave(m_sceneMap);
    m_project->saveToFile(m_project->filePath);

    // ── Step 1/3: 生成 IEC 61131-3 ST ──────────────────────────────
    m_consoleEdit->appendPlainText("[ 1/5 ] Generating IEC 61131-3 ST ...");

    QString xmlContent;
    {
        QFile f(m_project->filePath);
        if (f.open(QFile::ReadOnly | QFile::Text))
            xmlContent = QString::fromUtf8(f.readAll());
    }
    if (xmlContent.isEmpty()) {
        m_consoleEdit->appendPlainText("       Error: cannot read project file.");
        statusBar()->showMessage("Build failed.", 4000);
        return;
    }

    QString stCode = StGenerator::fromXml(xmlContent);
    if (stCode.isEmpty()) {
        m_consoleEdit->appendPlainText("       Error: " + StGenerator::lastError());
        statusBar()->showMessage("Build failed.", 4000);
        return;
    }
    m_consoleEdit->appendPlainText(
        QString("       OK — %1 lines").arg(stCode.count('\n') + 1));

    // ── 查找 matiec 工具目录 ────────────────────────────────────────
    auto findMatiecDir = []() -> QString {
        QStringList candidates = {
            QString(MATIEC_DIR),    // CMake 注入的绝对路径（开发模式）
#if defined(Q_OS_MAC)
            // macOS .app bundle：Contents/MacOS → OpenPLC/tools/matiec_mac
            QDir::cleanPath(QCoreApplication::applicationDirPath()
                            + "/../../../../../tools/matiec_mac"),
#else
            QDir::cleanPath(QCoreApplication::applicationDirPath()
                            + "/../../tools/matiec_linux"),
#endif
            // Linux / non-bundle build 目录
            QDir::cleanPath(QCoreApplication::applicationDirPath()
                            + "/../../tools/matiec_mac"),
        };
        for (const QString& p : candidates) {
#if !defined(Q_OS_MAC)
            if (p.endsWith("/matiec_mac"))
                continue;
#endif
            QFileInfo iec2c(p + "/iec2c");
            QFileInfo iec2iec(p + "/iec2iec");
            if (iec2c.exists() && iec2iec.exists()
                && iec2c.isExecutable() && iec2iec.isExecutable())
                return p;
        }
        return {};
    };

    const QString matiecDir = findMatiecDir();
    if (matiecDir.isEmpty()) {
        m_consoleEdit->appendPlainText(
            "       Error: matiec tools not found.\n"
            "       Expected at: " + QString(MATIEC_DIR));
        statusBar()->showMessage("Build failed.", 4000);
        return;
    }
    const QString libDir = matiecDir + "/lib";

    // ── 准备临时构建目录 ────────────────────────────────────────────
    // 安全化项目名（仅保留字母/数字/下划线）
    QString safeProj;
    for (QChar c : m_project->projectName)
        safeProj += (c.isLetterOrNumber() ? c : QChar('_'));

    const QString buildDir = QCoreApplication::applicationDirPath() + "/output/" + safeProj;
    const QString outDir   = buildDir + "/out_c";
    QDir().mkpath(outDir);

    // 写入临时 ST 文件
    const QString stFile = buildDir + "/project.st";
    {
        QFile f(stFile);
        if (!f.open(QFile::WriteOnly | QFile::Text | QFile::Truncate)) {
            m_consoleEdit->appendPlainText("       Error: cannot write " + stFile);
            statusBar()->showMessage("Build failed.", 4000);
            return;
        }
        f.write(stCode.toUtf8());
    }

    // ── Step 2/3: iec2iec — 语法校验 ───────────────────────────────
    m_consoleEdit->appendPlainText("[ 2/5 ] Validating ST (iec2iec) ...");
    {
        QProcess proc;
        // -p: 允许前向引用  -i: 允许无参数 POU（PROGRAM 通常无 I/O 参数）
        QStringList args = {"-p", "-i", "-I", libDir, stFile};
        proc.start(matiecDir + "/iec2iec", args);
        if (!proc.waitForFinished(30000)) {
            m_consoleEdit->appendPlainText("       Error: iec2iec timed out.");
            statusBar()->showMessage("Build failed.", 4000);
            return;
        }
        const QString errOut = QString::fromUtf8(proc.readAllStandardError()).trimmed();
        if (!errOut.isEmpty())
            m_consoleEdit->appendPlainText(errOut);
        if (proc.exitCode() != 0) {
            m_consoleEdit->appendPlainText("       Validation FAILED.");
            statusBar()->showMessage("Build failed.", 4000);
            return;
        }
        m_consoleEdit->appendPlainText("       OK");
    }

    // ── Step 3/5: iec2c — 编译 ST → C ──────────────────────────────
    m_consoleEdit->appendPlainText("[ 3/5 ] Compiling to C (iec2c) ...");
    {
        QProcess proc;
        QStringList args = {"-p", "-i", "-I", libDir, "-T", outDir, stFile};
        proc.start(matiecDir + "/iec2c", args);
        if (!proc.waitForFinished(30000)) {
            m_consoleEdit->appendPlainText("       Error: iec2c timed out.");
            statusBar()->showMessage("Build failed.", 4000);
            return;
        }
        const QString errOut = QString::fromUtf8(proc.readAllStandardError()).trimmed();
        if (!errOut.isEmpty())
            m_consoleEdit->appendPlainText(errOut);
        if (proc.exitCode() != 0) {
            m_consoleEdit->appendPlainText("       Compilation FAILED.");
            statusBar()->showMessage("Build failed.", 4000);
            return;
        }
    }

    m_consoleEdit->appendPlainText("       OK");

    const QString target = m_project->targetType;  // "Linux" / "Mac" / "Embedded"

    // ── 收集 matiec 生成的 C 源文件（resource*.c 已 #include POUS.c，勿重复编译）
    QStringList iecSources = {outDir + "/config.c"};
    for (const QFileInfo& fi : QDir(outDir).entryInfoList({"resource*.c"}, QDir::Files))
        iecSources << fi.absoluteFilePath();

    // ─────────────────────────────────────────────────────────────────
    // Step 4/5 + 5/5: 通过 driver 配置生成 wrapper 并编译
    // ─────────────────────────────────────────────────────────────────

    // 查找 driver 目录：
    //   1. 优先使用 m_project->driver（用户在 Project Settings 中明确选择的 driver 名）
    //   2. 回退到 targetType 的默认映射（向后兼容）
    auto findDriverDir = [&](const QString& explicitDriver, const QString& targetType) -> QString {
        static const QMap<QString, QString> kTargetToDriver = {
            {"Mac",      "macos"},
            {"Linux",    "linux"},
            {"Embedded", "lpc824"},
        };
        QString driverName = explicitDriver.isEmpty()
            ? kTargetToDriver.value(targetType, targetType.toLower())
            : explicitDriver;
        QStringList searchPaths = {
            QCoreApplication::applicationDirPath() + "/drivers/" + driverName,
            QDir::cleanPath(QString(DRIVERS_DIR) + "/" + driverName),
        };
        for (const QString& p : searchPaths) {
            if (QFileInfo(p + "/driver.json").exists())
                return p;
        }
        return {};
    };

    // 查找 WASI-SDK 目录（XCODE 模式使用）
    auto findWasiSdkDir = []() -> QString {
        QStringList candidates = {
            qEnvironmentVariable("WASI_SDK_DIR"),
            QCoreApplication::applicationDirPath() + "/tools/wasm/wasi-sdk",
            QString(WASI_SDK_DIR),
        };
        for (const QString& p : candidates) {
            if (QFileInfo(p + "/bin/clang").exists())
                return p;
        }
        return {};
    };

    const QString driverDir = findDriverDir(m_project->driver, target);
    if (driverDir.isEmpty()) {
        m_consoleEdit->appendPlainText(
            QString("       Error: no driver found for target \"%1\".\n"
                    "       Expected in: %2").arg(target, QString(DRIVERS_DIR)));
        statusBar()->showMessage("Build failed.", 4000);
        return;
    }

    // 加载 driver.json
    QJsonObject driver, compObj;
    {
        QFile driverFile(driverDir + "/driver.json");
        if (!driverFile.open(QFile::ReadOnly)) {
            m_consoleEdit->appendPlainText("       Error: cannot read driver.json at " + driverDir);
            statusBar()->showMessage("Build failed.", 4000);
            return;
        }
        QJsonParseError err;
        QJsonDocument doc = QJsonDocument::fromJson(driverFile.readAll(), &err);
        if (doc.isNull()) {
            m_consoleEdit->appendPlainText("       Error: driver.json parse error: " + err.errorString());
            statusBar()->showMessage("Build failed.", 4000);
            return;
        }
        driver  = doc.object();
        // 根据项目编译模式（NCC/XCODE）读取对应的 compiler 子节
        const QString modeKey = m_project->mode.toLower(); // "ncc" or "xcode"
        compObj = driver["compiler"][modeKey].toObject();
        if (compObj.isEmpty()) {
            // 回退：driver 可能仍用旧的扁平 compiler 结构
            compObj = driver["compiler"].toObject();
        }
    }

    // ─────────────────────────────────────────────────────────────────
    // ══ XCODE 模式：wasi-clang → .wasm 字节码 ══
    // ─────────────────────────────────────────────────────────────────
    if (m_project->mode == "XCODE") {

        const QString wasiSdkDir = findWasiSdkDir();
        if (wasiSdkDir.isEmpty()) {
            m_consoleEdit->appendPlainText(
                "       Error: WASI-SDK not found.\n"
                "       Set WASI_SDK_DIR to a native WASI-SDK installation, or install one at:\n"
                "       " + QString(WASI_SDK_DIR));
            statusBar()->showMessage("Build failed.", 4000);
            return;
        }
        m_consoleEdit->appendPlainText(QString("       WASI-SDK: %1").arg(wasiSdkDir));

        // Step 4/5: 读取 WASM wrapper 模板
        m_consoleEdit->appendPlainText("[ 4/5 ] Generating WASM wrapper from driver template ...");
        m_consoleEdit->appendPlainText(QString("       Driver: %1  [XCODE]").arg(driver["name"].toString()));

        const QString templatePath = driverDir + "/" + compObj["template"].toString();
        QString wrapperContent;
        {
            QFile tmpl(templatePath);
            if (!tmpl.open(QFile::ReadOnly | QFile::Text)) {
                m_consoleEdit->appendPlainText("       Error: cannot read template: " + templatePath);
                statusBar()->showMessage("Build failed.", 4000);
                return;
            }
            wrapperContent = QString::fromUtf8(tmpl.readAll());
        }

        const QString outputName   = compObj["output_name"].toString("plc_program");
        const QString outputSuffix = compObj["output_suffix"].toString(".wasm");
        const QString wrapperFile  = buildDir + "/" + outputName + "_main.c";
        const QString wasmFile     = buildDir + "/" + outputName + outputSuffix;
        {
            QFile f(wrapperFile);
            if (!f.open(QFile::WriteOnly | QFile::Text | QFile::Truncate)) {
                m_consoleEdit->appendPlainText("       Error: cannot write wrapper file.");
                statusBar()->showMessage("Build failed.", 4000);
                return;
            }
            f.write(wrapperContent.toUtf8());
        }
        m_consoleEdit->appendPlainText("       OK");

        // Step 5/5: wasi-clang → .wasm
        m_consoleEdit->appendPlainText("[ 5/5 ] Compiling to WASM (wasi-clang) ...");
        {
            const QString wasiClang   = wasiSdkDir + "/bin/clang";
            const QString wasiSysroot = wasiSdkDir + "/share/wasi-sysroot";

            QProcess proc;
            QStringList args;

            // sysroot + target
            args << "--sysroot=" + wasiSysroot;

            // driver cflags（含 --target=wasm32-wasi）
            for (const QJsonValue& v : compObj["cflags"].toArray())
                args << v.toString();

            // driver include_dirs（相对 driverDir，通常为空）
            for (const QJsonValue& v : compObj["include_dirs"].toArray())
                args << "-I" << (driverDir + "/" + v.toString());

            // matiec 头文件 + iec2c 生成的头文件
            args << "-I" << matiecDir + "/lib/C"
                 << "-I" << outDir;
            // 注意：WASI sysroot 自带 time.h，不需要 -include time.h

            // 源文件
            args << wrapperFile << iecSources;

            // 输出
            args << "-o" << wasmFile;

            // driver ldflags（--no-entry / --export=...）
            for (const QJsonValue& v : compObj["ldflags"].toArray())
                args << v.toString();

            // 项目自定义标志
            if (!m_project->cflags.isEmpty())
                args << m_project->cflags.split(' ', Qt::SkipEmptyParts);
            if (!m_project->ldflags.isEmpty())
                args << m_project->ldflags.split(' ', Qt::SkipEmptyParts);

            proc.start(wasiClang, args);
            if (!proc.waitForFinished(60000)) {
                m_consoleEdit->appendPlainText("       Error: wasi-clang timed out.");
                statusBar()->showMessage("Build failed.", 4000);
                return;
            }
            const QString ccOut = QString::fromUtf8(proc.readAllStandardOutput()).trimmed();
            const QString ccErr = QString::fromUtf8(proc.readAllStandardError()).trimmed();
            if (!ccOut.isEmpty()) m_consoleEdit->appendPlainText(ccOut);
            if (!ccErr.isEmpty()) m_consoleEdit->appendPlainText(ccErr);
            if (proc.exitCode() != 0) {
                m_consoleEdit->appendPlainText("       WASM compilation FAILED.");
                statusBar()->showMessage("Build failed.", 4000);
                return;
            }
        }

        const QString imageFile = buildDir + "/" + outputName + ".xcode.bin";
        {
            QFile wasmIn(wasmFile);
            if (!wasmIn.open(QFile::ReadOnly)) {
                m_consoleEdit->appendPlainText("       Error: cannot read WASM output.");
                statusBar()->showMessage("Build failed.", 4000);
                return;
            }
            const QByteArray wasmBytes = wasmIn.readAll();

            QFile imageOut(imageFile);
            if (!imageOut.open(QFile::WriteOnly | QFile::Truncate)) {
                m_consoleEdit->appendPlainText("       Error: cannot write XCODE download image.");
                statusBar()->showMessage("Build failed.", 4000);
                return;
            }
            QDataStream ds(&imageOut);
            ds.setByteOrder(QDataStream::LittleEndian);
            ds << quint32(0x57415300u); // XCODE_WASM_MAGIC, "WAS\0"
            ds << quint32(wasmBytes.size());
            imageOut.write(wasmBytes);
        }

        qint64 wasmSize = QFileInfo(wasmFile).size();
        qint64 imageSize = QFileInfo(imageFile).size();
        const QJsonObject memMap = driver["memory_map"].toObject();
        const qint64 maxImageSize = qint64(memMap["user_flash_size_kb"].toInt(0)) * 1024;
        if (maxImageSize > 0 && imageSize > maxImageSize) {
            m_consoleEdit->appendPlainText(
                QString("       Error: XCODE image too large: %1 bytes / %2 max")
                    .arg(imageSize).arg(maxImageSize));
            statusBar()->showMessage("Build failed: output too large.", 4000);
            return;
        }

        m_lastBuildOutput = imageFile;
        m_consoleEdit->appendPlainText("─────────────────────────────────────────");
        m_consoleEdit->appendPlainText(
            QString("[ Build ] SUCCESS  -->  %1  (%2 bytes, wasm %3 bytes)")
            .arg(QFileInfo(imageFile).fileName()).arg(imageSize).arg(wasmSize));
        statusBar()->showMessage(
            QString("Build complete -- %1 (%2 bytes)")
            .arg(QFileInfo(imageFile).fileName()).arg(imageSize), 5000);
        return;
    }

    // ─────────────────────────────────────────────────────────────────
    // ══ NCC 模式：原生编译（arm-gcc / gcc）══
    // ─────────────────────────────────────────────────────────────────

    // Step 4/5: 读取 wrapper 模板，写入构建目录
    m_consoleEdit->appendPlainText("[ 4/5 ] Generating wrapper from driver template ...");
    m_consoleEdit->appendPlainText(QString("       Driver: %1  [NCC]").arg(driver["name"].toString()));

    const QString templatePath = driverDir + "/" + compObj["template"].toString();
    QString wrapperContent;
    {
        QFile tmpl(templatePath);
        if (!tmpl.open(QFile::ReadOnly | QFile::Text)) {
            m_consoleEdit->appendPlainText("       Error: cannot read template: " + templatePath);
            statusBar()->showMessage("Build failed.", 4000);
            return;
        }
        wrapperContent = QString::fromUtf8(tmpl.readAll());
    }

    const QString outputName   = compObj["output_name"].toString("plc_program");
    const QString outputSuffix = compObj["output_suffix"].toString();
    const QString wrapperFile  = buildDir + "/" + outputName + "_main.c";
    {
        QFile f(wrapperFile);
        if (!f.open(QFile::WriteOnly | QFile::Text | QFile::Truncate)) {
            m_consoleEdit->appendPlainText("       Error: cannot write wrapper file.");
            statusBar()->showMessage("Build failed.", 4000);
            return;
        }
        f.write(wrapperContent.toUtf8());
    }
    m_consoleEdit->appendPlainText("       OK");

    // Step 5/5: 编译
    const QString cc      = compObj["cc"].toString("gcc");
    const QString elfFile = buildDir + "/" + outputName + outputSuffix;
    m_consoleEdit->appendPlainText(
        QString("[ 5/5 ] Compiling for \"%1\" (%2) ...").arg(target, cc));
    {
        QProcess proc;
        QStringList args;

        // driver 定义的 cflags
        for (const QJsonValue& v : compObj["cflags"].toArray())
            args << v.toString();

        // driver 定义的 include_dirs（相对于 driverDir）
        for (const QJsonValue& v : compObj["include_dirs"].toArray())
            args << "-I" << (driverDir + "/" + v.toString());

        // matiec lib/C 头文件 + iec2c 生成的头文件
        args << "-I" << matiecDir + "/lib/C"
             << "-I" << outDir
             << "-include" << "time.h";

        // linker script（可选，Embedded 专用）
        const QString ldRel = compObj["linker_script"].toString();
        if (!ldRel.isEmpty())
            args << "-Wl,-T," + driverDir + "/" + ldRel;

        // 源文件：wrapper + matiec 生成的 C 文件
        args << wrapperFile << iecSources;

        // 输出
        args << "-o" << elfFile;

        // driver 定义的 ldflags
        for (const QJsonValue& v : compObj["ldflags"].toArray())
            args << v.toString();

        // 项目级别自定义标志（可覆盖 driver 默认值）
        if (!m_project->cflags.isEmpty())
            args << m_project->cflags.split(' ', Qt::SkipEmptyParts);
        if (!m_project->ldflags.isEmpty())
            args << m_project->ldflags.split(' ', Qt::SkipEmptyParts);

        proc.start(cc, args);
        if (!proc.waitForFinished(60000)) {
            m_consoleEdit->appendPlainText("       Error: compiler timed out.");
            statusBar()->showMessage("Build failed.", 4000);
            return;
        }
        const QString ccOut = QString::fromUtf8(proc.readAllStandardOutput()).trimmed();
        const QString ccErr = QString::fromUtf8(proc.readAllStandardError()).trimmed();
        if (!ccOut.isEmpty()) m_consoleEdit->appendPlainText(ccOut);
        if (!ccErr.isEmpty()) m_consoleEdit->appendPlainText(ccErr);
        if (proc.exitCode() != 0) {
            m_consoleEdit->appendPlainText("       Compilation FAILED.");
            statusBar()->showMessage("Build failed.", 4000);
            return;
        }
    }

    // post_build（可选，如 Embedded 的 objcopy .elf -> .bin）
    // 在新的嵌套 compiler 结构中，post_build 在 compiler.ncc 内；
    // 旧扁平结构中在顶层 driver。两处都检查，优先 compObj。
    QString finalOutput = elfFile;
    if (compObj.contains("post_build") || driver.contains("post_build")) {
        QJsonObject pb = compObj.contains("post_build")
            ? compObj["post_build"].toObject()
            : driver["post_build"].toObject();
        const QString objcopy   = pb["objcopy"].toString();
        const QString format    = pb["format"].toString("binary");
        const QString binSuffix = pb["output_suffix"].toString(".bin");
        const QString binFile   = buildDir + "/" + outputName + binSuffix;

        m_consoleEdit->appendPlainText(
            QString("       Post-build: %1 -O %2 ...").arg(objcopy, format));
        {
            QProcess proc;
            proc.start(objcopy, {"-O", format, elfFile, binFile});
            if (!proc.waitForFinished(15000)) {
                m_consoleEdit->appendPlainText("       Error: " + objcopy + " timed out.");
                statusBar()->showMessage("Build failed.", 4000);
                return;
            }
            if (proc.exitCode() != 0) {
                const QString err = QString::fromUtf8(proc.readAllStandardError()).trimmed();
                m_consoleEdit->appendPlainText("       " + objcopy + " FAILED: " + err);
                statusBar()->showMessage("Build failed.", 4000);
                return;
            }
        }
        finalOutput = binFile;

        // 显示 .bin 大小与容量限制
        qint64 sz    = QFileInfo(binFile).size();
        qint64 maxSz = pb["max_size_bytes"].toInteger(0);
        if (maxSz > 0 && sz > maxSz) {
            m_consoleEdit->appendPlainText(
                QString("       Error: output too large: %1 bytes / %2 max")
                    .arg(sz).arg(maxSz));
            statusBar()->showMessage("Build failed: output too large.", 4000);
            return;
        }
        QString sizeStr = maxSz > 0
            ? QString("%1 bytes / %2 max").arg(sz).arg(maxSz)
            : QString("%1 bytes").arg(sz);
        m_consoleEdit->appendPlainText(
            QString("       %1%2  (%3)").arg(outputName, binSuffix, sizeStr));
    }

    m_consoleEdit->appendPlainText("─────────────────────────────────────────");
    m_consoleEdit->appendPlainText(
        QString("[ Build ] SUCCESS  -->  %1").arg(finalOutput));
    m_lastBuildOutput = finalOutput;
    statusBar()->showMessage(
        QString("Build complete -- %1").arg(QFileInfo(finalOutput).fileName()), 5000);
}

// ============================================================
// 下载：打开 DownloadDialog
// ============================================================
void MainWindow::downloadProject()
{
    if (m_connState != PlcConnState::Connected) {
        QMessageBox::information(this, "Download", "Please connect to a PLC before downloading.");
        statusBar()->showMessage("Not connected to PLC.", 3000);
        return;
    }

    if (m_plcUri == "SIM://SmartSim") {
        if (!m_project) {
            QMessageBox::warning(this, "SmartSim", "Please create or open a project first.");
            return;
        }
        if (m_project->filePath.isEmpty()) {
            QMessageBox::warning(this, "SmartSim", "Please save the project before downloading to simulation.");
            return;
        }

        ProjectManager::syncScenesBeforeSave(m_sceneMap);
        m_project->saveToFile(m_project->filePath);

        if (!m_smartSimWindow)
            openSmartSim();
        if (!m_smartSimWindow)
            return;

        statusBar()->showMessage("Downloading program to SmartSim...", 3000);
        if (m_smartSimWindow->downloadProgram()) {
            setPlcConnState(PlcConnState::Connected);
            setPlcRunState(PlcRunState::Stopped);
            statusBar()->showMessage("Program downloaded to SmartSim.", 3000);
        } else {
            setPlcRunState(PlcRunState::Error);
            statusBar()->showMessage("SmartSim download failed.", 5000);
        }
        return;
    }

    DownloadDialog dlg(this);
    if (!m_lastBuildOutput.isEmpty() && QFileInfo::exists(m_lastBuildOutput)) {
        dlg.setBinaryPath(m_lastBuildOutput);
    } else {
        statusBar()->showMessage("No recent build output. Select a binary manually or build first.", 5000);
    }
    dlg.exec();
}

// ============================================================
// Driver 安装：解析 TiZi .cab 包并解压到 <appDir>/drivers/
// ============================================================
//
// .cab 二进制格式（由 tools/pack_driver.py 生成）：
//   [4B]  Magic  b'TZDR'
//   [1B]  Version (== 1)
//   [4B]  Manifest JSON 长度 (uint32 LE)
//   [N]   Manifest JSON (UTF-8)
//   [4B]  文件数量 (uint32 LE)
//   对每个文件：
//     [2B]  相对路径长度 (uint16 LE)
//     [N]   相对路径 (UTF-8, '/' 分隔)
//     [4B]  数据长度 (uint32 LE)
//     [N]   文件内容
//
void MainWindow::installDriverCab(const QString& cabPath)
{
    // ── 打开文件 ──────────────────────────────────────────────
    QFile f(cabPath);
    if (!f.open(QFile::ReadOnly)) {
        QMessageBox::critical(this, "Driver Install",
            "Cannot open file:\n" + cabPath);
        return;
    }

    QDataStream ds(&f);
    ds.setByteOrder(QDataStream::LittleEndian);

    // ── 校验 Magic ────────────────────────────────────────────
    char magic[4] = {};
    if (ds.readRawData(magic, 4) != 4 || QByteArray(magic, 4) != "TZDR") {
        QMessageBox::critical(this, "Driver Install",
            "Not a valid TiZi driver package.\n(Bad magic bytes)");
        return;
    }

    // ── 校验版本 ──────────────────────────────────────────────
    quint8 version = 0;
    ds >> version;
    if (version != 1) {
        QMessageBox::critical(this, "Driver Install",
            QString("Unsupported package version: %1\n"
                    "Please update TiZi to install this driver.").arg(version));
        return;
    }

    // ── 读取 Manifest JSON ────────────────────────────────────
    quint32 manifestLen = 0;
    ds >> manifestLen;
    QByteArray manifestBytes(static_cast<int>(manifestLen), '\0');
    if (ds.readRawData(manifestBytes.data(), static_cast<int>(manifestLen))
            != static_cast<int>(manifestLen)) {
        QMessageBox::critical(this, "Driver Install",
            "Corrupted package (manifest read error).");
        return;
    }
    QJsonParseError jerr;
    QJsonDocument manifestDoc = QJsonDocument::fromJson(manifestBytes, &jerr);
    if (manifestDoc.isNull()) {
        QMessageBox::critical(this, "Driver Install",
            "Corrupted package (bad manifest JSON):\n" + jerr.errorString());
        return;
    }
    const QJsonObject manifest  = manifestDoc.object();
    const QString dirName       = manifest["dir_name"].toString();
    const QString driverName    = manifest["name"].toString();
    const QString description   = manifest["description"].toString();

    // mode 可能是字符串或数组，统一转为可读字符串
    QString modeStr;
    if (manifest["mode"].isArray())
        modeStr = QString::fromUtf8(
            QJsonDocument(manifest["mode"].toArray())
            .toJson(QJsonDocument::Compact));
    else
        modeStr = manifest["mode"].toString();

    if (dirName.isEmpty()) {
        QMessageBox::critical(this, "Driver Install",
            "Corrupted package (missing dir_name in manifest).");
        return;
    }

    // ── 读取文件数量 ──────────────────────────────────────────
    quint32 fileCount = 0;
    ds >> fileCount;

    // ── 确认安装路径 ──────────────────────────────────────────
    const QString installBase = QCoreApplication::applicationDirPath() + "/drivers";
    const QString installDir  = installBase + "/" + dirName;

    QString confirmMsg =
        QString("Install driver:\n\n"
                "  Name:  %1\n"
                "  Mode:  %2\n"
                "  Files: %3\n\n"
                "Install to:\n  %4\n\n"
                "Continue?")
        .arg(driverName, modeStr)
        .arg(fileCount)
        .arg(installDir);

    if (!description.isEmpty())
        confirmMsg.prepend(description + "\n\n");

    const auto btn = QMessageBox::question(this, "Install Driver", confirmMsg);
    if (btn != QMessageBox::Yes)
        return;

    // ── 解压文件 ──────────────────────────────────────────────
    QDir().mkpath(installDir);
    int extracted = 0;

    for (quint32 i = 0; i < fileCount; i++) {
        quint16 pathLen = 0;
        ds >> pathLen;
        QByteArray pathBytes(static_cast<int>(pathLen), '\0');
        ds.readRawData(pathBytes.data(), static_cast<int>(pathLen));
        const QString relPath = QString::fromUtf8(pathBytes);

        quint32 dataLen = 0;
        ds >> dataLen;
        QByteArray data(static_cast<int>(dataLen), '\0');
        ds.readRawData(data.data(), static_cast<int>(dataLen));

        // 防止路径穿越（path traversal）
        const QString fullPath = QDir::cleanPath(installDir + "/" + relPath);
        if (!fullPath.startsWith(installDir)) {
            QMessageBox::critical(this, "Driver Install",
                QString("Security: illegal path in package: %1").arg(relPath));
            return;
        }

        QDir().mkpath(QFileInfo(fullPath).absolutePath());
        QFile outFile(fullPath);
        if (!outFile.open(QFile::WriteOnly | QFile::Truncate)) {
            QMessageBox::critical(this, "Driver Install",
                "Failed to write file:\n" + fullPath);
            return;
        }
        outFile.write(data);
        extracted++;
    }

    // ── 安装成功 ──────────────────────────────────────────────
    m_consoleEdit->appendPlainText(
        QString("[ Driver ] Installed \"%1\"  →  %2  (%3 files)")
        .arg(driverName, installDir).arg(extracted));
    m_consoleTabs->setCurrentWidget(m_consoleEdit);
    statusBar()->showMessage(
        QString("Driver \"%1\" installed.").arg(driverName), 4000);

    QMessageBox::information(this, "Driver Install",
        QString("Driver \"%1\" installed successfully.\n\n"
                "Location: %2\n"
                "Files: %3\n\n"
                "Reopen Project Settings to use the new driver.")
        .arg(driverName, installDir).arg(extracted));
}

// ============================================================
// 状态栏：PLC 连接状态指示器
// ============================================================

// 生成圆形 LED 的 QLabel stylesheet
QString MainWindow::ledStyle(const QString& color)
{
    return QString(
        "background-color: %1;"
        "border-radius: 6px;"
        "border: 1px solid rgba(0,0,0,0.25);"
    ).arg(color);
}

QString MainWindow::debugValueText(const QVariant& value)
{
    if (value.metaType().id() == QMetaType::Bool)
        return value.toBool() ? "true" : "false";
    if (value.canConvert<double>())
        return QString::number(value.toDouble(), 'g', 9);
    return value.toString();
}

QVariant MainWindow::parseDebugForceValue(const QString& type, const QString& text, bool* ok)
{
    if (ok)
        *ok = false;

    const QString normalizedType = type.trimmed().toUpper();
    const QString normalizedText = text.trimmed().toLower();
    if (normalizedType == "BOOL") {
        if (normalizedText == "true" || normalizedText == "1" || normalizedText == "on") {
            if (ok)
                *ok = true;
            return true;
        }
        if (normalizedText == "false" || normalizedText == "0" || normalizedText == "off") {
            if (ok)
                *ok = true;
            return false;
        }
        return {};
    }

    if (normalizedType == "INT" || normalizedType == "DINT") {
        bool converted = false;
        const qlonglong value = text.trimmed().toLongLong(&converted);
        if (ok)
            *ok = converted;
        return converted ? QVariant(value) : QVariant();
    }

    bool converted = false;
    const double value = text.trimmed().toDouble(&converted);
    if (ok)
        *ok = converted;
    return converted ? QVariant(value) : QVariant();
}

void MainWindow::setupStatusBar()
{
    // ── 连接状态区（左侧组）──────────────────────────────────
    auto* connFrame = new QFrame();
    connFrame->setFrameShape(QFrame::StyledPanel);
    connFrame->setContentsMargins(0, 0, 0, 0);
    auto* connLay = new QHBoxLayout(connFrame);
    connLay->setContentsMargins(6, 1, 6, 1);
    connLay->setSpacing(5);

    // 小图标（插头）
    auto* connIcon = new QLabel();
    connIcon->setPixmap(QPixmap(":/images/Connect.png")
                            .scaled(14, 14, Qt::KeepAspectRatio,
                                    Qt::SmoothTransformation));
    connLay->addWidget(connIcon);

    // LED
    m_connLed = new QLabel();
    m_connLed->setFixedSize(12, 12);
    m_connLed->setStyleSheet(ledStyle("#888888"));
    connLay->addWidget(m_connLed);

    // 文字
    m_connLabel = new QLabel("Disconnected");
    m_connLabel->setMinimumWidth(100);
    connLay->addWidget(m_connLabel);

    // PLC URI（连接后显示）
    m_uriLabel = new QLabel();
    m_uriLabel->setStyleSheet("color: #666666; font-size: 10px;");
    m_uriLabel->hide();
    connLay->addWidget(m_uriLabel);

    // 点击整个区域 → 连接/断开对话框
    connFrame->setCursor(Qt::PointingHandCursor);
    connFrame->installEventFilter(this);
    connFrame->setProperty("plcConnFrame", true);

    // ── 分隔线 ──────────────────────────────────────────────
    auto* sep = new QFrame();
    sep->setFrameShape(QFrame::VLine);
    sep->setFrameShadow(QFrame::Sunken);

    // ── 运行状态区 ──────────────────────────────────────────
    auto* stateFrame = new QFrame();
    stateFrame->setFrameShape(QFrame::StyledPanel);
    auto* stateLay = new QHBoxLayout(stateFrame);
    stateLay->setContentsMargins(6, 1, 6, 1);
    stateLay->setSpacing(5);

    auto* stateIcon = new QLabel("PLC:");
    stateIcon->setStyleSheet("font-weight: bold; font-size: 10px;");
    stateLay->addWidget(stateIcon);

    m_stateLed = new QLabel();
    m_stateLed->setFixedSize(12, 12);
    m_stateLed->setStyleSheet(ledStyle("#888888"));
    stateLay->addWidget(m_stateLed);

    m_stateLabel = new QLabel("Unknown");
    m_stateLabel->setMinimumWidth(70);
    stateLay->addWidget(m_stateLabel);

    // ── 添加到状态栏（永久控件显示在右侧）──────────────────
    statusBar()->addPermanentWidget(connFrame);
    statusBar()->addPermanentWidget(sep);
    statusBar()->addPermanentWidget(stateFrame);

    // 初始状态
    setPlcConnState(PlcConnState::Disconnected);
    setPlcRunState(PlcRunState::Unknown);
}

// ── 更新连接状态 LED + 文字 ───────────────────────────────────
void MainWindow::setPlcConnState(PlcConnState state)
{
    m_connState = state;
    struct { const char* color; const char* text; } cfg[] = {
        { "#888888", "Disconnected" },   // Disconnected
        { "#FFC107", "Connecting…"  },   // Connecting
        { "#4CAF50", "Connected"    },   // Connected
    };
    int i = static_cast<int>(state);
    m_connLed->setStyleSheet(ledStyle(cfg[i].color));
    m_connLabel->setText(cfg[i].text);

    // URI 仅在已连接时显示
    if (state == PlcConnState::Connected && !m_plcUri.isEmpty()) {
        m_uriLabel->setText(m_plcUri);
        m_uriLabel->show();
    } else {
        m_uriLabel->hide();
    }

    // 工具栏按钮：已连接时切换图标 + 启用 Run/Stop/Transfer
    const bool connected = (state == PlcConnState::Connected);
    if (m_aConnect) {
        m_aConnect->setIcon(makeLdIcon(connected ? "disconnect" : "connect"));
        m_aConnect->setToolTip(connected
            ? "Disconnect from PLC  [Ctrl+D]"
            : "Connect to PLC  [Ctrl+D]");
    }
    if (m_aRun)  m_aRun->setEnabled(connected);
    if (m_aStop) m_aStop->setEnabled(connected);
    if (m_aTransfer) m_aTransfer->setEnabled(connected);

    // 未连接时重置运行状态
    if (state == PlcConnState::Disconnected)
        setPlcRunState(PlcRunState::Unknown);
}

// ── 更新运行状态 LED + 文字 ───────────────────────────────────
void MainWindow::setPlcRunState(PlcRunState state)
{
    m_runState = state;
    struct { const char* color; const char* text; } cfg[] = {
        { "#888888", "Unknown" },   // Unknown
        { "#FF7043", "Stopped" },   // Stopped
        { "#4CAF50", "Running" },   // Running
        { "#FFC107", "Paused"  },   // Paused
        { "#F44336", "Error"   },   // Error
    };
    int i = static_cast<int>(state);
    m_stateLed->setStyleSheet(ledStyle(cfg[i].color));
    m_stateLabel->setText(cfg[i].text);
}

// ── 过滤事件：点击连接区域 → 打开连接对话框 ──────────────────
bool MainWindow::eventFilter(QObject* obj, QEvent* ev)
{
    if (obj->property("plcConnFrame").toBool() &&
        ev->type() == QEvent::MouseButtonRelease)
    {
        connectToPlc();
        return true;
    }
    return QMainWindow::eventFilter(obj, ev);
}

// ── 连接 / 断开 PLC 对话框（存根，待实现真正协议）─────────────
void MainWindow::connectToPlc()
{
    if (m_connState == PlcConnState::Connected) {
        // 已连接 → 断开
        int ret = QMessageBox::question(
            this, "Disconnect",
            QString("Disconnect from %1?").arg(
                m_plcUri.isEmpty() ? "PLC" : m_plcUri),
            QMessageBox::Yes | QMessageBox::No);
        if (ret == QMessageBox::Yes) {
            const bool disconnectingSim = (m_plcUri == "SIM://SmartSim");
            if (disconnectingSim && m_smartSimWindow)
                m_smartSimWindow->close();
            m_plcUri.clear();
            setPlcConnState(PlcConnState::Disconnected);
            setPlcRunState(PlcRunState::Unknown);
            statusBar()->showMessage("Disconnected from PLC.", 3000);
        }
        return;
    }

    // 未连接 → 弹出连接对话框
    QDialog dlg(this);
    dlg.setWindowTitle("Connect to PLC");
    dlg.setFixedWidth(380);

    QFormLayout form(&dlg);

    auto* targetCombo = new QComboBox(&dlg);
    targetCombo->addItem("SmartSim (PC Simulation)", "sim");
    targetCombo->addItem("Custom PLC URI", "uri");
    form.addRow("PLC:", targetCombo);

    const QString defaultUri =
        (!m_plcUri.isEmpty() && m_plcUri != "SIM://SmartSim")
            ? m_plcUri
            : QStringLiteral("PYRO://localhost:61131");
    auto* uriEdit = new QLineEdit(defaultUri, &dlg);
    form.addRow("PLC URI:", uriEdit);

    const int defaultIndex = (m_plcUri == "SIM://SmartSim") ? 0 : 1;
    targetCombo->setCurrentIndex(defaultIndex);
    uriEdit->setEnabled(defaultIndex == 1);
    connect(targetCombo,
            QOverload<int>::of(&QComboBox::currentIndexChanged),
            &dlg,
            [targetCombo, uriEdit](int) {
        const bool customUri = (targetCombo->currentData().toString() == "uri");
        uriEdit->setEnabled(customUri);
        if (customUri)
            uriEdit->setFocus();
    });

    QDialogButtonBox btns(QDialogButtonBox::Ok | QDialogButtonBox::Cancel,
                          Qt::Horizontal, &dlg);
    form.addRow(&btns);
    connect(&btns, &QDialogButtonBox::accepted, &dlg, &QDialog::accept);
    connect(&btns, &QDialogButtonBox::rejected, &dlg, &QDialog::reject);

    if (dlg.exec() != QDialog::Accepted) return;

    if (targetCombo->currentData().toString() == "sim") {
        if (!m_project) {
            QMessageBox::warning(this, "SmartSim", "Please create or open a project first.");
            return;
        }
        if (m_project->filePath.isEmpty()) {
            QMessageBox::warning(this, "SmartSim", "Please save the project before starting simulation.");
            return;
        }

        openSmartSim();
        m_plcUri = "SIM://SmartSim";
        setPlcConnState(PlcConnState::Connected);
        setPlcRunState(PlcRunState::Stopped);
        statusBar()->showMessage("Connected to SmartSim.", 3000);
        return;
    }

    const QString uri = uriEdit->text().trimmed();
    if (uri.isEmpty()) return;

    m_plcUri = uri;

    // 模拟连接过程（真实协议在此接入）
    setPlcConnState(PlcConnState::Connecting);
    statusBar()->showMessage(QString("Connecting to %1…").arg(uri), 2000);

    // 延迟 800ms 模拟握手，然后显示已连接
    // （真实实现中：在此发起网络连接，回调时更新状态）
    QTimer::singleShot(800, this, [this]() {
        setPlcConnState(PlcConnState::Connected);
        setPlcRunState(PlcRunState::Stopped);
        statusBar()->showMessage(
            QString("Connected to %1").arg(m_plcUri), 3000);
    });
}

// ============================================================
// 视图缩放
// ============================================================

// 从当前活跃的 MDI 子窗口中找到 LadderView
LadderView* MainWindow::activeView() const
{
    QMdiSubWindow* sw = m_mdiArea ? m_mdiArea->activeSubWindow() : nullptr;
    if (!sw) return nullptr;
    return sw->findChild<LadderView*>();
}

void MainWindow::zoomIn()
{
    if (LadderView* v = activeView()) {
        // 限制最大缩放 5×
        if (v->transform().m11() < 5.0)
            v->scale(1.25, 1.25);
    }
}

void MainWindow::zoomOut()
{
    if (LadderView* v = activeView()) {
        // 限制最小缩放 0.05×
        if (v->transform().m11() > 0.05)
            v->scale(1.0 / 1.25, 1.0 / 1.25);
    }
}

void MainWindow::zoomFit()
{
    LadderView* v = activeView();
    if (!v || !v->scene()) return;

    QRectF r = v->scene()->itemsBoundingRect().adjusted(-40, -40, 40, 40);
    if (r.isEmpty()) r = QRectF(0, 0, 800, 600);
    v->fitInView(r, Qt::KeepAspectRatio);
}

// ============================================================
// 窗口标题：项目名 [*] — TiZi
// ============================================================
void MainWindow::updateWindowTitle()
{
    if (!m_project) {
        setWindowTitle("TiZi PLC Editor");
        return;
    }
    const QString dirty = m_project->isDirty() ? " *" : "";
    setWindowTitle(QString("%1%2 — TiZi PLC Editor")
                       .arg(m_project->projectName)
                       .arg(dirty));
}

// ============================================================
// 主题切换
// ============================================================
void MainWindow::applyTheme(const QString& qrcPath)
{
    QFile f(qrcPath);
    if (!f.open(QFile::ReadOnly)) return;

    qApp->setStyleSheet(QString::fromUtf8(f.readAll()));
    m_currentTheme = qrcPath;

    // 同步 QPalette —— macOS 上 QTreeWidget viewport 直接读 palette，不读 QSS
    const bool dark = qrcPath.contains("dark_theme");
    QPalette pal = qApp->palette();
    if (dark) {
        pal.setColor(QPalette::Window,          QColor("#1E1E1E"));
        pal.setColor(QPalette::WindowText,      QColor("#D4D4D4"));
        pal.setColor(QPalette::Base,            QColor("#252526"));
        pal.setColor(QPalette::AlternateBase,   QColor("#1E1E1E"));
        pal.setColor(QPalette::Text,            QColor("#D4D4D4"));
        pal.setColor(QPalette::Button,          QColor("#3C3C3C"));
        pal.setColor(QPalette::ButtonText,      QColor("#D4D4D4"));
        pal.setColor(QPalette::Highlight,       QColor("#094771"));
        pal.setColor(QPalette::HighlightedText, QColor("#FFFFFF"));
        pal.setColor(QPalette::ToolTipBase,     QColor("#252526"));
        pal.setColor(QPalette::ToolTipText,     QColor("#D4D4D4"));
        pal.setColor(QPalette::PlaceholderText, QColor("#6E6E6E"));
    } else {
        pal.setColor(QPalette::Window,          QColor("#F0F0F0"));
        pal.setColor(QPalette::WindowText,      QColor("#1A1A1A"));
        pal.setColor(QPalette::Base,            QColor("#FFFFFF"));
        pal.setColor(QPalette::AlternateBase,   QColor("#F7F9FC"));
        pal.setColor(QPalette::Text,            QColor("#1A1A1A"));
        pal.setColor(QPalette::Button,          QColor("#E8E8E8"));
        pal.setColor(QPalette::ButtonText,      QColor("#1A1A1A"));
        pal.setColor(QPalette::Highlight,       QColor("#0078D7"));
        pal.setColor(QPalette::HighlightedText, QColor("#FFFFFF"));
        pal.setColor(QPalette::ToolTipBase,     QColor("#FFFFFF"));
        pal.setColor(QPalette::ToolTipText,     QColor("#1A1A1A"));
        pal.setColor(QPalette::PlaceholderText, QColor("#999999"));
    }
    qApp->setPalette(pal);

    // 强制所有顶层及子控件刷新 palette（macOS 有时需要显式通知）
    for (QWidget* w : qApp->allWidgets())
        w->setPalette(pal);

    // 通知所有已打开的图形场景重绘背景
    for (PlcOpenViewer* scene : m_sceneMap)
        scene->update();
}
