#include "tracewidget.h"

#include "ui_tracewidget.h"
#include "traceeditdialog.h"
#include "traceimportdialog.h"
#include "tracetouchstoneexport.h"
#include "trace.h"
#include "unit.h"
#include "Util/util.h"
#include "appwindow.h"

#include <QKeyEvent>
#include <QFileDialog>
#include <QDrag>
#include <QMimeData>
#include <QDebug>
#include <QMenu>


TraceWidget::TraceWidget(TraceModel &model, QWidget *parent) :
    QWidget(parent),
    SCPINode("TRACe"),
    dragTrace(nullptr),
    ui(new Ui::TraceWidget),
    model(model)
{
    ui->setupUi(this);
    ui->view->setModel(&model);
    ui->view->setAutoScroll(false);
    ui->view->viewport()->installEventFilter(this);
    connect(ui->bImport, &QPushButton::clicked, this, &TraceWidget::importDialog);
    connect(ui->bExport, &QPushButton::clicked, this, &TraceWidget::exportDialog);
    installEventFilter(this);
    createCount = 0;
    SetupSCPI();
}

TraceWidget::~TraceWidget()
{
    delete ui;
}

void TraceWidget::on_add_clicked()
{
    createCount++;
    auto t = new Trace("Trace #"+QString::number(createCount), Qt::darkYellow, defaultParameter());
    t->setColor(QColor::fromHsl((createCount * 50) % 360, 250, 128));
    model.addTrace(t);
    ui->view->selectRow(model.getTraces().size() - 1);
    on_edit_clicked();
}

void TraceWidget::on_remove_clicked()
{
    QModelIndex index = ui->view->currentIndex();
    if (index.isValid()) {      // if nothing clicked, index.row() = -1
        model.removeTrace(index.row());
        // otherwise, TraceModel casts index to unsigned int and compares with traces.size() which is int
    };
}

bool TraceWidget::eventFilter(QObject *, QEvent *event)
{
    if (event->type() == QEvent::KeyPress) {
        int key = static_cast<QKeyEvent *>(event)->key();
        if(key == Qt::Key_Escape) {
            ui->view->clearSelection();
            return true;
        } else if(key == Qt::Key_Delete) {
            model.removeTrace(ui->view->currentIndex().row());
            return true;
        }
    } else if(event->type() == QEvent::MouseButtonPress) {
        auto mouseEvent = static_cast<QMouseEvent*>(event);
        if (mouseEvent->button() == Qt::LeftButton) {
            auto index = ui->view->indexAt(mouseEvent->pos());
            if(index.isValid()) {
                dragStartPosition = mouseEvent->pos();
                dragTrace = model.trace(index.row());
            } else {
                dragTrace = nullptr;
            }
        }
        return false;
    } else if(event->type() == QEvent::MouseMove) {
        auto mouseEvent = static_cast<QMouseEvent*>(event);
        if (!(mouseEvent->buttons() & Qt::LeftButton)) {
            return false;
        }
        if (!dragTrace) {
            return false;
        }
        if ((mouseEvent->pos() - dragStartPosition).manhattanLength()
             < QApplication::startDragDistance()) {
            return false;
        }

        QDrag *drag = new QDrag(this);
        QMimeData *mimeData = new QMimeData;

        QByteArray encodedPointer;
        QDataStream stream(&encodedPointer, QIODevice::WriteOnly);
        stream << quintptr(dragTrace);

        mimeData->setData("trace/pointer", encodedPointer);
        drag->setMimeData(mimeData);

        drag->exec(Qt::CopyAction);
        return true;
    }
    return false;
}

void TraceWidget::on_edit_clicked()
{
    if(ui->view->currentIndex().isValid()) {
        auto edit = new TraceEditDialog(*model.trace(ui->view->currentIndex().row()));
        if(AppWindow::showGUI()) {
            edit->show();
        }
    }
}

void TraceWidget::on_view_doubleClicked(const QModelIndex &index)
{
    if(index.column() == TraceModel::ColIndexName) {
        auto edit = new TraceEditDialog(*model.trace(index.row()));
        if(AppWindow::showGUI()) {
            edit->show();
        }
    }
}

void TraceWidget::on_view_clicked(const QModelIndex &index)
{
    switch(index.column()) {
    case TraceModel::ColIndexVisible:
        model.toggleVisibility(index.row());
        break;
    case TraceModel::ColIndexPlayPause:
        model.togglePause(index.row());
        break;
    case TraceModel::ColIndexDeembedding:
        model.toggleDeembedding(index.row());
        break;
    case TraceModel::ColIndexMath:
        model.toggleMath(index.row());
        break;
    default:
        break;
    }
}

void TraceWidget::SetupSCPI()
{
    auto findTraceFromName = [=](QString name) -> Trace* {
        // check if trace is specified by number
        bool ok;
        auto n = name.toUInt(&ok);
        if(ok) {
            // check if enough traces exist
            if(n < model.getTraces().size()) {
                return model.getTraces()[n];
            } else {
                // invalid number
                return nullptr;
            }
        } else {
            // trace specified by name
            for(auto t : model.getTraces()) {
                if(t->name().compare(name, Qt::CaseInsensitive) == 0) {
                    return t;
                }
            }
            // not found
            return nullptr;
        }
    };
    auto findTrace = [=](QStringList params) -> Trace* {
        if(params.size() < 1) {
            return nullptr;
        }
        return findTraceFromName(params[0]);
    };

    auto createStringFromData = [](Trace *t, const Trace::Data &d) -> QString {
        if(Trace::isSAParameter(t->liveParameter())) {
            if(std::isnan(d.x)) {
                return "NaN";
            }
            return QString::number(Util::SparamTodB(d.y.real()));
        } else {
            if(std::isnan(d.x)) {
                return "NaN,NaN";
            }
            return QString::number(d.y.real())+","+QString::number(d.y.imag());
        }
    };

    add(new SCPICommand("LIST", nullptr, [=](QStringList){
       QString ret;
       for(auto t : model.getTraces()) {
           ret += t->name() + ",";
       }
       ret.chop(1);
       return  ret;
    }));
    add(new SCPICommand("DATA", nullptr, [=](QStringList params) -> QString {
        auto t = findTrace(params);
        if(!t) {
           return SCPI::getResultName(SCPI::Result::Error);
        }
        QString ret;
        if(t->size() > 0) {
            for(unsigned int i=0;i<t->size();i++) {
                auto d = t->sample(i);
                int precision = 0;
                switch(t->outputType()) {
                case Trace::DataType::Invalid:
                case Trace::DataType::Frequency: precision = 0; break;
                case Trace::DataType::Time: precision = 12; break;
                case Trace::DataType::Power: precision = 3; break;
                case Trace::DataType::TimeZeroSpan: precision = 4; break;
                }
                ret += "[" + QString::number(d.x, 'f', precision) + ","+createStringFromData(t, d)+"],";
            }
            ret.chop(1);
        } else {
            ret = "EMPTY";
        }
        return ret;
    }));
    add(new SCPICommand("AT", nullptr, [=](QStringList params) -> QString {
        auto t = findTrace(params);
        if(!t) {
           return SCPI::getResultName(SCPI::Result::Error);
        }
        double x;
        if(!SCPI::paramToDouble(params, 1, x)) {
            return SCPI::getResultName(SCPI::Result::Error);
        } else {
            auto d = t->interpolatedSample(x);
            if(std::isnan(d.x)) {
                return "NaN,NaN";
            } else {
                return createStringFromData(t, d);
            }
        }
    }));
    add(new SCPICommand("TOUCHSTONE", nullptr, [=](QStringList params) -> QString {
        if(params.size() < 1) {
            // no traces given
            return SCPI::getResultName(SCPI::Result::Error);
        }
        // check number of paramaters, must be a square number
        int numTraces = params.size();
        int ports = round(sqrt(numTraces));
        if(ports * ports != numTraces) {
            // invalid number of traces
            return SCPI::getResultName(SCPI::Result::Error);
        }
        Trace* traces[numTraces];
        for(int i=0;i<numTraces;i++) {
            traces[i] = findTraceFromName(params[i]);
            if(!traces[i]) {
                // couldn't find that trace
                return SCPI::getResultName(SCPI::Result::Error);
            }
        }
        // check if trace selection is valid
        auto npoints = traces[0]->size();
        auto f_start = traces[0]->minX();
        auto f_stop = traces[0]->maxX();
        for(int i=0;i<ports;i++) {
            for(int j=0;j<ports;j++) {
                bool need_reflection = i==j;
                auto t = traces[j+i*ports];
                if(t->getDataType() != Trace::DataType::Frequency) {
                    // invalid domain
                    return SCPI::getResultName(SCPI::Result::Error);
                }
                if(t->isReflection() != need_reflection) {
                    // invalid measurement at this position
                    return SCPI::getResultName(SCPI::Result::Error);
                }
                if((t->size() != npoints) || (t->minX() != f_start) || (t->maxX() != f_stop)) {
                    // frequency points are not identical
                    return SCPI::getResultName(SCPI::Result::Error);
                }
            }
        }
        // all traces checked, they are valid.
        // Constructing touchstone
        Touchstone t = Touchstone(ports);
        for(unsigned int i=0;i<npoints;i++) {
            Touchstone::Datapoint d;
            d.frequency = traces[0]->getSample(i).x;
            for(auto trace : traces) {
                d.S.push_back(trace->getSample(i).y);
            }
            t.AddDatapoint(d);
        }
        // touchstone assembled, save to dummyfile
        auto s = t.toString(Touchstone::Scale::GHz, Touchstone::Format::RealImaginary);
        return QString::fromStdString(s.str());
    }));
    add(new SCPICommand("MAXFrequency", nullptr, [=](QStringList params) -> QString {
        auto t = findTrace(params);
        if(!t) {
           return SCPI::getResultName(SCPI::Result::Error);
        }
        return QString::number(t->maxX(), 'f', 0);
    }));
    add(new SCPICommand("MINFrequency", nullptr, [=](QStringList params) -> QString {
        auto t = findTrace(params);
        if(!t) {
           return SCPI::getResultName(SCPI::Result::Error);
        }
        return QString::number(t->minX(), 'f', 0);
    }));
    add(new SCPICommand("MAXAmplitude", nullptr, [=](QStringList params) -> QString {
        auto t = findTrace(params);
        if(!t) {
           return SCPI::getResultName(SCPI::Result::Error);
        }
        auto d = t->interpolatedSample(t->findExtremum(true));
        return QString::number(d.x, 'f', 0)+","+createStringFromData(t, d);
    }));
    add(new SCPICommand("MINAmplitude", nullptr, [=](QStringList params) -> QString {
        auto t = findTrace(params);
        if(!t) {
           return SCPI::getResultName(SCPI::Result::Error);
        }
        auto d = t->interpolatedSample(t->findExtremum(false));
        return QString::number(d.x, 'f', 0)+","+createStringFromData(t, d);
    }));
    add(new SCPICommand("NEW", [=](QStringList params) -> QString {
        if(params.size() != 1) {
            return SCPI::getResultName(SCPI::Result::Error);
        }
        createCount++;
        auto t = new Trace(params[0], Qt::darkYellow, defaultParameter());
        t->setColor(QColor::fromHsl((createCount * 50) % 360, 250, 128));
        model.addTrace(t);
        return SCPI::getResultName(SCPI::Result::Empty);
    }, nullptr));
    add(new SCPICommand("RENAME", [=](QStringList params) -> QString {
        auto t = findTrace(params);
        if(!t) {
           return SCPI::getResultName(SCPI::Result::Error);
        }
        if(params.size() != 2) {
            return SCPI::getResultName(SCPI::Result::Error);
        }
        t->setName(params[1]);
        return SCPI::getResultName(SCPI::Result::Empty);
    }, nullptr));
    add(new SCPICommand("PAUSE", [=](QStringList params) -> QString {
        auto t = findTrace(params);
        if(!t) {
           return SCPI::getResultName(SCPI::Result::Error);
        }
        t->pause();
        return SCPI::getResultName(SCPI::Result::Empty);
    }, nullptr));
    add(new SCPICommand("RESUME", [=](QStringList params) -> QString {
        auto t = findTrace(params);
        if(!t) {
            return SCPI::getResultName(SCPI::Result::Error);
        }
        t->resume();
        return SCPI::getResultName(SCPI::Result::Empty);
    }, nullptr));
    add(new SCPICommand("PAUSED", nullptr, [=](QStringList params) -> QString {
        auto t = findTrace(params);
        if(!t) {
           return SCPI::getResultName(SCPI::Result::Error);
        }
        return t->isPaused() ? SCPI::getResultName(SCPI::Result::True) : SCPI::getResultName(SCPI::Result::False);
    }));
    auto deembed = new SCPINode("DEEMBedding");
    deembed->add(new SCPICommand("ACTive", [=](QStringList params) -> QString {
        Trace* t = findTrace(params);
        if(!t) {
           return SCPI::getResultName(SCPI::Result::Error);
        }
        bool activate = false;
        if(!SCPI::paramToBool(params, 1, activate)) {
            return SCPI::getResultName(SCPI::Result::Error);
        }
        if(activate) {
            if(!t->deembeddingAvailable()) {
                return SCPI::getResultName(SCPI::Result::Error);
            }
            t->setDeembeddingActive(true);
        } else {
            t->setDeembeddingActive(false);
        }
        return SCPI::getResultName(SCPI::Result::Empty);
    }, [=](QStringList params) -> QString {
        Trace* t = findTrace(params);
        if(!t) {
           return SCPI::getResultName(SCPI::Result::Error);
        }
        return t->isDeembeddingActive() ? SCPI::getResultName(SCPI::Result::True) : SCPI::getResultName(SCPI::Result::False);
    }));
    deembed->add(new SCPICommand("AVAILable", nullptr, [=](QStringList params) -> QString {
        Trace* t = findTrace(params);
        if(!t) {
           return SCPI::getResultName(SCPI::Result::Error);
        }
        return t->deembeddingAvailable() ? SCPI::getResultName(SCPI::Result::True) : SCPI::getResultName(SCPI::Result::False);
    }));
    add(deembed);
    add(new SCPICommand("PARAMeter", [=](QStringList params) -> QString {
        auto t = findTrace(params);
        if(!t || params.size() < 2) {
           return SCPI::getResultName(SCPI::Result::Error);
        }
        auto newparam = params[1];
        if((Trace::isVNAParameter(t->liveParameter()) && Trace::isVNAParameter(newparam))
                || (Trace::isSAParameter(t->liveParameter()) && Trace::isSAParameter(newparam))) {
            t->fromLivedata(t->liveType(), newparam);
            return SCPI::getResultName(SCPI::Result::Empty);
        } else {
            return SCPI::getResultName(SCPI::Result::Error);
        }
    }, [=](QStringList params) -> QString {
        auto t = findTrace(params);
        if(!t) {
           return SCPI::getResultName(SCPI::Result::Error);
        }
        return t->liveParameter();
    }));
    add(new SCPICommand("TYPE", [=](QStringList params) -> QString {
        auto t = findTrace(params);
        if(!t || params.size() < 2) {
           return SCPI::getResultName(SCPI::Result::Error);
        }
        auto newtype = Trace::TypeFromString(params[1]);
        if(newtype != Trace::LivedataType::Invalid) {
            t->fromLivedata(newtype, t->liveParameter());
            return SCPI::getResultName(SCPI::Result::Empty);
        } else {
            return SCPI::getResultName(SCPI::Result::Error);
        }
    }, [=](QStringList params) -> QString {
        auto t = findTrace(params);
        if(!t) {
           return SCPI::getResultName(SCPI::Result::Error);
        }
        return Trace::TypeToString(t->liveType());
    }));
}

void TraceWidget::contextMenuEvent(QContextMenuEvent *event)
{
    auto index = ui->view->indexAt(event->pos());
    if(!index.isValid()) {
        return;
    }
    auto trace = model.trace(index.row());
    auto ctxmenu = new QMenu();
    auto action_delete = new QAction("Delete");
    connect(action_delete, &QAction::triggered, this, &TraceWidget::on_remove_clicked);
    ctxmenu->addAction(action_delete);
    auto action_duplicate = new QAction("Duplicate");
    connect(action_duplicate, &QAction::triggered, this, [=](){
        auto json = trace->toJSON();
        auto duplicate = new Trace();
        duplicate->fromJSON(json);
        duplicate->setName(duplicate->name() + " - Duplicate");
        // force update of hash
        duplicate->toHash(true);
        model.addTrace(duplicate);
    });
    ctxmenu->addAction(action_duplicate);
    ctxmenu->exec(event->globalPos());
}