/*  Copyright (C) 2017 Eric Wasylishen

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA

See file, 'COPYING', for details.
*/

#include "mainwindow.h"

#include <QClipboard>
#include <QCoreApplication>
#include <QDockWidget>
#include <QString>
#include <QDragEnterEvent>
#include <QMimeData>
#include <QFileSystemWatcher>
#include <QFileInfo>
#include <QInputDialog>
#include <QFormLayout>
#include <QLineEdit>
#include <QSplitter>
#include <QCheckBox>
#include <QPushButton>
#include <QSlider>
#include <QSettings>
#include <QMenu>
#include <QMenuBar>
#include <QFileDialog>
#include <QGroupBox>
#include <QRadioButton>
#include <QTimer>
#include <QScrollArea>
#include <QSpinBox>
#include <QScrollBar>
#include <QFrame>
#include <QLabel>
#include <QTextEdit>
#include <QStatusBar>
#include <QStringList>
#include <QThread>
#include <QApplication>
#include <QDesktopServices>

#include <common/bspfile.hh>
#include <common/litfile.hh>
#include <qbsp/qbsp.hh>
#include <vis/vis.hh>
#include <light/light.hh>
#include <common/bspinfo.hh>
#include <fmt/chrono.h>
#include <QMessageBox>

#include "glview.h"
#include "stats.h"
#include "face.h"

// Recent files

static constexpr auto RECENT_SETTINGS_KEY = "recent_files";
static constexpr size_t MAX_RECENTS = 10;

static void ClearRecents()
{
    QSettings s;
    s.setValue(RECENT_SETTINGS_KEY, QStringList());
}

/**
 * Updates the recent files settings by pushing the given file to the front
 * and trimming the list to SETTINGS_MAX.
 *
 * @param file the file to push
 * @return the new recent files list
 */
static QStringList AddRecent(const QString &file)
{
    QSettings s;
    QStringList recents = s.value(RECENT_SETTINGS_KEY).toStringList();

    recents.removeOne(file); // no-op if not present
    recents.push_front(file);
    while (recents.size() > MAX_RECENTS) {
        recents.pop_back();
    }

    s.setValue(RECENT_SETTINGS_KEY, recents);

    return recents;
}

static QStringList GetRecents()
{
    QSettings s;
    QStringList recents = s.value(RECENT_SETTINGS_KEY).toStringList();
    return recents;
}

// Camera Bookmarks

static constexpr auto CAMERA_BOOKMARKS_SETTINGS_KEY = "camera_bookmarks";
static constexpr size_t MAX_CAMERA_BOOKMARKS = 10;

struct camera_bookmark_t
{
    qvec3f origin;
    qvec3f forward;
};

static void ClearCameraBookmarks()
{
    QSettings s;
    s.setValue(CAMERA_BOOKMARKS_SETTINGS_KEY, QStringList());
}

static QString CameraBookmarkToQString(const camera_bookmark_t &b)
{
    return QString("%1 %2 %3 %4 %5 %6")
        .arg(b.origin[0])
        .arg(b.origin[1])
        .arg(b.origin[2])
        .arg(b.forward[0])
        .arg(b.forward[1])
        .arg(b.forward[2]);
}

static std::optional<camera_bookmark_t> CameraBookmarkFromQString(const QString &string)
{
    QStringList parts = string.split(' ', Qt::SkipEmptyParts);
    if (parts.length() != 6)
        return std::nullopt;

    camera_bookmark_t result;
    result.origin[0] = parts[0].toFloat();
    result.origin[1] = parts[1].toFloat();
    result.origin[2] = parts[2].toFloat();

    result.forward[0] = parts[3].toFloat();
    result.forward[1] = parts[4].toFloat();
    result.forward[2] = parts[5].toFloat();

    return {result};
}

/**
 * Updates the recent camera bookmarks settings by pushing the given camera pos to the front
 * and trimming the list to MAX_CAMERA_BOOKMARKS.
 */
static void AddCameraBookmark(const camera_bookmark_t &b)
{
    QSettings s;
    QStringList qt_strings = s.value(CAMERA_BOOKMARKS_SETTINGS_KEY).toStringList();

    qt_strings.push_front(CameraBookmarkToQString(b));

    while (qt_strings.size() > MAX_CAMERA_BOOKMARKS) {
        qt_strings.pop_back();
    }

    s.setValue(CAMERA_BOOKMARKS_SETTINGS_KEY, qt_strings);
}

static std::vector<camera_bookmark_t> GetCameraBookmarks()
{
    QSettings s;
    QStringList qt_strings = s.value(CAMERA_BOOKMARKS_SETTINGS_KEY).toStringList();

    std::vector<camera_bookmark_t> result;
    for (const QString &qt_string : qt_strings) {
        if (auto parsed = CameraBookmarkFromQString(qt_string); parsed) {
            result.push_back(*parsed);
        }
    }
    return result;
}

// ETLogWidget
ETLogWidget::ETLogWidget(QWidget *parent)
    : QTabWidget(parent)
{
    for (size_t i = 0; i < std::size(logTabNames); i++) {
        m_textEdits[i] = new QTextEdit();

        auto *formLayout = new QFormLayout();
        auto *form = new QWidget();
        formLayout->addRow(m_textEdits[i]);
        form->setLayout(formLayout);
        setTabText(i, logTabNames[i]);
        addTab(form, logTabNames[i]);
        formLayout->setContentsMargins(0, 0, 0, 0);
    }
}

// MainWindow

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
{
    // create the menu first as it is used by other things (dock widgets)
    setupMenu();

    // gl view
    glView = new GLView(this);
    setCentralWidget(glView);

    setAcceptDrops(true);

    createPropertiesSidebar();
    createOutputLog();
    createStatsSidebar();
    createFaceSidebar();

    createStatusBar();

    resize(1024, 768);
}

void MainWindow::createPropertiesSidebar()
{
    QDockWidget *dock = new QDockWidget(tr("Properties"), this);

    auto *formLayout = new QFormLayout();

    vis_checkbox = new QCheckBox(tr("vis"));
    light_checkbox = new QCheckBox(tr("light"));
    light_checkbox->setChecked(true);

    common_options = new QLineEdit();
    qbsp_options = new QLineEdit();
    vis_options = new QLineEdit();
    light_options = new QLineEdit();
    auto *reload_button = new QPushButton(tr("Reload"));

    auto *lightmapped = new QRadioButton(tr("Lightmapped"));
    lightmapped->setChecked(true);
    auto *lightmap_only = new QRadioButton(tr("Lightmap Only"));
    auto *fullbright = new QRadioButton(tr("Fullbright"));
    auto *normals = new QRadioButton(tr("Normals"));
    auto *drawflat = new QRadioButton(tr("Flat shading"));
    auto *hull0 = new QRadioButton(tr("Leafs"));
    auto *hull1 = new QRadioButton(tr("Hull 1"));
    auto *hull2 = new QRadioButton(tr("Hull 2"));
    auto *hull3 = new QRadioButton(tr("Hull 3"));
    auto *hull4 = new QRadioButton(tr("Hull 4"));
    auto *hull5 = new QRadioButton(tr("Hull 5"));

    lightmapped->setShortcut(QKeySequence("Alt+1"));
    lightmap_only->setShortcut(QKeySequence("Alt+2"));
    fullbright->setShortcut(QKeySequence("Alt+3"));
    normals->setShortcut(QKeySequence("Alt+4"));
    drawflat->setShortcut(QKeySequence("Alt+5"));
    hull0->setShortcut(QKeySequence("Alt+6"));

    lightmapped->setToolTip("Lighmapped textures (Alt+1)");
    lightmap_only->setToolTip("Lightmap only (Alt+2)");
    fullbright->setToolTip("Textures without lightmap (Alt+3)");
    normals->setToolTip("Visualize normals (Alt+4)");
    drawflat->setToolTip("Flat-shaded polygons (Alt+5)");

    auto *rendermode_layout = new QVBoxLayout();
    rendermode_layout->addWidget(lightmapped);
    rendermode_layout->addWidget(lightmap_only);
    rendermode_layout->addWidget(fullbright);
    rendermode_layout->addWidget(normals);
    rendermode_layout->addWidget(drawflat);
    rendermode_layout->addWidget(hull0);
    rendermode_layout->addWidget(hull1);
    rendermode_layout->addWidget(hull2);
    rendermode_layout->addWidget(hull3);
    rendermode_layout->addWidget(hull4);
    rendermode_layout->addWidget(hull5);

    auto *rendermode_group = new QGroupBox(tr("Render mode"));
    rendermode_group->setLayout(rendermode_layout);

    auto *drawportals = new QCheckBox(tr("Draw Portals (PRT)"));
    auto *drawleak = new QCheckBox(tr("Draw Leak (PTS/LIN)"));
    auto *drawlightgrid = new QCheckBox(tr("Draw Lightgrid"));

    auto *showtris = new QCheckBox(tr("Show Tris"));
    auto *showtris_seethrough = new QCheckBox(tr("Show Tris (See Through)"));
    auto *visculling = new QCheckBox(tr("Vis Culling"));
    visculling->setChecked(true);

    auto *keepposition = new QCheckBox(tr("Keep Camera Pos"));
    auto *keepcullfrustum = new QCheckBox(tr("Keep Cull Frustum"));
    auto *keepcullposition = new QCheckBox(tr("Keep Cull Pos"));

    nearest = new QCheckBox(tr("Nearest Filter"));

    bspx_decoupled_lm = new QCheckBox(tr("BSPX: Decoupled Lightmap"));
    bspx_decoupled_lm->setChecked(true);

    bspx_normals = new QCheckBox(tr("BSPX: Face Normals"));
    bspx_normals->setChecked(true);

    auto *draw_opaque = new QCheckBox(tr("Draw Translucency as Opaque"));
    auto *show_bmodels = new QCheckBox(tr("Show Bmodels"));
    show_bmodels->setChecked(true);

    formLayout->addRow(tr("common"), common_options);
    formLayout->addRow(tr("qbsp"), qbsp_options);
    formLayout->addRow(vis_checkbox, vis_options);
    formLayout->addRow(light_checkbox, light_options);
    formLayout->addRow(reload_button);
    formLayout->addRow(rendermode_group);
    formLayout->addRow(drawportals);
    formLayout->addRow(drawleak);
    formLayout->addRow(drawlightgrid);
    formLayout->addRow(showtris);
    formLayout->addRow(showtris_seethrough);
    formLayout->addRow(visculling);
    formLayout->addRow(keepcullposition);
    formLayout->addRow(keepcullfrustum);
    formLayout->addRow(keepposition);
    formLayout->addRow(nearest);
    formLayout->addRow(bspx_decoupled_lm);
    formLayout->addRow(bspx_normals);
    formLayout->addRow(draw_opaque);
    formLayout->addRow(show_bmodels);

    lightstyles = new QVBoxLayout();

    auto *lightstyles_group = new QGroupBox(tr("Lightstyles"));
    lightstyles_group->setLayout(lightstyles);

    formLayout->addRow(lightstyles_group);

    // brightness slider
    auto *brightnessSlider = new QSlider(Qt::Horizontal);
    brightnessSlider->setMinimum(-100);
    brightnessSlider->setMaximum(100);
    brightnessSlider->setSliderPosition(0);

    auto *brightnessLabel = new QLabel(QString("0.0"));
    auto *brightnessLayout = new QHBoxLayout();
    auto *brightnessReset = new QPushButton(tr("Reset"));
    brightnessLayout->addWidget(brightnessSlider, 1);
    brightnessLayout->addWidget(brightnessLabel, 0);
    brightnessLayout->addWidget(brightnessReset, 0);
    formLayout->addRow(tr("Exposure"), brightnessLayout);

    // wrap formLayout in a scroll area
    auto *form = new QWidget();
    form->setLayout(formLayout);

    auto *scrollArea = new QScrollArea();
    scrollArea->setWidgetResizable(true);
    scrollArea->setWidget(form);
    scrollArea->setBackgroundRole(QPalette::Window);
    scrollArea->setFrameShadow(QFrame::Plain);
    scrollArea->setFrameShape(QFrame::NoFrame);
    scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
    scrollArea->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Expanding);

    // finish dock setup
    dock->setWidget(scrollArea);
    addDockWidget(Qt::LeftDockWidgetArea, dock);
    viewMenu->addAction(dock->toggleViewAction());

    // load state persisted in settings
    QSettings s;
    common_options->setText(s.value("common_options").toString());
    qbsp_options->setText(s.value("qbsp_options").toString());
    vis_checkbox->setChecked(s.value("vis_enabled").toBool());
    keepcullposition->setEnabled(vis_checkbox->isChecked());
    keepcullfrustum->setEnabled(keepcullposition->isChecked());
    keepcullfrustum->setChecked(true);
    vis_options->setText(s.value("vis_options").toString());
    light_options->setText(s.value("light_options").toString());
    nearest->setChecked(s.value("nearest").toBool());
    if (nearest->isChecked()) {
        glView->setMagFilter(QOpenGLTexture::Nearest);
    }

    // setup event handlers

    connect(reload_button, &QAbstractButton::clicked, this, &MainWindow::reload);
    connect(lightmap_only, &QAbstractButton::toggled, this, [this](bool checked) { glView->setLighmapOnly(checked); });
    connect(fullbright, &QAbstractButton::toggled, this, [this](bool checked) { glView->setFullbright(checked); });
    connect(normals, &QAbstractButton::toggled, this, [this](bool checked) { glView->setDrawNormals(checked); });
    connect(showtris, &QAbstractButton::toggled, this, [this](bool checked) { glView->setShowTris(checked); });
    connect(showtris_seethrough, &QAbstractButton::toggled, this,
        [this](bool checked) { glView->setShowTrisSeeThrough(checked); });
    connect(visculling, &QAbstractButton::toggled, this, [this, keepcullposition, keepcullfrustum](bool checked) {
        glView->setVisCulling(checked);
        keepcullposition->setEnabled(checked);
        keepcullfrustum->setEnabled(keepcullposition->isEnabled());
    });
    connect(keepcullposition, &QAbstractButton::toggled, this, [this, keepcullfrustum](bool checked) {
        glView->setKeepCullOrigin(checked);
        keepcullfrustum->setEnabled(checked);
    });
    connect(keepcullfrustum, &QAbstractButton::toggled, this,
        [this](bool checked) { glView->setKeepCullFrustum(checked); });
    connect(drawflat, &QAbstractButton::toggled, this, [this](bool checked) { glView->setDrawFlat(checked); });
    connect(hull0, &QAbstractButton::toggled, this,
        [this](bool checked) { glView->setDrawLeafs(checked ? std::optional<int>{0} : std::nullopt); });
    connect(hull1, &QAbstractButton::toggled, this,
        [this](bool checked) { glView->setDrawLeafs(checked ? std::optional<int>{1} : std::nullopt); });
    connect(hull2, &QAbstractButton::toggled, this,
        [this](bool checked) { glView->setDrawLeafs(checked ? std::optional<int>{2} : std::nullopt); });
    connect(hull3, &QAbstractButton::toggled, this,
        [this](bool checked) { glView->setDrawLeafs(checked ? std::optional<int>{3} : std::nullopt); });
    connect(hull4, &QAbstractButton::toggled, this,
        [this](bool checked) { glView->setDrawLeafs(checked ? std::optional<int>{4} : std::nullopt); });
    connect(hull5, &QAbstractButton::toggled, this,
        [this](bool checked) { glView->setDrawLeafs(checked ? std::optional<int>{5} : std::nullopt); });
    connect(drawportals, &QAbstractButton::toggled, this, [this](bool checked) { glView->setDrawPortals(checked); });
    connect(drawleak, &QAbstractButton::toggled, this, [this](bool checked) { glView->setDrawLeak(checked); });
    connect(
        drawlightgrid, &QAbstractButton::toggled, this, [this](bool checked) { glView->setDrawLightgrid(checked); });
    connect(keepposition, &QAbstractButton::toggled, this, [this](bool checked) { glView->setKeepOrigin(checked); });
    connect(nearest, &QAbstractButton::toggled, this,
        [this](bool checked) { glView->setMagFilter(checked ? QOpenGLTexture::Nearest : QOpenGLTexture::Linear); });
    connect(draw_opaque, &QAbstractButton::toggled, this,
        [this](bool checked) { glView->setDrawTranslucencyAsOpaque(checked); });
    connect(glView, &GLView::cameraMoved, this, &MainWindow::displayCameraPositionInfo);
    connect(glView, &GLView::selectedFaceChanged, this, &MainWindow::updateCameraFaceInfo);
    connect(show_bmodels, &QAbstractButton::toggled, this, [this](bool checked) { glView->setShowBmodels(checked); });
    connect(brightnessSlider, &QAbstractSlider::valueChanged, this, [this, brightnessLabel](int value) {
        float brightness = value / 10.0f;
        brightnessLabel->setText(QString::fromLatin1("%1").arg(brightness, 0, 'f', 2));
        glView->setBrightness(brightness);
    });
    connect(brightnessReset, &QAbstractButton::pressed, this,
        [this, brightnessSlider]() { brightnessSlider->setValue(0); });

    // set up load timer
    m_fileReloadTimer = std::make_unique<QTimer>();

    m_fileReloadTimer->setSingleShot(true);
    m_fileReloadTimer->connect(m_fileReloadTimer.get(), &QTimer::timeout, this, &MainWindow::fileReloadTimerExpired);
}

void MainWindow::createStatsSidebar()
{
    QDockWidget *dock = new QDockWidget(tr("Stats"), this);

    stats_panel = new StatsPanel();

    // finish dock setup
    dock->setWidget(stats_panel);
    addDockWidget(Qt::RightDockWidgetArea, dock);
    viewMenu->addAction(dock->toggleViewAction());
}

void MainWindow::createFaceSidebar()
{
    QDockWidget *dock = new QDockWidget(tr("Face"), this);

    face_panel = new FacePanel();

    // finish dock setup
    dock->setWidget(face_panel);
    addDockWidget(Qt::RightDockWidgetArea, dock);
    viewMenu->addAction(dock->toggleViewAction());
}

void MainWindow::logWidgetSetText(ETLogTab tab, const std::string &str)
{
    m_outputLogWidget->setTabText((int32_t)tab, str.c_str());
}

void MainWindow::lightpreview_percent_callback(std::optional<uint32_t> percent, std::optional<duration> elapsed)
{
    int32_t tabIndex = (int32_t)m_activeLogTab;

    if (elapsed.has_value()) {
        lightpreview_log_callback(
            logging::flag::PROGRESS, fmt::format("finished in: {:.3}\n", elapsed.value()).c_str());
        QMetaObject::invokeMethod(
            this, std::bind(&MainWindow::logWidgetSetText, this, m_activeLogTab, ETLogWidget::logTabNames[tabIndex]));
    } else {
        if (percent.has_value()) {
            QMetaObject::invokeMethod(
                this, std::bind(&MainWindow::logWidgetSetText, this, m_activeLogTab,
                          fmt::format("{} [{:>3}%]", ETLogWidget::logTabNames[tabIndex], percent.value())));
        } else {
            QMetaObject::invokeMethod(this, std::bind(&MainWindow::logWidgetSetText, this, m_activeLogTab,
                                                fmt::format("{} (...)", ETLogWidget::logTabNames[tabIndex])));
        }
    }
}

void MainWindow::lightpreview_log_callback(logging::flag flags, const char *str)
{
    if (bitflags(flags) & logging::flag::PERCENT)
        return;

    if (QApplication::instance()->thread() != QThread::currentThread()) {
        QMetaObject::invokeMethod(this,
            std::bind([this, flags](const std::string &s) -> void { lightpreview_log_callback(flags, s.c_str()); },
                std::string(str)));
        return;
    }

    auto *textEdit = m_outputLogWidget->textEdit(m_activeLogTab);
    const bool atBottom = textEdit->verticalScrollBar()->value() == textEdit->verticalScrollBar()->maximum();
    QTextDocument *doc = textEdit->document();
    QTextCursor cursor(doc);
    cursor.movePosition(QTextCursor::End);
    cursor.beginEditBlock();
    cursor.insertBlock();
    cursor.insertHtml(QString::asprintf("%s\n", str));
    cursor.endEditBlock();

    // scroll scrollarea to bottom if it was at bottom when we started
    //(we don't want to force scrolling to bottom if user is looking at a
    // higher position)
    if (atBottom) {
        QScrollBar *bar = textEdit->verticalScrollBar();
        bar->setValue(bar->maximum());
    }
}

void MainWindow::createOutputLog()
{
    QDockWidget *dock = new QDockWidget(tr("Tool Logs"), this);

    m_outputLogWidget = new ETLogWidget();

    // finish dock widget setup
    dock->setWidget(m_outputLogWidget);

    addDockWidget(Qt::BottomDockWidgetArea, dock);
    viewMenu->addAction(dock->toggleViewAction());

    logging::set_print_callback(
        std::bind(&MainWindow::lightpreview_log_callback, this, std::placeholders::_1, std::placeholders::_2));
    logging::set_percent_callback(
        std::bind(&MainWindow::lightpreview_percent_callback, this, std::placeholders::_1, std::placeholders::_2));
}

void MainWindow::createStatusBar()
{
    m_cameraStatus = new QLabel();
    statusBar()->addWidget(m_cameraStatus);
}

/**
 * Precondition: openRecentMenu is created.
 *
 * Clears and rebuilds the menu given the list of files that should be displayed in it.
 */
void MainWindow::updateRecentsSubmenu(const QStringList &recents)
{
    openRecentMenu->clear();

    for (const QString &recent : recents) {
        auto *action = openRecentMenu->addAction(recent);
        connect(action, &QAction::triggered, this, [this, recent]() { loadFile(recent); });
    }

    openRecentMenu->addSeparator();
    openRecentMenu->addAction(tr("Clear Recents"), this, [this]() {
        ClearRecents();
        this->updateRecentsSubmenu(GetRecents());
    });
}

void MainWindow::updateCameraBookmarksSubmenu()
{
    cameraBookmarksMenu->clear();

    cameraBookmarksMenu->addAction(tr("Bookmark Current Camera Position"), this, [this]() {
        camera_bookmark_t b{.origin = this->glView->cameraPosition(), .forward = this->glView->cameraForward()};
        AddCameraBookmark(b);
        this->updateCameraBookmarksSubmenu();
    });
    cameraBookmarksMenu->addSeparator();

    auto bookmarks = GetCameraBookmarks();
    for (const auto &bookmark : bookmarks) {
        auto *action = cameraBookmarksMenu->addAction(CameraBookmarkToQString(bookmark));
        connect(action, &QAction::triggered, this,
            [this, bookmark]() { this->glView->setCamera(bookmark.origin, bookmark.forward); });
    }

    cameraBookmarksMenu->addSeparator();
    cameraBookmarksMenu->addAction(tr("Clear Camera Bookmarks"), this, [this]() {
        ClearCameraBookmarks();
        this->updateCameraBookmarksSubmenu();
    });
}

MainWindow::~MainWindow() { }

static void OpenHelpFile(const QString &file)
{
    QString fileString = QCoreApplication::applicationDirPath() + QStringLiteral("/doc/") + file;
    QUrl fileUrl = QUrl::fromLocalFile(fileString);

    QDesktopServices::openUrl(fileUrl);
};

void MainWindow::setupMenu()
{
    auto *menu = menuBar()->addMenu(tr("&File"));

    auto *open = menu->addAction(tr("&Open"), this, &MainWindow::fileOpen);
    open->setShortcut(QKeySequence::Open);

    openRecentMenu = menu->addMenu(tr("Open &Recent"));
    updateRecentsSubmenu(GetRecents());

    menu->addSeparator();

    menu->addAction(tr("Save Screenshot..."), this, &MainWindow::takeScreenshot);

    menu->addSeparator();

    auto *exit = menu->addAction(tr("E&xit"), this, &QWidget::close);
    exit->setShortcut(QKeySequence::Quit);

    // edit menu

    auto *editMenu = menuBar()->addMenu(tr("&Edit"));
    editMenu->addAction(tr("&Copy Camera Position"), this, [this]() {
        qvec3f pos = this->glView->cameraPosition();

        std::string cpp_str = fmt::format("{}", pos);

        QClipboard *clipboard = QGuiApplication::clipboard();
        clipboard->setText(QString::fromStdString(cpp_str));
    });

    // view menu

    viewMenu = menuBar()->addMenu(tr("&View"));
    cameraBookmarksMenu = viewMenu->addMenu(tr("Camera Bookmarks"));
    updateCameraBookmarksSubmenu();
    viewMenu->addAction(tr("&Move camera to..."), this, [this]() {
        bool ok = false;
        QString text = QInputDialog::getText(
            this, tr("Move camera to"), tr("Enter X Y Z coords, space-separated"), QLineEdit::Normal, QString(), &ok);

        QStringList comps = text.split(QString::fromLatin1(" "), Qt::SkipEmptyParts);

        if (comps.length() >= 3) {
            this->glView->setCamera(qvec3d{comps[0].toDouble(), comps[1].toDouble(), comps[2].toDouble()});
        }
    });

    // help menu

    auto *helpMenu = menuBar()->addMenu(tr("&Help"));

    helpMenu->addAction(tr("&Lightpreview Documentation"), this, []() { OpenHelpFile("lightpreview.html"); });
    helpMenu->addAction(tr("&About"), this,
        [this]() { QMessageBox::about(this, tr("About lightpreview"), tr("ericw-tools " ERICWTOOLS_VERSION)); });
}

void MainWindow::dragEnterEvent(QDragEnterEvent *event)
{
    if (event->mimeData()->hasUrls())
        event->acceptProposedAction();
}

void MainWindow::dropEvent(QDropEvent *event)
{
    auto urls = event->mimeData()->urls();
    if (!urls.empty()) {
        const QUrl &url = urls[0];
        if (url.isLocalFile()) {
            loadFile(url.toLocalFile());

            event->acceptProposedAction();
        }
    }
}

void MainWindow::showEvent(QShowEvent *event)
{
    // FIXME: move command-line parsing somewhere else?
    // FIXME: support more command-line options?
    auto args = QCoreApplication::arguments();
    if (args.size() == 2) {
        QTimer::singleShot(0, this, [this, args] { loadFile(args.at(1)); });
    }
}

void MainWindow::fileOpen()
{
    QSettings settings;
    QString lastDir = settings.value("lastDir").toString();

    QString fileName = QFileDialog::getOpenFileName(this, tr("Open File"), lastDir, tr("Map (*.map);; BSP (*.bsp)"));

    if (!fileName.isEmpty()) {
        // update the lastDir setting
        QString newDir = QFileInfo(fileName).absolutePath();
        settings.setValue("lastDir", newDir);

        loadFile(fileName);
    }
}

void MainWindow::takeScreenshot()
{
    QString fileName = QFileDialog::getSaveFileName(this, tr("Save Screenshot"), "", tr("PNG (*.png)"));

    if (!fileName.isEmpty())
        glView->takeScreenshot(fileName, 3840, 2160);
}

void MainWindow::fileReloadTimerExpired()
{
    qint64 currentSize = QFileInfo(m_mapFile).size();

    // it was rewritten...
    if (currentSize != m_fileSize) {
        qDebug() << "size changed since last write, restarting timer";
        m_fileReloadTimer->start(150);
        return;
    }

    // good to go? maybe?
    qDebug() << "size not changed, good to go";
    loadFileInternal(m_mapFile, true);

    m_fileSize = -1;
}

void MainWindow::loadFile(const QString &file)
{
    qDebug() << "load " << file;

    // update recents
    updateRecentsSubmenu(AddRecent(file));

    m_mapFile = file;

    if (m_watcher) {
        delete m_watcher;
    }
    m_watcher = new QFileSystemWatcher(this);
    m_fileSize = -1;

    // start watching it
    qDebug() << "adding path: " << m_watcher->addPath(file);

    connect(m_watcher, &QFileSystemWatcher::fileChanged, this, [&](const QString &path) {
        qDebug() << "got change notif for " << m_mapFile;

        // check current files' size
        m_fileSize = QFileInfo(m_mapFile).size();

        // start timer
        m_fileReloadTimer->start(150);
    });

    loadFileInternal(file, false);
}

std::filesystem::path MakeFSPath(const QString &string)
{
    return std::filesystem::path{string.toStdU16String()};
}

bspdata_t MainWindow::QbspVisLight_Common(const std::filesystem::path &name, std::vector<std::string> extra_common_args,
    std::vector<std::string> extra_qbsp_args, std::vector<std::string> extra_vis_args,
    std::vector<std::string> extra_light_args, bool run_vis, bool run_light)
{
    auto resetActiveTabText = [&]() {
        QMetaObject::invokeMethod(this, std::bind(&MainWindow::logWidgetSetText, this, m_activeLogTab,
                                            ETLogWidget::logTabNames[(int32_t)m_activeLogTab]));
    };

    auto bsp_path = name;
    bsp_path.replace_extension(".bsp");

    std::vector<std::string> args{
        "", // the exe path, which we're ignoring in this case
    };
    for (auto &extra : extra_common_args) {
        args.push_back(extra);
    }
    for (auto &extra : extra_qbsp_args) {
        args.push_back(extra);
    }
    args.push_back(name.string());

    // run qbsp
    m_activeLogTab = ETLogTab::TAB_BSP;

    InitQBSP(args);
    ProcessFile();

    resetActiveTabText();

    // run vis
    if (run_vis) {
        m_activeLogTab = ETLogTab::TAB_VIS;
        std::vector<std::string> vis_args{
            "", // the exe path, which we're ignoring in this case
        };
        for (auto &extra : extra_common_args) {
            vis_args.push_back(extra);
        }
        for (auto &extra : extra_vis_args) {
            vis_args.push_back(extra);
        }
        vis_args.push_back(name.string());
        vis_main(vis_args);
    }

    resetActiveTabText();

    // run light
    if (run_light) {
        m_activeLogTab = ETLogTab::TAB_LIGHT;
        std::vector<std::string> light_args{
            "", // the exe path, which we're ignoring in this case
        };
        for (auto &extra : extra_common_args) {
            light_args.push_back(extra);
        }
        for (auto &arg : extra_light_args) {
            light_args.push_back(arg);
        }
        light_args.push_back(name.string());

        light_main(light_args);
    }

    resetActiveTabText();

    m_activeLogTab = ETLogTab::TAB_LIGHTPREVIEW;

    // serialize obj
    {
        bspdata_t bspdata;
        LoadBSPFile(bsp_path, &bspdata);

        ConvertBSPFormat(&bspdata, &bspver_generic);

        return bspdata;
    }
}

static std::vector<std::string> ParseArgs(const QLineEdit *line_edit)
{
    std::vector<std::string> result;

    QString text = line_edit->text().trimmed();
    if (text.isEmpty())
        return result;

    bool inside_quotes = false;
    for (const auto &str : text.split('"')) {
        qDebug() << "got token " << str << " inside quote? " << inside_quotes;

        if (inside_quotes) {
            result.push_back(str.toStdString());
        } else {
            // split by spaces
            for (const auto &str2 : str.split(' ', Qt::SkipEmptyParts)) {
                qDebug() << "got sub token " << str2;
                result.push_back(str2.toStdString());
            }
        }

        inside_quotes = !inside_quotes;
    }

    return result;
}

void MainWindow::reload()
{
    if (m_mapFile.isEmpty())
        return;

    loadFileInternal(m_mapFile, true);
}

class QLightStyleSlider : public QFrame
{
public:
    int32_t style_id;

    QLightStyleSlider(int32_t style_id, GLView *glView)
        : QFrame(),
          style_id(style_id),
          glView(glView)
    {
        auto *style_layout = new QHBoxLayout();

        auto *style = new QSpinBox();
        style->setRange(0, 200);
        style->setValue(100);
        style->setSingleStep(10);
        // style->setTickPosition(QSlider::TicksBothSides);
        // style->setTickInterval(50);

        connect(style, QOverload<int>::of(&QSpinBox::valueChanged), this, &QLightStyleSlider::setValue);

        auto *style_label = new QLabel();
        style_label->setText(QString::asprintf("%i", style_id));

        style_layout->addWidget(style_label);
        style_layout->addWidget(style);

        setLayout(style_layout);
        setFrameShadow(QFrame::Plain);
        setFrameShape(QFrame::NoFrame);
    }

private:
    void setValue(int value) { glView->setLightStyleIntensity(style_id, value); }

    GLView *glView;
};

int MainWindow::compileMap(const QString &file, bool is_reload)
{
    fs::path fs_path = MakeFSPath(file);

    m_bspdata = {};
    render_settings.reset();

    try {
        if (fs_path.extension().compare(".bsp") == 0) {

            LoadBSPFile(fs_path, &m_bspdata);

            auto opts = ParseArgs(common_options);

            std::vector<const char *> argPtrs;

            argPtrs.push_back("");

            for (const std::string &arg : opts) {
                argPtrs.push_back(arg.data());
            }

            render_settings.preinitialize(argPtrs.size(), argPtrs.data());
            render_settings.initialize(argPtrs.size() - 1, argPtrs.data() + 1);
            render_settings.postinitialize(argPtrs.size(), argPtrs.data());

            m_bspdata.version->game->init_filesystem(fs_path, render_settings);

            ConvertBSPFormat(&m_bspdata, &bspver_generic);

        } else {
            m_bspdata =
                QbspVisLight_Common(fs_path, ParseArgs(common_options), ParseArgs(qbsp_options), ParseArgs(vis_options),
                    ParseArgs(light_options), vis_checkbox->isChecked(), light_checkbox->isChecked());

            // FIXME: move to a lightpreview_settings
            settings::common_settings settings;

            // FIXME: copy the -path args from light
            settings.paths.copy_from(::light_options.paths);

            m_bspdata.loadversion->game->init_filesystem(file.toStdString(), settings);
        }
    } catch (const settings::parse_exception &p) {
        // FIXME: threading error: don't call Qt widgets code from background thread
        auto *textEdit = m_outputLogWidget->textEdit(m_activeLogTab);
        textEdit->append(QString::fromUtf8(p.what()) + QString::fromLatin1("\n"));
        m_activeLogTab = ETLogTab::TAB_LIGHTPREVIEW;
        return 1;
    } catch (const settings::quit_after_help_exception &p) {
        // FIXME: threading error: don't call Qt widgets code from background thread
        auto *textEdit = m_outputLogWidget->textEdit(m_activeLogTab);
        textEdit->append(QString::fromUtf8(p.what()) + QString::fromLatin1("\n"));
        m_activeLogTab = ETLogTab::TAB_LIGHTPREVIEW;
        return 1;
    } catch (const std::exception &other) {
        // FIXME: threading error: don't call Qt widgets code from background thread
        auto *textEdit = m_outputLogWidget->textEdit(m_activeLogTab);
        textEdit->append(QString::fromUtf8(other.what()) + QString::fromLatin1("\n"));
        m_activeLogTab = ETLogTab::TAB_LIGHTPREVIEW;
        return 1;
    }

    // try to load .lit
    auto lit_path = fs_path;
    lit_path.replace_extension(".lit");

    m_hdr_litdata = {};
    m_litdata = {};

    try {
        if (const auto *bsp = std::get_if<mbsp_t>(&m_bspdata.bsp)) {
            auto lit_variant = LoadLitFile(lit_path, *bsp);

            if (auto *lit1_ptr = std::get_if<lit1_t>(&lit_variant)) {
                m_litdata = std::move(lit1_ptr->rgbdata);
            } else if (auto *lit_hdr_ptr = std::get_if<lit_hdr>(&lit_variant)) {
                m_hdr_litdata = std::move(lit_hdr_ptr->samples);
            }
        }
    } catch (const std::runtime_error &error) {
        logging::print("error loading lit: {}", error.what());
        m_litdata = {};
        m_hdr_litdata = {};
    }

    return 0;
}

void MainWindow::compileThreadExited()
{
    // clear lightstyle widgets
    while (QWidget *w = lightstyles->parentWidget()->findChild<QWidget *>(QString(), Qt::FindDirectChildrenOnly)) {
        delete w;
    }

    delete m_compileThread;
    m_compileThread = nullptr;

    if (!std::holds_alternative<mbsp_t>(m_bspdata.bsp)) {
        return;
    }
    const auto &bsp = std::get<mbsp_t>(m_bspdata.bsp);

    m_entities = EntData_Parse(bsp);

    // build lightmap atlas
    auto atlas = build_lightmap_atlas(
        bsp, m_bspdata.bspx.entries, m_litdata, m_hdr_litdata, false, bspx_decoupled_lm->isChecked());

    glView->renderBSP(
        m_mapFile, bsp, m_bspdata.bspx.entries, m_entities, atlas, render_settings, bspx_normals->isChecked());

    if (!m_fileWasReload && !glView->getKeepOrigin()) {
        for (auto &ent : m_entities) {
            if (ent.get("classname") == "info_player_start") {
                qvec3f origin;
                ent.get_vector("origin", origin);

                qvec3f angles{};

                if (ent.has("angles")) {
                    ent.get_vector("angles", angles);
                    angles = {angles[1], -angles[0], angles[2]}; // -pitch yaw roll -> yaw pitch roll
                } else if (ent.has("angle"))
                    angles = {ent.get_float("angle"), 0, 0};
                else if (ent.has("mangle"))
                    ent.get_vector("mangle", angles);

                glView->setCamera(origin, qv::vec_from_mangle(angles));
                break;
            }
        }
    }

    // set lightstyle data
    for (auto &style_entry : atlas.style_to_lightmap_atlas) {

        auto *style = new QLightStyleSlider(style_entry.first, glView);
        lightstyles->addWidget(style);
    }

    stats_panel->updateWithBSP(&bsp, m_bspdata.bspx.entries);
}

void MainWindow::loadFileInternal(const QString &file, bool is_reload)
{
    // TODO
    if (m_compileThread)
        return;

    qDebug() << "loadFileInternal " << file;

    // just in case
    m_fileReloadTimer->stop();
    m_fileWasReload = is_reload;

    // persist settings
    QSettings s;
    s.setValue("common_options", common_options->text());
    s.setValue("qbsp_options", qbsp_options->text());
    s.setValue("vis_enabled", vis_checkbox->isChecked());
    s.setValue("vis_options", vis_options->text());
    s.setValue("light_options", light_options->text());
    s.setValue("nearest", nearest->isChecked());

    // update title bar
    setWindowFilePath(file);
    setWindowTitle(QFileInfo(file).fileName() + " - lightpreview");

    for (auto &edit : m_outputLogWidget->textEdits()) {
        edit->clear();
    }

    m_compileThread = QThread::create(std::bind(&MainWindow::compileMap, this, file, is_reload));
    connect(m_compileThread, &QThread::finished, this, &MainWindow::compileThreadExited);
    m_compileThread->start();
}

void MainWindow::displayCameraPositionInfo()
{
    const qvec3f point = glView->cameraPosition();
    const qvec3f forward = glView->cameraForward();

    std::string leaf_type;
    int32_t area = -1;
    {
        const auto *bsp = std::get_if<mbsp_t>(&m_bspdata.bsp);
        if (!bsp)
            return;

        const mleaf_t *leaf = BSP_FindLeafAtPoint(bsp, &bsp->dmodels[0], point);
        if (leaf) {
            auto *game = bsp->loadversion->game;
            leaf_type = game->create_contents_from_native(leaf->contents).to_string();

            area = leaf->area;
        }
    }

    std::string cpp_str = fmt::format("pos ({}) forward ({}) contents ({}) area ({})", point, forward, leaf_type, area);

    m_cameraStatus->setText(QString::fromStdString(cpp_str));
}

void MainWindow::updateCameraFaceInfo()
{
    face_panel->updateWithBSP(
        &std::get<mbsp_t>(m_bspdata.bsp), m_entities, m_bspdata.bspx.entries, glView->getSelectedFace());
}