#include "mainwindow.h"
#include "ui_mainwindow.h"
using Xybrid::MainWindow;

#include <QDebug>
#include <QKeyEvent>
#include <QShortcut>
#include <QTabWidget>
#include <QFileDialog>
#include <QInputDialog>
#include <QMessageBox>
#include <QDialogButtonBox>
#include <QSpinBox>
#include <QWindow>
#include <QUndoStack>
#include <QTimer>
#include <QOpenGLWidget>
#include <QGLWidget>

#include <QScroller>
#include <QGraphicsTextItem>

#include <QJsonObject>

#include "data/graph.h"

#include "util/strings.h"
#include "util/lambdaeventfilter.h"
#include "fileops.h"

#include "ui/patternlistmodel.h"
#include "ui/patternsequencermodel.h"
#include "ui/patterneditoritemdelegate.h"

#include "ui/patchboard/patchboardscene.h"
#include "ui/patchboard/nodeuiscene.h"

#include "ui/samplelistmodel.h"

#include "editing/compositecommand.h"
#include "editing/projectcommands.h"
#include "editing/patterncommands.h"

#include "config/pluginregistry.h"
#include "audio/audioengine.h"

using Xybrid::Data::Project;
using Xybrid::Data::Pattern;
using Xybrid::Data::SequenceEntry;
using Xybrid::Data::Graph;
using Xybrid::Data::Node;
using Xybrid::Data::Port;

using Xybrid::UI::PatternListModel;
using Xybrid::UI::PatternSequencerModel;
using Xybrid::UI::PatternEditorModel;
using Xybrid::UI::PatternEditorItemDelegate;

using Xybrid::UI::PatchboardScene;
using Xybrid::UI::NodeUIScene;

using Xybrid::UI::SampleListModel;

using namespace Xybrid::Editing;
using namespace Xybrid::Config;
using namespace Xybrid::Audio;

namespace {
    //
}

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow) {
    socket = new UISocket(); // create this first
    ui->setupUi(this);

    // remove tab containing system widgets
    ui->tabWidget->removeTab(ui->tabWidget->indexOf(ui->extra_));

    undoStack = new QUndoStack(this);
    //undoStack->setUndoLimit(256);
    connect(undoStack, &QUndoStack::cleanChanged, this, [this](bool) {
        updateTitle();
    });

    auto* undoAction = undoStack->createUndoAction(this, tr("&Undo"));
    undoAction->setShortcuts(QKeySequence::Undo);
    ui->menuEdit->addAction(undoAction);

    auto* redoAction = undoStack->createRedoAction(this, tr("&Redo"));
    redoAction->setShortcuts(QKeySequence::Redo);
    ui->menuEdit->addAction(redoAction);

    // prevent right pane of pattern view from being collapsed
    ui->patternViewSplitter->setCollapsible(1, false);
    connect(ui->patternViewSplitter, &QSplitter::splitterMoved, this, [this](int, int) {
        // and when the list is collapsed, make sure header size is updated
        ui->patternEditor->updateHeader();
    });

    { /* Set up toolbar */ } {
        auto* t = ui->tabWidget;
        t->setCurrentWidget(ui->pattern); // set default regardless of what was edited last in the designer :|
        t->setCornerWidget(ui->logo, Qt::TopLeftCorner);
        auto* tb = ui->toolBar;
        t->setCornerWidget(tb);
        tb->layout()->addWidget(ui->menuBar);

        ui->playButton->setIcon(ui->playButton->style()->standardIcon(QStyle::SP_MediaPlay));
        // play/stop
        auto play = new QAction(this);
        play->setShortcuts({ QKeySequence("Ctrl+P"), QKeySequence("Ctrl+Shift+P") });
        ui->playButton->setDefaultAction(play);
        connect(play, &QAction::triggered, this, [this] {
            if (audioEngine->playbackMode() == AudioEngine::Playing && audioEngine->playingProject() == project) audioEngine->stop();
            else {
                bool shift = QGuiApplication::keyboardModifiers().testFlag(Qt::KeyboardModifier::ShiftModifier);
                if (shift) audioEngine->play(project, ui->patternSequencer->currentIndex().column());
                else audioEngine->play(project);
            }
        });

        //ui->menuBar->setStyleSheet("QMenuBar { background: transparent; vertical-align: center; } QMenuBar::item { } QMenuBar::item:!pressed { background: transparent; }");
    }

    { /* Set up pattern list */ } {
        // model
        ui->patternList->setModel(new PatternListModel(ui->patternList, this));

        // events
        // on selection change
        connect(ui->patternList->selectionModel(), &QItemSelectionModel::currentChanged, this, [this](const QModelIndex& index, const QModelIndex& old) {
            if (index == old) return; // no actual change
            size_t idx = static_cast<size_t>(index.row());
            if (idx >= project->patterns.size()) return;
            this->selectPatternForEditing(project->patterns[idx].get());
        });
        // on click
        connect(ui->patternList, &QListView::clicked, this, [this] { // deselect on sequencer when list clicked
            ui->patternSequencer->setCurrentIndex(ui->patternSequencer->model()->index(0, -1));
        });

        // rightclick menu
        connect(ui->patternList, &QListView::customContextMenuRequested, this, [this](const QPoint& pt) {
            auto ind = ui->patternList->indexAt(pt);
            size_t idx = static_cast<size_t>(ind.row());
            std::shared_ptr<Pattern> p = nullptr;
            if (idx < project->patterns.size()) p = project->patterns[idx];

            QMenu* menu = new QMenu(this);
            if (ind.isValid()) menu->addAction("Rename...", this, [this, ind] { ui->patternList->edit(ind); });
            menu->addAction("New Pattern", this, [this, idx] {
                (new ProjectPatternAddCommand(project, static_cast<int>(idx)))->commit();
            });
            if (p) {
                menu->addAction("Duplicate Pattern", this, [this, p, idx] {
                    (new ProjectPatternAddCommand(project, static_cast<int>(idx) + 1, -1, p))->commit();
                });
                menu->addSeparator();
                menu->addAction("Properties...", this, [this, p] { openPatternProperties(p); });
                menu->addSeparator();
                menu->addAction("Delete Pattern", this, [this, p] {
                    if (QMessageBox::warning(this, "Are you sure?", QString("Remove pattern %1?").arg(Util::numAndName(p->index, p->name)), QMessageBox::Yes | QMessageBox::No, QMessageBox::No) != QMessageBox::Yes) return;
                    (new ProjectPatternDeleteCommand(project, p))->commit();
                });
            }
            menu->setAttribute(Qt::WA_DeleteOnClose);
            menu->popup(ui->patternList->mapToGlobal(pt));
        });//*/
    }

    { /* Set up song info pane */ } {
        ui->expandSongInfo->setIcon(ui->expandSongInfo->style()->standardIcon(QStyle::SP_ToolBarVerticalExtensionButton));
        ui->collapseSongInfo->setIcon(ui->expandSongInfo->style()->standardIcon(QStyle::SP_ToolBarVerticalExtensionButton));

        connect(ui->expandSongInfo, &QPushButton::pressed, this, [this] { setSongInfoPaneExpanded(true); });
        connect(ui->collapseSongInfo, &QPushButton::pressed, this, [this] { setSongInfoPaneExpanded(false); });

        connect(ui->editArtist, &QLineEdit::textEdited, this, [this](const QString& s) { project->artist = s; updateTitle(); });
        connect(ui->editTitle, &QLineEdit::textEdited, this, [this](const QString& s) { project->title = s; updateTitle(); });

        connect(ui->editComment, &QPlainTextEdit::modificationChanged, this, [this](bool b) { if (b) { project->comment = ui->editComment->document()->toPlainText(); ui->editComment->document()->setModified(false); } });

        connect(ui->editTempo, qOverload<double>(&QDoubleSpinBox::valueChanged), this, [this](double t) { project->tempo = t; });
    }

    { /* Set up sequencer */ } {
        // model
        ui->patternSequencer->setModel(new PatternSequencerModel(ui->patternSequencer, this));
        // some metrics that the designer doesn't seem to like doing
        ui->patternSequencer->horizontalHeader()->setSectionResizeMode(QHeaderView::Fixed);
        ui->patternSequencer->horizontalHeader()->setDefaultSectionSize(24);
        ui->patternSequencer->verticalHeader()->setSectionResizeMode(QHeaderView::Fixed);
        ui->patternSequencer->verticalHeader()->setDefaultSectionSize(24);

        // events
        // on selection change
        connect(ui->patternSequencer->selectionModel(), &QItemSelectionModel::currentChanged, this, [this](const QModelIndex& index, const QModelIndex&) {
            size_t idx = static_cast<size_t>(index.column());
            if (idx >= project->sequence.size()) return;
            this->selectPatternForEditing(project->sequence[idx].pattern().get());
        });

        // middle click
        auto mouselmb = [this, seq = ui->patternSequencer](QObject*, QEvent* e) {
            if (e->type() == QEvent::MouseButtonRelease) {
                auto me = static_cast<QMouseEvent*>(e);
                if (me->button() == Qt::MouseButton::MiddleButton) {
                    auto idx = static_cast<size_t>(seq->indexAt(me->pos()).column());
                    if (idx >= project->sequence.size()) return false; // nothing to remove
                    auto* c = new ProjectSequencerDeltaCommand(project);
                    c->seq.erase(c->seq.begin() + static_cast<ptrdiff_t>(idx));
                    c->seqSel = static_cast<int>(idx)-1;
                    return c->commit();
                }
            }
            return false;
        };
        ui->patternSequencer->viewport()->installEventFilter(new LambdaEventFilter(this, mouselmb));

        // rightclick menu
        connect(ui->patternSequencer, &QTableView::customContextMenuRequested, this, [this](const QPoint& pt) {
            size_t idx = static_cast<size_t>(ui->patternSequencer->indexAt(pt).column());

            QMenu* menu = new QMenu(this);
            menu->addAction("Insert Pattern", this, [this, idx] {
                if (!editingPattern->validFor(project)) return; // nope
                int si = static_cast<int>(std::min(idx, project->sequence.size()));
                auto* c = new ProjectSequencerDeltaCommand(project);
                c->seq.insert(c->seq.begin() + si, editingPattern);
                c->seqSel = si+1;
                c->commit();
            });
            menu->addAction("Insert Separator", this, [this, idx] {
                int si = static_cast<int>(std::min(idx, project->sequence.size()));
                auto* c = new ProjectSequencerDeltaCommand(project);
                c->seq.insert(c->seq.begin() + si, SequenceEntry::Separator);
                c->seqSel = si+1;
                c->commit();
            });
            menu->addAction("Insert Loop Point", this, [this, idx] {
                int si = static_cast<int>(std::min(idx, project->sequence.size()));
                auto* c = new ProjectSequencerDeltaCommand(project);
                c->seq.insert(c->seq.begin() + si, SequenceEntry::LoopStart);
                c->seqSel = si+1;
                c->commit();
            });
            menu->addAction("Insert Loop Trigger", this, [this, idx] {
                int si = static_cast<int>(std::min(idx, project->sequence.size()));
                auto* c = new ProjectSequencerDeltaCommand(project);
                c->seq.insert(c->seq.begin() + si, SequenceEntry::LoopTrigger);
                c->seqSel = si+1;
                c->commit();
            });
            if (idx < project->sequence.size()) menu->addAction("Remove", this, [this, idx] {
                auto* c = new ProjectSequencerDeltaCommand(project);
                c->seq.erase(c->seq.begin() + static_cast<ptrdiff_t>(idx));
                c->seqSel = static_cast<int>(idx)-1;
                c->commit();
            });
            menu->addSeparator();
            menu->addAction("Create New Pattern", this, [this, idx] {
                int si = static_cast<int>(std::min(idx, project->sequence.size()));
                (new ProjectPatternAddCommand(project, -1, si))->commit();
            });
            if (idx < project->sequence.size()) if (auto p = project->sequence[idx].pattern(); p) {
                    menu->addAction("Duplicate Pattern", this, [this, idx, p] {
                        int si = static_cast<int>(std::min(idx + 1, project->sequence.size()));
                        (new ProjectPatternAddCommand(project, static_cast<int>(p->index) + 1, si, p))->commit();
                    });
                    menu->addSeparator();
                    menu->addAction("Properties...", this, [this, p] { openPatternProperties(p); });
                }

            menu->setAttribute(Qt::WA_DeleteOnClose);
            menu->popup(ui->patternSequencer->mapToGlobal(pt));
        });
    }

    { /* Set up keyboard shortcuts for pattern view */ } {
        // Ctrl+PgUp/Down - previous or next pattern in sequencer
        auto nxPt = [this](int off) {
            auto ix = ui->patternSequencer->currentIndex();
            int i = ix.column();
            int ss = static_cast<int>(project->sequence.size());
            if (!ix.isValid()) i = off > 0 ? -1 : 0;
            for (int c = 0; c < ss; c++) {
                i = (ss+i+off) % ss;
                auto& se = project->sequence[static_cast<size_t>(i)];
                if (auto p = se.pattern(); p) {
                    ui->patternSequencer->setCurrentIndex(ui->patternSequencer->model()->index(0, i));
                    return;
                }
            }
        };
        connect(new QShortcut(QKeySequence("Ctrl+PgUp"), ui->pattern), &QShortcut::activated, this, [nxPt] { nxPt(-1); });
        connect(new QShortcut(QKeySequence("Ctrl+PgDown"), ui->pattern), &QShortcut::activated, this, [nxPt] { nxPt(1); });

        /* tmp test
        connect(new QShortcut(QKeySequence("Ctrl+F1"), ui->patchboard), &QShortcut::activated, this, [this] {
          auto inp = QInputDialog::getText(this, "yes", "yes");
          WId id = inp.toULongLong();
          auto* w = QWindow::fromWinId(id);
          auto* w2 = new QWindow();
          auto* wc = QWidget::createWindowContainer(w2);
          w->setParent(w2);
          ui->patchboard->layout()->addWidget(wc);
        });
        */
    }

    { /* Set up patchboard view */ } {
        //ui->patchboardView->setDragMode(QGraphicsView::DragMode::RubberBandDrag);
        auto* view = ui->patchboardView;

        bool enableHWAccel = false; // disabled because QOpenGLWidget has some huge lag issues in this context
        if (enableHWAccel) {
            auto* vp = new QOpenGLWidget();
            view->setViewport(vp); // enable hardware acceleration
        }
        view->setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform | QPainter::HighQualityAntialiasing);
        // Under OSX these cause Xybrid to crash.
        #ifndef __APPLE__
        glEnable(GL_MULTISAMPLE);
        glEnable(GL_LINE_SMOOTH);
        #endif
        //QGL::FormatOption::Rgba


        view->setAlignment(Qt::AlignTop | Qt::AlignLeft);
        view->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
        view->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
        view->setAttribute(Qt::WA_AcceptTouchEvents, true);
        QScroller::grabGesture(view, QScroller::MiddleMouseButtonGesture);
        {
            auto prop = QScroller::scroller(view)->scrollerProperties();
            prop.setScrollMetric(QScrollerProperties::HorizontalOvershootPolicy, QScrollerProperties::OvershootAlwaysOff);
            prop.setScrollMetric(QScrollerProperties::VerticalOvershootPolicy, QScrollerProperties::OvershootAlwaysOff);
            prop.setScrollMetric(QScrollerProperties::AxisLockThreshold, 1);
            QScroller::scroller(view)->setScrollerProperties(prop);
            QScroller::scroller(view)->setSnapPositionsX({});
            QScroller::scroller(view)->setSnapPositionsY({});
        }

        // event filter to make drag-to-select only happen on left click
        view->viewport()->installEventFilter(new LambdaEventFilter(view, [view](QObject* w, QEvent* e) {
            if (e->type() == QEvent::MouseButtonPress) {
                auto* me = static_cast<QMouseEvent*>(e);
                // initiate drag
                if (me->button() == Qt::LeftButton) {
                    view->setDragMode(QGraphicsView::RubberBandDrag);
                }
            } else if (e->type() == QEvent::MouseButtonRelease) { // disable drag after end
                QTimer::singleShot(1, [view] {
                    view->setDragMode(QGraphicsView::NoDrag);
                });
            }
            return w->QObject::eventFilter(w, e);
        }));

        connect(new QShortcut(QKeySequence("Esc"), view), &QShortcut::activated, this, [this] { ui->patchboardBreadcrumbs->up(); });

    }

    { /* Set up sample list */ } {
        // model
        auto mdl = new SampleListModel(ui->sampleList, this);
        ui->sampleList->setModel(mdl);

        connect(ui->sampleList->selectionModel(), &QItemSelectionModel::currentChanged, this, [this, mdl](const QModelIndex& ind, const QModelIndex& old [[maybe_unused]]) {
            selectSampleForEditing(mdl->itemAt(ind));
        });
    }

    // Set up signaling from project to UI
    socket->setParent(this);
    socket->window = this;
    socket->undoStack = undoStack;
    connect(socket, &UISocket::updatePatternLists, this, &MainWindow::updatePatternLists);
    connect(socket, &UISocket::patternUpdated, this, [this](Pattern* p) {
        if (editingPattern.get() != p) return;
        ui->patternEditor->refresh();
    });
    connect(socket, &UISocket::rowUpdated, this, [this](Pattern* p, int ch, int r) {
        if (editingPattern.get() != p) return;
        const auto cpc = PatternEditorModel::colsPerChannel;
        auto ind = ui->patternEditor->model()->index(r, ch * cpc);
        emit ui->patternEditor->model()->dataChanged(ind, ind.siblingAtColumn((ch+1)*cpc-1));
        static_cast<PatternEditorModel*>(ui->patternEditor->model())->updateColumnDisplay();
    });
    connect(socket, &UISocket::openGraph, this, [this](Graph* g) {
        if (!g) return;
        auto gg = std::static_pointer_cast<Graph>(g->shared_from_this());
        QString name = gg->name;
        if (name.isEmpty()) name = gg->pluginName();
        ui->patchboardBreadcrumbs->push(name, this, [this, gg] {
            openGraph(gg);
        });
    });
    connect(socket, &UISocket::openNodeUI, this, [this](Node* n) {
        if (!n) return;
        auto nn = n->shared_from_this();
        QString name = nn->name;
        if (name.isEmpty()) name = nn->pluginName();
        ui->patchboardBreadcrumbs->push(name, this, [this, nn] {
            openNodeUI(nn);
        });
    });

    // and from audio engine
    connect(audioEngine, &AudioEngine::playbackModeChanged, this, [this, undoAction, redoAction] {
        bool locked = project->editingLocked();
        undoAction->setEnabled(!locked);
        redoAction->setEnabled(!locked);

        bool playingThis = audioEngine->playbackMode() == AudioEngine::Playing && audioEngine->playingProject() == project;
        if (playingThis) ui->playButton->setIcon(ui->playButton->style()->standardIcon(QStyle::SP_MediaStop));
        else ui->playButton->setIcon(ui->playButton->style()->standardIcon(QStyle::SP_MediaPlay));
    });

    selectSampleForEditing(nullptr); // init blank

    // and start with a new project
    menuFileNew();
}

MainWindow::~MainWindow() {
    if (audioEngine->playingProject() == project) audioEngine->stop();
    delete ui;
}

void MainWindow::closeEvent(QCloseEvent*) {
    undoStack->clear();
    setAttribute(Qt::WA_DeleteOnClose); // delete when done
}

bool MainWindow::eventFilter(QObject *obj [[maybe_unused]], QEvent *event) {
    if (event->type() == QEvent::KeyPress) {
        auto ke = static_cast<QKeyEvent*>(event);
        qDebug() << QString("key pressed: %1").arg((ke->key()));
        if (ke->key() == Qt::Key_Tab) {
            qDebug() << QString("tab");
            auto* t = this->ui->tabWidget;
            t->setTabPosition(QTabWidget::TabPosition::South);
            t->setCurrentIndex(t->currentIndex() + 1);
            return true;
        }
    }
    return false;
}

void MainWindow::menuFileNew() {
    auto hold = project; // keep alive until done
    if (audioEngine->playingProject() == project) audioEngine->stop();
    project = std::make_shared<Project>();
    project->sequence.emplace_back(project->newPattern());

    onNewProjectLoaded();
}

void MainWindow::menuFileOpen() {
    if (auto fileName = FileOps::showOpenDialog(this, "Open project...", Config::Directories::projects, FileOps::Filter::project); !fileName.isEmpty()) {
        auto np = FileOps::loadProject(fileName);
        if (!np) {
            QMessageBox::critical(this, "Error", "Error loading project");
            return;
        }
        if (audioEngine->playingProject() == project) audioEngine->stop();
        project = np;
        onNewProjectLoaded();
    }
}

void MainWindow::menuFileSave() {
    if (project->fileName.isEmpty()) menuFileSaveAs();
    else {
        FileOps::saveProject(project);
        undoStack->setClean();
    }
}

void MainWindow::menuFileSaveAs() {
    QString saveDir = Config::Directories::projects;
    if (!project->fileName.isEmpty()) {
        QFileInfo f(project->fileName);
        saveDir = f.dir().filePath(f.baseName());
    }
    if (auto fileName = FileOps::showSaveAsDialog(this, "Save project as...", saveDir, FileOps::Filter::project, "xyp"); !fileName.isEmpty()) {
        FileOps::saveProject(project, fileName);
        undoStack->setClean();
        updateTitle();
    }
}

void MainWindow::menuFileExport() {
    if (project->exportFileName.isEmpty()) menuFileExportAs();
    else {
        audioEngine->render(project, project->exportFileName);
    }
}

void MainWindow::menuFileExportAs() {
    QString saveDir = Config::Directories::projects;
    if (!project->fileName.isEmpty()) {
        QFileInfo f(project->fileName);
        saveDir = f.dir().filePath(f.baseName());
    }
    if (auto fileName = FileOps::showSaveAsDialog(this, "Save project as...", saveDir, FileOps::Filter::audioOut, "mp3"); !fileName.isEmpty()) {
        project->exportFileName = fileName;
        audioEngine->render(project, project->exportFileName);
    }
}

void MainWindow::menuFileNewWindow() {
    auto w = new MainWindow();
    w->show();
}

void MainWindow::onNewProjectLoaded() {
    undoStack->clear();

    project->socket = socket;
    updatePatternLists();
    patternSelection(0);
    sequenceSelection(-1);
    for (size_t i = 0; i < project->sequence.size(); i++) { // find first actual pattern in sequence, else fall back to first pattern numerically
        if (!project->sequence[i].pattern()) continue;
        sequenceSelection(static_cast<int>(i));
        break;
    }

    //openGraph(project->rootGraph);
    ui->patchboardBreadcrumbs->clear();
    ui->patchboardBreadcrumbs->push("/", this, [this, gg = project->rootGraph] {
        openGraph(gg);
    });

    ui->editTempo->setValue(project->tempo);

    ui->editArtist->setText(project->artist);
    ui->editTitle->setText(project->title);
    ui->editComment->document()->setPlainText(project->comment);

    updateTitle();
    setSongInfoPaneExpanded(false);

    emit projectLoaded();
}

int MainWindow::patternSelection(int n) {
    auto i = ui->patternList->currentIndex();
    if (n >= -1) ui->patternList->setCurrentIndex(ui->patternList->model()->index(n, 0));
    return i.isValid() ? i.row() : -1;
}

int MainWindow::sequenceSelection(int n) {
    auto i = ui->patternSequencer->currentIndex();
    if (n >= -1) ui->patternSequencer->setCurrentIndex(ui->patternSequencer->model()->index(0, n));
    return i.isValid() ? i.column() : -1;
}

void MainWindow::playbackPosition(int seq, int row) {
    sequenceSelection(seq);
    if (ui->patternEditor->isFolded() && editingPattern->fold > 1) row -= row % editingPattern->fold;
    auto mi = ui->patternEditor->currentIndex().siblingAtRow(row+1);

    if (!mi.isValid()) mi = ui->patternEditor->model()->index(row+1, 0);
    ui->patternEditor->setCurrentIndex(mi);
    ui->patternEditor->selectionModel()->select(QItemSelection(mi.siblingAtColumn(0), mi.siblingAtColumn(ui->patternEditor->horizontalHeader()->count()-1)), QItemSelectionModel::SelectionFlag::ClearAndSelect);
    ui->patternEditor->scrollTo(mi, QAbstractItemView::PositionAtCenter);

}

void MainWindow::updatePatternLists() {
    emit ui->patternList->model()->layoutChanged();
    emit ui->patternSequencer->model()->layoutChanged();
    if (auto i = ui->patternList->currentIndex(); i.isValid() && !ui->patternSequencer->currentIndex().isValid())
        selectPatternForEditing(project->patterns[static_cast<size_t>(i.row())].get()); // make sure pattern editor matches selection
    if (editingPattern && !editingPattern->validFor(project)) // if current pattern invalidated, select new one
        selectPatternForEditing(project->patterns[std::min(editingPattern->index, project->patterns.size() - 1)].get());
}

void MainWindow::updateTitle() {
    if (!project) return;
    QString songTitle;
    if (project->title.isEmpty()) {
        if (project->fileName.isEmpty()) songTitle = qs("(new project)");
        else songTitle = QFileInfo(project->fileName).baseName();
    } else {
        if (!project->artist.isEmpty()) songTitle = qs("%1 - %2").arg(project->artist).arg(project->title);
        else songTitle = project->title;
    }

    if (!undoStack->isClean()) songTitle.append(" (modified)");
    ui->labelSongInfo->setText(songTitle);
    setWindowTitle(qs("Xybrid - ") % songTitle);
}

void MainWindow::setSongInfoPaneExpanded(bool open) {
    if (open) {
        ui->songInfoPane->setCurrentIndex(1);
    } else {
        ui->songInfoPane->setCurrentIndex(0);
        ui->patternEditor->setFocus(Qt::FocusReason::ShortcutFocusReason);
    }
    auto s = ui->songInfoPane->currentWidget()->minimumHeight();
    ui->songInfoPane->setMinimumHeight(s);
    ui->songInfoPane->setMaximumHeight(s);
}

bool MainWindow::selectPatternForEditing(Pattern* pattern) {
    if (!pattern || pattern == editingPattern.get()) return false; // no u
    if (pattern->project != project.get()) return false; // wrong project
    if (project->patterns.size() <= pattern->index) return false; // invalid id
    auto sp = project->patterns[pattern->index];
    if (sp.get() != pattern) return false; // wrong index
    auto hold = editingPattern; // keep alive until done
    editingPattern = sp;

    ui->patternEditor->setPattern(editingPattern);
    ui->patternList->setCurrentIndex(ui->patternList->model()->index(static_cast<int>(editingPattern->index), 0));

    return true;
}

void MainWindow::selectSampleForEditing(std::shared_ptr<Xybrid::Data::Sample> smp) {
    if (!smp || smp->project != project.get()) { // no valid sample selected
        ui->sampleViewPane->setEnabled(false);
        ui->sampleInfo->setText(qs("(no sample selected)"));
    } else {
        ui->sampleViewPane->setEnabled(true);
        ui->sampleInfo->setText(
            qs("%1 // %2\n%3 %4Hz, %5 frames (%6)")
            .arg(smp->name.section('/', -1, -1))
            .arg(smp->uuid.toString())
            .arg(smp->numChannels() == 2 ? qs("Stereo") : qs("Mono"))
            .arg(smp->sampleRate)
            .arg(smp->length())
            .arg(Util::sampleLength(smp->sampleRate, smp->length()))
        );
    }
    ui->waveformPreview->setSample(smp);
}

void MainWindow::openGraph(const std::shared_ptr<Data::Graph>& g) {
    if (!g) return; // invalid
    QPointF scrollPt(g->viewX, g->viewY);
    //delete ui->patchboardView->scene();
    if (auto s = ui->patchboardView->scene(); s) s->deleteLater();
    //ui->patchboardView->setScene(nullptr);
    ui->patchboardView->setScene(new PatchboardScene(ui->patchboardView, g));
    auto sz = ui->patchboardView->viewport()->visibleRegion().boundingRect().size();
    ui->patchboardView->centerOn(scrollPt + QPointF(sz.width()/2, sz.height()/2));
}

void MainWindow::openNodeUI(const std::shared_ptr<Data::Node>& n) {
    if (!n) return;
    //delete ui->patchboardView->scene();
    if (auto s = ui->patchboardView->scene(); s) s->deleteLater();
    ui->patchboardView->setScene(new NodeUIScene(ui->patchboardView, n));
    // scene handles initial scroll; don't need to do it here
}

void MainWindow::openPatternProperties(const std::shared_ptr<Xybrid::Data::Pattern>& p) {
    if (!p || p->project != project.get()) return;
    auto dlg = new QDialog(this);
    dlg->setWindowTitle("Pattern Properties");
    dlg->setModal(true);
    dlg->setAttribute(Qt::WA_DeleteOnClose);
    dlg->setSizePolicy(QSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed));
    //dlg->resize(320, 200);
    auto dl = new QVBoxLayout();
    dlg->setLayout(dl);
    //dlg->layout()->setMargin(3);

    auto eName = new QLineEdit(p->name);
    dl->addWidget(eName);
    //dlg->layout()->setAlignment(eName, Qt::AlignTop);

    auto gLength = new QHBoxLayout();
    dl->addLayout(gLength);
    gLength->addWidget(new QLabel("Length"));
    auto gLengthBox = new QSpinBox();
    gLengthBox->setRange(1, 1024);
    gLengthBox->setValue(p->rows);
    gLengthBox->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
    gLength->addWidget(gLengthBox);
    gLength->addWidget(new QLabel("Fold"));
    auto gFoldBox = new QSpinBox();
    gFoldBox->setRange(0, 128);
    gFoldBox->setValue(p->fold);
    gFoldBox->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
    gLength->addWidget(gFoldBox);

    auto gTime = new QHBoxLayout();
    dl->addLayout(gTime);
    auto gTimeBoxW = 60;
    auto gTimeBoxMax = 32;
    gTime->addWidget(new QLabel("Beats"));
    auto gTimeBeats = new QSpinBox();
    gTimeBeats->setRange(1, gTimeBoxMax);
    gTimeBeats->setValue(p->time.beatsPerMeasure);
    gTimeBeats->setMaximumWidth(gTimeBoxW);
    gTime->addWidget(gTimeBeats);
    gTime->addWidget(new QLabel("Rows"));
    auto gTimeRows = new QSpinBox();
    gTimeRows->setRange(1, gTimeBoxMax);
    gTimeRows->setValue(p->time.rowsPerBeat);
    gTimeRows->setMaximumWidth(gTimeBoxW);
    gTime->addWidget(gTimeRows);
    gTime->addWidget(new QLabel("Ticks"));
    auto gTimeTicks = new QSpinBox();
    gTimeTicks->setRange(1, gTimeBoxMax);
    gTimeTicks->setValue(p->time.ticksPerRow);
    gTimeTicks->setMaximumWidth(gTimeBoxW);
    gTime->addWidget(gTimeTicks);

    auto bbox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
    dl->addWidget(bbox);
    connect(bbox, &QDialogButtonBox::rejected, dlg, &QDialog::reject);
    connect(bbox, &QDialogButtonBox::accepted, dlg, [=] {
        auto cc = new CompositeCommand();
        if (auto n = eName->text(); n != p->name) cc->compose(new PatternRenameCommand(p, n));
        if (auto t = Data::TimeSignature(gTimeBeats->value(), gTimeRows->value(), gTimeTicks->value()); t != p->time)
            cc->compose(new PatternTimeSignatureCommand(p, t));
        if (auto nr = gLengthBox->value(); nr != p->rows) {
            if (nr < p->rows) { // preserve contents beyond cutoff
                for (auto r = nr; r < p->rows; r++) {
                    for (auto c = 0; c < static_cast<int>(p->numChannels()); c++) {
                        cc->compose(new PatternDeltaCommand(p, c, r));
                    }
                }
            }
            cc->compose(new PatternLengthCommand(p, nr));
        }
        if (auto f = gFoldBox->value(); f != p->fold) cc->compose(new PatternFoldCommand(p, f));

        cc->commit("edit pattern properties");
        dlg->accept();
    });
    dlg->show();
}
