/*
 * Copyright (c) 2015, University of Kaiserslautern
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 * 1. Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * 3. Neither the name of the copyright holder nor the names of its
 *    contributors may be used to endorse or promote products derived from
 *    this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER
 * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * Authors:
 *    Janik Schlemminger
 *    Robert Gernhardt
 *    Matthias Jung
 */

#include <qwt_plot_grid.h>
#include <QWheelEvent>
#include <QMessageBox>
#include <QMouseEvent>
#include <iostream>
#include <QMenu>
#include <cmath>
#include <QInputDialog>
#include <QKeySequence>
#include <QFileInfo>
#include <qfiledialog.h>
#include <iostream>
#include <qwt_plot_renderer.h>

#include "tracePlotMouseLabel.h"
#include "traceplot.h"
#include "gototimedialog.h"
#include "tracedrawing.h"
#include "util/engineeringScaleDraw.h"
#include "util/clkgrid.h"
#include "util/customlabelscaledraw.h"

using namespace std;


TracePlot::TracePlot(QWidget *parent):
    QwtPlot(parent), isInitialized(false)
{
    canvas()->setCursor(Qt::ArrowCursor);
    setUpActions();
}

void TracePlot::setUpActions()
{
    insertComment = new QAction("Insert comment", this);
    QObject::connect(insertComment, SIGNAL(triggered()), this,
                     SLOT(on_insertComment()));

    goToTime = new QAction("Go to time", this);
    QObject::connect(goToTime, SIGNAL(triggered()), this, SLOT(on_goToTime()));

    goToTransaction = new QAction("Go to transaction", this);
    QObject::connect(goToTransaction, SIGNAL(triggered()), this,
                     SLOT(on_goToTransaction()));

    deselectAll = new QAction("Deselect all", this);
    QObject::connect(deselectAll, SIGNAL(triggered()), this,
                     SLOT(on_deselectAll()));

    goToPhase = new QAction("Go to phase", this);
    QObject::connect(goToPhase, SIGNAL(triggered()), this, SLOT(on_goToPhase()));

    showQueryEditor = new QAction("Execute query", this);
    showQueryEditor->setShortcut(QKeySequence("ctrl+e"));
    addAction(showQueryEditor);
    QObject::connect(showQueryEditor, SIGNAL(triggered()), this,
                     SLOT(on_executeQuery()));

    selectNextRefresh = new QAction("Select next refresh", this);
    selectNextRefresh->setShortcut(QKeySequence("alt+r"));
    addAction(selectNextRefresh);
    QObject::connect(selectNextRefresh, SIGNAL(triggered()), this,
                     SLOT(on_selectNextRefresh()));

    selectNextActivate = new QAction("Select next activate", this);
    selectNextActivate->setShortcut(QKeySequence("alt+a"));
    addAction(selectNextActivate);
    QObject::connect(selectNextActivate, SIGNAL(triggered()), this,
                     SLOT(on_selectNextActivate()));

    selectNextPrecharge = new QAction("Select next precharge", this);
    selectNextPrecharge->setShortcut(QKeySequence("alt+p"));
    addAction(selectNextPrecharge);
    QObject::connect(selectNextPrecharge, SIGNAL(triggered()), this,
                     SLOT(on_selectNextPrecharge()));

    selectNextActb = new QAction("Select next atcb", this);
    selectNextActb->setShortcut(QKeySequence("alt+b"));
    addAction(selectNextActb);
    QObject::connect(selectNextActb, SIGNAL(triggered()), this,
                     SLOT(on_selectNextActb()));

    selectNextPreb = new QAction("Select next preb", this);
    selectNextPreb->setShortcut(QKeySequence("alt+q"));
    addAction(selectNextPreb);
    QObject::connect(selectNextPreb, SIGNAL(triggered()), this,
                     SLOT(on_selectNextPreb()));

    selectNextRefb = new QAction("Select next refb", this);
    selectNextRefb->setShortcut(QKeySequence("alt+s"));
    addAction(selectNextRefb);
    QObject::connect(selectNextRefb, SIGNAL(triggered()), this,
                     SLOT(on_selectNextRefb()));

    setColorGroupingPhase = new QAction("Group by Phase", this);
    addAction(setColorGroupingPhase);
    QObject::connect(setColorGroupingPhase, SIGNAL(triggered()), this,
                     SLOT(on_colorGroupingPhase()));

    setColorGroupingTransaction = new QAction("Group by Transaction", this);
    addAction(setColorGroupingTransaction);
    QObject::connect(setColorGroupingTransaction, SIGNAL(triggered()), this,
                     SLOT(on_colorGroupingTransaction()));

    setColorGroupingThread = new QAction("Group by Thread", this);
    addAction(setColorGroupingThread);
    QObject::connect(setColorGroupingThread, SIGNAL(triggered()), this,
                     SLOT(on_colorGroupingThread()));

    exportToPdf = new QAction("Export to SVG", this);
    addAction(exportToPdf);
    QObject::connect(exportToPdf, SIGNAL(triggered()), this,
                     SLOT(on_exportToPDF()));

    setUpContextMenu();
}

void TracePlot::setUpContextMenu()
{
    contextMenu = new QMenu(this);
    contextMenu->addActions({deselectAll});

    QMenu *colorGroupingSubMenu = new QMenu("Group by", contextMenu);
    colorGroupingSubMenu->addActions({setColorGroupingTransaction, setColorGroupingPhase, setColorGroupingThread});
    contextMenu->addMenu(colorGroupingSubMenu);

    QMenu *goToSubMenu = new QMenu("Go to", contextMenu);
    goToSubMenu->addActions({goToPhase, goToTransaction, goToTime});
    contextMenu->addMenu(goToSubMenu);

    QMenu *selectSubMenu = new QMenu("Select", contextMenu);
    selectSubMenu->addActions({selectNextRefresh, selectNextActivate, selectNextPrecharge, selectNextActb, selectNextPreb, selectNextRefb});
    contextMenu->addMenu(selectSubMenu);

    contextMenu->addActions({showQueryEditor, insertComment, exportToPdf});
}

void TracePlot::init(TraceNavigator *navigator)
{
    Q_ASSERT(isInitialized == false);
    isInitialized = true;

    this->navigator = navigator;
    connectNavigatorQ_SIGNALS();
    setUpDrawingProperties();
    setUpAxis();
    setUpGrid();
    setUpTracePlotItem();
    setUpZoom();
    setUpQueryEditor();
    mouseLabel = new TracePlotMouseLabel(this,
                                         navigator->GeneralTraceInfo().clkPeriod, this->mouseDownData.zoomSpan);
    getAndDrawComments();
    setZoomLevel(1000);
    replot();
}


void TracePlot::connectNavigatorQ_SIGNALS()
{
    QObject::connect(navigator, SIGNAL(currentTraceTimeChanged()), this,
                     SLOT(currentTraceTimeChanged()));
    QObject::connect(navigator, SIGNAL(selectedTransactionsChanged()), this,
                     SLOT(selectedTransactionsChanged()));
    QObject::connect(navigator, SIGNAL(commentsChanged()), this,
                     SLOT(commentsChanged()));
}

void TracePlot::setUpDrawingProperties()
{
    unsigned int numberOfBanks = navigator -> GeneralTraceInfo().numberOfBanks;
    drawingProperties.numberOfBanks =  numberOfBanks;
    drawingProperties.yValResponse = numberOfBanks;
    drawingProperties.yValRequest = numberOfBanks + 1;
    drawingProperties.yValCommandBus = -3;
    drawingProperties.yValDataBus = -4;
}


void TracePlot::setUpQueryEditor()
{
    queryEditor = new QueryEditor(this);
    queryEditor->setWindowFlags(Qt::Window);
    queryEditor->setWindowTitle("Query " + QFileInfo(
                                    navigator->TraceFile().getPathToDB()).baseName());
    queryEditor->init(navigator);
}

void TracePlot::setUpTracePlotItem()
{
    tracePlotItem = new TracePlotItem(transactions, *navigator, drawingProperties);
    tracePlotItem->setZ(1);
    tracePlotItem->attach(this);
}

void TracePlot::setUpGrid()
{
    unsigned int clk = navigator->GeneralTraceInfo().clkPeriod;
    QwtPlotGrid *grid = new ClkGrid(clk, GridVisiblityClks * clk);
    grid->setZ(0);
    grid->attach(this);
}

void TracePlot::setUpZoom()
{
    minZoomLevel = minZoomClks * navigator->GeneralTraceInfo().clkPeriod;
    maxZoomLevel = maxZoomClks * navigator->GeneralTraceInfo().clkPeriod;
    textVisibilityZoomLevel = textVisibilityClks *
                              navigator->GeneralTraceInfo().clkPeriod;
    zoomZone = new QwtPlotZoneItem();
    zoomZone->setZ(2);
    zoomZone->attach(this);
    zoomZone->setVisible(false);
}

void TracePlot::setUpAxis()
{
    int numberOfBanks = navigator -> GeneralTraceInfo().numberOfBanks;

    setAxisScale(yLeft, -5, numberOfBanks + 2, 1.0);
    setAxisScaleDraw(yLeft, new CustomLabelScaleDraw(
                         drawingProperties.getLabels()));

    setAxisTitle(xBottom, "Time in ns");
    setAxisScaleDraw(xBottom, new EngineeringScaleDraw);
}

Timespan TracePlot::GetCurrentTimespan()
{
    Timespan span(navigator->CurrentTraceTime() - zoomLevel / 2,
                  navigator->CurrentTraceTime() + zoomLevel / 2);
    if (span.Begin() < 0)
        span.shift(-span.Begin());
    else if (span.End() > navigator->GeneralTraceInfo().span.End())
        span.shift(navigator->GeneralTraceInfo().span.End() - span.End());
    return span;
}

void TracePlot::getAndDrawComments()
{
    for (const auto &pair : navigator->getComments()) {
        const Comment &comment = pair.second;
        QwtPlotMarker *maker = new QwtPlotMarker();
        maker->setLabel(comment.Text());
        maker->setLabelOrientation(Qt::Vertical);
        maker->setLabelAlignment(Qt::AlignLeft | Qt::AlignBottom);
        maker->setXValue(static_cast<double>(comment.Time()));
        maker->setLineStyle(QwtPlotMarker::LineStyle::VLine);
        maker->setLinePen(QColor(Qt::blue), 2);
        maker->attach(this);
    }
}

void TracePlot::enterZoomMode()
{
    mouseDownData.mouseIsDownForZooming = true;
    mouseLabel->setMode(MouseLabelMode::Timedifference);
    zoomZone->setVisible(true);
    zoomZone->setInterval(mouseDownData.zoomSpan.Begin(),
                          mouseDownData.zoomSpan.End());
}

void TracePlot::exitZoomMode()
{
    mouseDownData.mouseIsDownForZooming = false;
    mouseLabel->setMode(MouseLabelMode::AbsoluteTime);
    zoomZone->setVisible(false);
}
void TracePlot::zoomIn(traceTime zoomCenter)
{
    setZoomLevel(zoomLevel * zoomFactor);
    traceTime time = zoomCenter + (GetCurrentTimespan().Middle() - zoomCenter) *
                     zoomFactor;
    Q_EMIT tracePlotZoomChanged();
    navigator->navigateToTime(time);
}

void TracePlot::zoomOut(traceTime zoomCenter)
{
    setZoomLevel(zoomLevel / zoomFactor);
    Q_EMIT tracePlotZoomChanged();
    navigator->navigateToTime(static_cast<traceTime>(zoomCenter +
                                                     (GetCurrentTimespan().Middle() - zoomCenter) / zoomFactor));
}

void TracePlot::setZoomLevel(traceTime newZoomLevel)
{
    zoomLevel = newZoomLevel;
    if (zoomLevel < minZoomLevel)
        zoomLevel = minZoomLevel;
    if (zoomLevel > navigator->GeneralTraceInfo().span.timeCovered())
        zoomLevel = navigator->GeneralTraceInfo().span.timeCovered();
    if (zoomLevel > maxZoomLevel)
        zoomLevel = maxZoomLevel;

    if (zoomLevel < textVisibilityZoomLevel)
        drawingProperties.drawText = true;

    if (zoomLevel > textVisibilityZoomLevel)
        drawingProperties.drawText = false;
}


/* Q_SLOTS
 *
 *
 */

void TracePlot::currentTraceTimeChanged()
{
    transactions = navigator->TraceFile().getTransactionsInTimespan(
                       GetCurrentTimespan());
    setAxisScale(xBottom, GetCurrentTimespan().Begin(), GetCurrentTimespan().End());
    replot();
}

void TracePlot::selectedTransactionsChanged()
{
    replot();
}

void TracePlot::commentsChanged()
{
    detachItems(QwtPlotItem::Rtti_PlotMarker);
    getAndDrawComments();
    replot();
}

void TracePlot::on_executeQuery()
{
    queryEditor->show();
}

void TracePlot::on_selectNextRefresh()
{
    navigator->selectNextRefresh();
}

void TracePlot::on_selectNextActivate()
{
    navigator->selectNextActivate();
}

void TracePlot::on_selectNextPrecharge()
{
    navigator->selectNextPrecharge();
}

void TracePlot::on_selectNextActb()
{
    navigator->selectNextActb();
}

void TracePlot::on_selectNextPreb()
{
    navigator->selectNextPreb();
}

void TracePlot::on_selectNextRefb()
{
    navigator->selectNextRefb();
}

void TracePlot::on_colorGroupingPhase()
{
    drawingProperties.colorGrouping = ColorGrouping::PhaseType;
    Q_EMIT(colorGroupingChanged(ColorGrouping::PhaseType));
    replot();
}

void TracePlot::on_colorGroupingTransaction()
{
    drawingProperties.colorGrouping = ColorGrouping::Transaction;
    Q_EMIT(colorGroupingChanged(ColorGrouping::Transaction));
    replot();
}

void TracePlot::on_colorGroupingThread()
{
    drawingProperties.colorGrouping = ColorGrouping::Thread;
    Q_EMIT(colorGroupingChanged(ColorGrouping::Thread));
    replot();
}

void TracePlot::on_goToTransaction()
{
    bool ok;
    int maxID = navigator->GeneralTraceInfo().numberOfTransactions;
    int transactionID = QInputDialog::getInt(this, "Go to transaction",
                                             "Enter transaction ID (1 - " + QString::number(maxID) + ")", 0, 1, maxID, 1,
                                             &ok);
    if (ok) {
        navigator->clearSelectedTransactions();
        navigator->selectTransaction(transactionID);
    }
}

void TracePlot::on_goToPhase()
{
    bool ok;
    int maxID = navigator->GeneralTraceInfo().numberOfPhases;
    int phaseID = QInputDialog::getInt(this, "Go to phase",
                                       "Enter phase ID (1 - " + QString::number(maxID) + ")", 0, 1, maxID, 1, &ok);

    if (ok) {
        navigator->clearSelectedTransactions();
        navigator->selectTransaction(navigator->TraceFile().getTransactionIDFromPhaseID(
                                         phaseID));
    }
}

void TracePlot::on_deselectAll()
{
    navigator->clearSelectedTransactions();
}

void TracePlot::on_insertComment()
{
    traceTime time = invTransform(xBottom, contextMenuMouseDown.x());
    bool ok;
    QString text = QInputDialog::getText(this, QString("Insert comment"),
                                         QString("Insert comment at ") + prettyFormatTime(time), QLineEdit::Normal,
                                         QString(), &ok);
    if (ok) {
        navigator->insertComment(Comment(time, text));
    }
}

void TracePlot::on_goToTime()
{
    double goToTime;
    GoToTimeDialog dialog(&goToTime, this);
    int dialogCode = dialog.exec();
    if (dialogCode == QDialog::Accepted) {
        traceTime time = static_cast<traceTime>(goToTime) * 1000;
        navigator->navigateToTime(time);
    }
}

void TracePlot::on_exportToPDF()
{
    QwtPlotRenderer renderer;
    QString filename = QFileDialog::getSaveFileName(this, "Export to SVG", "",
                                                    "Portable Document Format(*.svg)");
    if (filename != "") {
        QBrush saved = this->canvasBackground();
        this->setCanvasBackground(QBrush(Qt::white));
        renderer.renderDocument(this, filename, "svg", QSizeF(this->widthMM(),
                                                              this->heightMM()));
        this->setCanvasBackground(QBrush(saved));
    }
}

/* Keyboard and mouse events
 *
 *
 */

void TracePlot::keyPressEvent(QKeyEvent *keyPressedEvent)
{
    int key = keyPressedEvent->key();
    if (Qt::Key_Control == key)
        keyPressData.ctrlPressed = true;
    else if (Qt::Key_Shift == key)
        keyPressData.shiftPressed = true;
    else if (Qt::Key_Right == key)
        navigator->selectNextTransaction();
    else if (Qt::Key_Left == key)
        navigator->selectPreviousTransaction();
    else if (Qt::Key_Minus == key) {
        zoomOut(GetCurrentTimespan().Middle());
    } else if (Qt::Key_Plus == key) {
        zoomIn(GetCurrentTimespan().Middle());
    }
}

void TracePlot::keyReleaseEvent(QKeyEvent *keyReleasedEvent)
{
    int key = keyReleasedEvent->key();

    if (Qt::Key_Control == key)
        keyPressData.ctrlPressed = false;
    else if (Qt::Key_Shift == key) {
        keyPressData.shiftPressed = false;
        exitZoomMode();
        replot();
    }
}


bool TracePlot::eventFilter(QObject *object, QEvent *event)
{
    if (object == canvas()) {
        switch (event->type()) {
        case QEvent::Wheel : {
            QWheelEvent *wheelEvent = static_cast<QWheelEvent *>(event);
            traceTime zoomCenter = static_cast<traceTime>(this->invTransform(xBottom,
                                                                             wheelEvent->x()));

            (wheelEvent->delta() > 0) ? zoomIn(zoomCenter) : zoomOut(zoomCenter);

            return true;
        }
        case QEvent::MouseButtonPress: {
            QMouseEvent *mouseEvent = static_cast<QMouseEvent *>(event);

            if (mouseEvent->button() == Qt::LeftButton) {
                if (keyPressData.shiftPressed) {
                    mouseDownData.zoomSpan.setBegin(alignToClk(invTransform(xBottom,
                                                                            mouseEvent->x()), navigator->GeneralTraceInfo().clkPeriod));
                    mouseDownData.zoomSpan.setEnd(alignToClk(invTransform(xBottom, mouseEvent->x()),
                                                             navigator->GeneralTraceInfo().clkPeriod));
                    enterZoomMode();
                } else {
                    mouseDownData.mouseDownX = mouseEvent->x();
                    mouseDownData.mouseDownTime = GetCurrentTimespan().Middle();
                    mouseDownData.mouseIsDownForDragging = true;
                    canvas()->setCursor(Qt::ClosedHandCursor);
                    SelectTransaction(mouseEvent->x(), mouseEvent->y());
                }
                return true;
            } else if (mouseEvent->button() == Qt::RightButton) {
                openContextMenu(this->canvas()->mapToGlobal(mouseEvent->pos()),
                                mouseEvent->pos());
                return true;
            }
            break;
        }
        case QEvent::MouseButtonRelease: {
            QMouseEvent *mouseEvent = static_cast<QMouseEvent *>(event);

            if (mouseEvent->button() == Qt::LeftButton) {
                if (mouseDownData.mouseIsDownForDragging) {
                    mouseDownData.mouseIsDownForDragging = false;
                    canvas()->setCursor(Qt::ArrowCursor);
                    return true;
                } else if (mouseDownData.mouseIsDownForZooming) {
                    exitZoomMode();
                    traceTime newCenter = mouseDownData.zoomSpan.Middle();
                    setZoomLevel(mouseDownData.zoomSpan.timeCovered());
                    navigator->navigateToTime(newCenter);
                    replot();
                    return true;
                }
            }
            break;
        }
        case QEvent::MouseMove: {
            QMouseEvent *mouseEvent = static_cast<QMouseEvent *>(event);

            if (mouseDownData.mouseIsDownForDragging) {
                traceTime deltaTime = invTransform(xBottom,
                                                   mouseDownData.mouseDownX) - invTransform(xBottom, mouseEvent->x());
                navigator->navigateToTime(mouseDownData.mouseDownTime + deltaTime);
                return true;
            } else if (mouseDownData.mouseIsDownForZooming) {
                mouseDownData.zoomSpan.setEnd(alignToClk(invTransform(xBottom, mouseEvent->x()),
                                                         navigator->GeneralTraceInfo().clkPeriod));
                if (mouseDownData.zoomSpan.Begin() < mouseDownData.zoomSpan.End())
                    zoomZone->setInterval(mouseDownData.zoomSpan.Begin(),
                                          mouseDownData.zoomSpan.End());
                else
                    zoomZone->setInterval(mouseDownData.zoomSpan.End(),
                                          mouseDownData.zoomSpan.Begin());

                replot();
            }
            break;

        }
        default:
            break;
        }
    }
    return false;
}

void TracePlot::SelectTransaction(int x, int y)
{
    double yVal = invTransform(yLeft, y);
    traceTime time = invTransform(xBottom, x);
    vector<shared_ptr<Transaction>> selectedTransactions =
                                     tracePlotItem->getSelectedTransactions(time, yVal);
    if (selectedTransactions.size() > 0) {
        if (!keyPressData.ctrlPressed)
            navigator->clearSelectedTransactions();
        navigator->addSelectedTransactions(selectedTransactions);
    }
}

void TracePlot::openContextMenu(const QPoint &pos, const QPoint &mouseDown)
{
    contextMenuMouseDown = mouseDown;
    contextMenu->exec(pos);
}


