#include "patterneditorview.h"
using Xybrid::UI::PatternEditorView;
#include "ui/patterneditormodel.h"
using Xybrid::UI::PatternEditorModel;
#include "ui/patterneditoritemdelegate.h"
using Xybrid::UI::PatternEditorItemDelegate;

#include "util/strings.h"
#include "util/keys.h"

#include "ui/channelheaderview.h"
using Xybrid::UI::ChannelHeaderView;

#include "data/project.h"
using Xybrid::Data::Project;
using Xybrid::Data::Pattern;

#include "editing/compositecommand.h"
#include "editing/patterncommands.h"
using namespace Xybrid::Editing;

#include "audio/audioengine.h"
using namespace Xybrid::Audio;

#include "util/pattern.h"

#include <QKeyEvent>
#include <QShortcut>
#include <QTimer>
#include <QGuiApplication>
#include <QClipboard>
#include <QMimeData>
#include <QCborArray>
#include <QCborMap>
#include <QDebug>

#include <QHeaderView>
#include <QScrollBar>
#include <QHBoxLayout>
#include <QMenu>
#include <QTextEdit>
#include <QInputDialog>
#include <QMessageBox>


namespace {
    std::shared_ptr<Pattern> pt = std::make_shared<Pattern>(1, 0); // fallback pattern
}

PatternEditorView::PatternEditorView(QWidget *parent) : QTableView(parent) {
    hdr.reset(new ChannelHeaderView(this));
    hdr->setSectionResizeMode(QHeaderView::Fixed);
    hdr->setTextElideMode(Qt::ElideNone);//Middle);
    hdr->setStretchLastSection(true);
    hdr->setSectionsMovable(true);
    hdr->setFirstSectionMovable(true);
    hdr->setEditTriggers(EditTrigger::DoubleClicked);

    // hook up scrolling to update header position
    connect(horizontalScrollBar(), &QScrollBar::valueChanged, this, &PatternEditorView::updateHeaderOffset);
    // and header swap
    connect(hdr.get(), &QHeaderView::sectionMoved, this, &PatternEditorView::headerMoved);

    // hook up context menu
    hdr->setContextMenuPolicy(Qt::CustomContextMenu);
    connect(hdr.get(), &QHeaderView::customContextMenuRequested, this, &PatternEditorView::headerContextMenu);

    connect(hdr.get(), &QHeaderView::sectionDoubleClicked, this, &PatternEditorView::headerDoubleClicked);

    horizontalHeader()->setSectionResizeMode(QHeaderView::Fixed);//ResizeToContents);
    horizontalHeader()->setStretchLastSection(true);
    verticalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
    verticalHeader()->setMinimumSectionSize(0);
    //verticalHeader()->setStretchLastSection(true);
    verticalHeader()->setSectionsClickable(false);
    setCornerButtonEnabled(false);
    //verticalHeader()->setDefaultAlignment(Qt::AlignTop);

    cornerBoxBox.reset(new QWidget(this));
    QHBoxLayout *layoutCheckBox = new QHBoxLayout(cornerBoxBox.get());
    cornerBox.reset(new QCheckBox());
    layoutCheckBox->addWidget(cornerBox.get());
    layoutCheckBox->setAlignment(Qt::AlignCenter);
    layoutCheckBox->setContentsMargins(0,0,0,0);
    cornerBox->setToolTip("Expand to fit names");
    cornerBox->setFocusPolicy(Qt::NoFocus);
    connect(cornerBox.get(), &QCheckBox::toggled, this, [this](bool state) {
        mdl->fitHeaderToName = state;
        mdl->updateColumnDisplay();
    });

    mdl.reset(new PatternEditorModel(this));
    del.reset(new PatternEditorItemDelegate(this));
    setItemDelegate(&*del);
    mdl->setPattern(pt);
    setModel(&*mdl);
    hdr->setModel(&*mdl->hprox);

    // smooth scroll
    setVerticalScrollMode(ScrollMode::ScrollPerPixel);
    setHorizontalScrollMode(ScrollMode::ScrollPerPixel);

    { /* set up hotkeys */ } {
        // transpose notes
        auto transpose = [this](int amt, int key = Qt::Key_Alt) {
            auto p = mdl->getPattern();
            auto sel = this->selectionModel()->selection().first();
            auto startCol = sel.left() % Util::colsPerChannel;
            if (sel.width() == 1 && startCol != 1) {
                if (startCol == 0) { // port
                    amt = std::clamp(amt, -16, 16);
                    auto cc = new CompositeCommand();
                    auto ch = Util::channelForColumn(sel.left());
                    auto last = sel.bottom();
                    for (auto i = sel.top(); i <= last; i++) {
                        if (p->rowAt(ch, i-1).port >= 0) {
                            auto c = new PatternDeltaCommand(p, ch, i-1);
                            auto op = c->row.port;
                            c->row.port = static_cast<int16_t>(std::clamp(op + amt, 0, 255));
                            if (c->row.port == op) c->cancel();
                            else cc->compose(c);
                        }
                    }
                    cc->commit("change note port(s)");
                } else if (startCol >= 2 && sel.height() == 1) { // single param
                    amt = std::clamp(amt, -16, 16);
                    auto c = new PatternDeltaCommand(p, Util::channelForColumn(sel.left()), sel.top()-1);
                    auto& r = c->row;
                    auto par = static_cast<size_t>((sel.left() % Util::colsPerChannel) - 2) / 2;
                    if (r.numParams() > par) {
                        if (r.param(par)[0] != ' ') r.param(par)[1] = static_cast<uint8_t>(std::clamp(static_cast<int>(r.param(par)[1]) + amt, 0, 255));
                        c->commit();
                    } else c->cancel();
                }
            } else { // note(s)
                amt = std::clamp(amt, -12, 12);
                auto cc = new CompositeCommand();
                for (auto s : sel.indexes()) {
                    if (s.column() % Util::colsPerChannel != 1) continue;
                    int ch = Util::channelForColumn(s.column());
                    auto c = new PatternDeltaCommand(p, ch, s.row()-1);
                    if (c->row.note >= 0) {
                        c->row.note = static_cast<int16_t>(std::max(c->row.note + amt, 0));
                        cc->compose(c);
                    } else c->cancel();
                }
                cc->commit("transpose note(s)");
                startPreview(key);
            }


        };

        connect(new QShortcut(QKeySequence("Alt+Up"), this), &QShortcut::activated, this, [transpose] { transpose(1, Qt::Key_Up); });
        connect(new QShortcut(QKeySequence("Alt+Down"), this), &QShortcut::activated, this, [transpose] { transpose(-1, Qt::Key_Down); });
        connect(new QShortcut(QKeySequence("Alt+Right"), this), &QShortcut::activated, this, [transpose] { transpose(100, Qt::Key_Right); });
        connect(new QShortcut(QKeySequence("Alt+Left"), this), &QShortcut::activated, this, [transpose] { transpose(-100, Qt::Key_Left); });

        // fold
        connect(new QShortcut(QKeySequence("Ctrl+Space"), this), &QShortcut::activated, this, [this] {
            setUpdatesEnabled(false);
            mdl->toggleFold();
            auto p = mdl->getPattern();
            auto ind = currentIndex();
            int row = ind.row() - 1;
            if (mdl->folded && p->fold > 1 && row % p->fold != 0) {
                ind = ind.siblingAtRow(1 + row - (row % p->fold));
                setCurrentIndex(ind);
                if (selectedIndexes().count() <= 1) selectionModel()->select(ind, QItemSelectionModel::SelectionFlag::ClearAndSelect);
            }
            scrollTo(ind, ScrollHint::PositionAtCenter);
            setUpdatesEnabled(true);
            //QTimer::singleShot(1, [this, ind]{ scrollTo(ind, ScrollHint::PositionAtCenter); });
        });

        // cut/copy/paste
        auto copy = [this] {
            auto* clip = QGuiApplication::clipboard();
            auto* data = new QMimeData();

            auto p = mdl->getPattern();
            auto sel = selectionModel()->selection()[0];
            auto chMin = Util::channelForColumn(sel.left());
            auto chMax = Util::channelForColumn(sel.right());

            // cbor data format: (may change between versions, assume a given system only has one Xybrid version installed)
            // [ integer first, last, n*[ integer port, note, (param, val)... ]... ]...
            QCborArray root;

            for (int ch = chMin; ch <= chMax; ch++) {
                QCborArray chm;
                int first = 0, last = 255;
                if (ch == chMin) first = Util::fieldForColumn(sel.left());
                if (ch == chMax) last = Util::fieldForColumn(sel.right());

                chm << first << last;
                for (int r = sel.top(); r <= sel.bottom(); r++) {
                    QCborArray rm;
                    auto& row = p->rowAt(ch, r-1);
                    rm << row.port;
                    rm << row.note;
                    if (row.params) for (auto p : *row.params) rm << p[0] << p[1];
                    chm << rm;
                }
                root << chm;
            }

            data->setData("xybrid-internal/x-pattern-copy", root.toCborValue().toCbor());
            clip->setMimeData(data);
        };

        connect(new QShortcut(QKeySequence(QKeySequence::StandardKey::Cut), this), &QShortcut::activated, this, [this, copy] {
            copy();
            this->edit(currentIndex(), EditTrigger::EditKeyPressed, new QKeyEvent(QKeyEvent::Type::KeyPress, Qt::Key_Delete, Qt::KeyboardModifier::NoModifier));
        });
        connect(new QShortcut(QKeySequence(QKeySequence::StandardKey::Copy), this), &QShortcut::activated, this, [copy] { copy(); });
        connect(new QShortcut(QKeySequence(QKeySequence::StandardKey::Paste), this), &QShortcut::activated, this, [this] {
            const auto* clip = QGuiApplication::clipboard();
            const auto* data = clip->mimeData();

            if (!data->hasFormat("xybrid-internal/x-pattern-copy")) return; // no pattern data

            auto p = mdl->getPattern();
            auto idx = currentIndex();
            auto chMin = Util::channelForColumn(idx.column());
            auto rMin = idx.row()-1;

            auto root = QCborValue::fromCbor(data->data("xybrid-internal/x-pattern-copy")).toArray();

            auto cc = new CompositeCommand();

            for (int ch = 0; ch < static_cast<int>(p->numChannels()) - chMin && ch < root.size(); ch++) {
                auto chm = root[ch].toArray();

                int first = static_cast<int>(chm[0].toInteger());
                int last = static_cast<int>(chm[1].toInteger());

                int pOff = 0;
                if (ch == 0 && first >= 2) pOff = std::max(0, Util::fieldForColumn(idx.column()) - first);

                for (int r = 0; r < p->rows - rMin && r < chm.size() - 2; r++) {
                    auto rm = chm[r+2].toArray();
                    auto c = new PatternDeltaCommand(p, ch+chMin, r+rMin);
                    auto& row = c->row;

                    if (first <= 0 && last >= 0) row.port = static_cast<int16_t>(rm[0].toInteger());
                    if (first <= 1 && last >= 1) row.note = static_cast<int16_t>(rm[1].toInteger());
                    for (int p = 0; p < (rm.size() - 2) / 2 && p+pOff < Util::paramSoftCap; p++)
                        if (first <= p+2 && last >= p+2) row.setParam(static_cast<size_t>(p+pOff), static_cast<char>(rm[p*2+2].toInteger()), static_cast<unsigned char>(rm[p*2+3].toInteger()));

                    cc->compose(c);
                }
            }

            cc->commit("paste pattern data");
        });
    }
}

PatternEditorView::~PatternEditorView() {
    //
    /*mdl.release();
    del.release();
    hdr.release();
    cornerBoxBox.release();
    cornerBox.release();*/
    //horizontalHeader()->deleteLater();
}

void PatternEditorView::keyPressEvent(QKeyEvent* e) {
    if (/*event->modifiers() & Qt::Modifier::CTRL &&*/ (e->key() == Qt::Key_Tab || e->key() == Qt::Key_Backtab)) { // don't block ctrl+tab
        e->ignore();
        return;
        //QKeyEvent()
    }
    if (e->key() == Qt::Key_Delete || e->key() == Qt::Key_Backspace || e->key() == Qt::Key_Insert) {
        if (!edit(currentIndex(), AnyKeyPressed, e)) {
            e->ignore();
            return;
        }
    }
    auto prevInd = currentIndex();
    QTableView::keyPressEvent(e);
    if (auto ind = currentIndex(); ind != prevInd) scrollTo(ind, ScrollHint::PositionAtCenter); // cursor position changed, scroll to keep up
    if (!e->isAutoRepeat()) {
        if (Util::keyToNote(e->key()) >= 0 || (e->key() >= Qt::Key_0 && e->key() <= Qt::Key_9) || e->key() == Qt::Key_Space) { // note-related key
            startPreview(Util::unshiftedKey(e->key()));
        }
    }
}

void PatternEditorView::keyReleaseEvent(QKeyEvent* e) {
    QAbstractItemView::keyReleaseEvent(e);
    if (!e->isAutoRepeat()) stopPreview(Util::unshiftedKey(e->key()));
}

void PatternEditorView::mouseReleaseEvent(QMouseEvent* e) {
    QTableView::mouseReleaseEvent(e);
    auto sm = selectionModel();
    if (sm->selectedIndexes().size() <= 1) {
        auto ind = indexAt(e->localPos().toPoint());
        if (ind.isValid() && model()->flags(ind) & Qt::ItemFlag::ItemIsEnabled) scrollTo(ind, ScrollHint::PositionAtCenter);
    }
}

void PatternEditorView::startPreview(int key) {
    auto ind = currentIndex();
    int cc = ind.column() % PatternEditorModel::colsPerChannel;
    int ch = (ind.column() - cc) / PatternEditorModel::colsPerChannel;
    if (cc == 1) { // note column
        stopPreview(key); // end current preview first, if applicable
        auto& r = mdl->getPattern()->rowAt(ch, ind.row()-1);
        auto p = mdl->getPattern()->project->shared_from_this();
        previewKey[key] = {r.port, audioEngine->preview(p, r.port, r.note)};
    }
}

void PatternEditorView::stopPreview(int key) {
    if (auto k = previewKey.find(key); k != previewKey.end()) {
        auto p = mdl->getPattern()->project->shared_from_this();
        audioEngine->preview(p, k->second.first, -2, k->second.second);
        previewKey.erase(k);
    }
}

void PatternEditorView::setPattern(const std::shared_ptr<Pattern>& pattern) {
    setUpdatesEnabled(false);
    mdl->setPattern(pattern);

    //rowCountChanged(0, mdl->rowCount());

    auto cur = currentIndex();
    auto ind = model()->index(std::clamp(cur.row(), 1, pattern->rows), std::clamp(cur.column(), 0, std::max(0, mdl->columnCount() - 2)));
    while (isColumnHidden(ind.column())) ind = ind.siblingAtColumn(ind.column()-1);
    while (isRowHidden(ind.row())) ind = ind.siblingAtRow(ind.row()-1);
    setCurrentIndex(ind);
    selectionModel()->select(ind, QItemSelectionModel::ClearAndSelect);
    scrollTo(ind, ScrollHint::PositionAtCenter);
    setUpdatesEnabled(true);
}

void PatternEditorView::updateGeometries() {
    if (mdl && colUpdateNeeded && horizontalHeader()->count() > 0) {
        mdl->updateColumnDisplay();
        colUpdateNeeded = false;
    } else updateHeader(true); // do this once on every geom update
    if (cornerBox) {
        cornerBoxBox->setGeometry(verticalHeader()->x(), horizontalHeader()->y(), verticalHeader()->width(), horizontalHeader()->height());
        //cornerBox->move(verticalHeader()->x() + verticalHeader()->width() / 2 - cornerBox->minimumWidth() / 2, horizontalHeader()->y() + cornerBox->height() / 2);
    }
    this->QTableView::updateGeometries();
}

void PatternEditorView::updateHeader(bool full) {
    auto* bh = horizontalHeader(); // base header
    if (full) {
        hdr->reset(); // force section update
        for (int i = 0; i < hdr->count(); i++) { // set sizes
            constexpr int cpc = PatternEditorModel::colsPerChannel;
            int w = 0;
            for (int j = 0; j < cpc; j++) {
                w += bh->sectionSize(i*cpc+j);
            }
            hdr->resizeSection(i, w);
        }
    }
    hdr->setGeometry(bh->x(), bh->y(), bh->width(), bh->height());
    hdr->setOffset(bh->offset());
}

void PatternEditorView::refresh() {
    mdl->refresh();
}

bool PatternEditorView::isFolded() {
    return mdl->folded;
}
void PatternEditorView::updateHeaderOffset(int) {
    updateHeader(false);
}

void PatternEditorView::headerMoved(int logicalIndex, int oldVisualIndex, int newVisualIndex) {
    if (logicalIndex == newVisualIndex) return; // assume moving back
    hdr->moveSection(newVisualIndex, logicalIndex); // maintain straight-through order
    if (logicalIndex >= hdr->count() - 1) return; // no dragging the endcap :|

    (new PatternChannelMoveCommand(mdl->getPattern(), oldVisualIndex, newVisualIndex))->commit();
}

void PatternEditorView::headerDoubleClicked(int section) {
    startRenameChannel(section);
}

void PatternEditorView::headerContextMenu(QPoint pt) {
    int idx = hdr->logicalIndexAt(pt);
    std::shared_ptr<Pattern> p = mdl->getPattern();

    QMenu* menu = new QMenu(this);
    menu->addAction("Add Channel", this, [/*this,*/ idx, p]() {
        (new PatternChannelAddCommand(p, idx))->commit();
    });
    if (idx < hdr->count() - 1) {
        menu->addAction("Delete Channel", this, [this, idx, p]() {
            if (QMessageBox::warning(this, "Are you sure?", QString("Remove channel %1 from pattern %2?").arg(Util::numAndName(idx, p->channel(idx).name)).arg(Util::numAndName(p->index, p->name)), QMessageBox::Yes | QMessageBox::No, QMessageBox::No) != QMessageBox::Yes) return;
            (new PatternChannelDeleteCommand(p, idx))->commit();
        });
        menu->addAction("Rename Channel...", this, [this, idx, p]() {
            if (p != mdl->getPattern()) return; // swapped already
            startRenameChannel(idx);
        });
    }
    menu->setAttribute(Qt::WA_DeleteOnClose);
    menu->popup(hdr->mapToGlobal(pt));

}

void PatternEditorView::startRenameChannel(int channel) {
    auto p = mdl->getPattern();
    if (static_cast<size_t>(channel) >= p->numChannels()) return;
    auto c = &p->channel(channel);
    bool ok = false;
    auto capt = QString("Rename channel %1:").arg(channel);
    auto n = QInputDialog::getText(this, "Rename...", capt, QLineEdit::Normal, c->name, &ok);
    if (!ok) return; // canceled
    if (p != mdl->getPattern() || c != &p->channel(channel)) return; // abort if this somehow isn't the channel it was before
    (new PatternChannelRenameCommand(p, channel, n))->commit();
}
