#include "flyinglabels.h"
#include "ui_flyinglabels.h"

#include "labelmodel.h"
#include "staticlabeler.h"
#include "dynamiclabeling.h"
#include "constants.h"
#include "assertions.h"

#include <QFontDialog>
#include <QFontMetrics>

#include <QMouseEvent>
#include <QTimer>

#include <QTextStream>
#include <QFileDialog>

FlyingLabels::FlyingLabels(QWidget* parent) :
    QMainWindow(parent),
    _ui(new Ui::FlyingLabels),
    _instance(new ProblemInstance),
    _mouseHandler(*this),
    _isPaused(true)
{
    _ui->setupUi(this);
    alignGraphingAreaWithTimeline();

    _playIcon  = QIcon::fromTheme("media-playback-start", QIcon(":/icons/play.png"));
    _pauseIcon = QIcon::fromTheme("media-playback-pause", QIcon(":/icons/pause.png"));
    updatePlayPauseButton();

    _ui->drawingArea->setFocus();
    updateDrawnNetworkParts();
    updateDrawnLabelParts();
    labelModelChanged();
    interpolationMethodChanged();
    timestepChanged();
    simulationDurationChanged();
    _instance->registerObserver(*this);
    displayedStatisticsChanged();

    _ui->drawingArea->setMouseTracking(true);

    QTimer* timer = new QTimer(this);
    connect(timer, SIGNAL(timeout()), this, SLOT(timerTick()));
    timer->start(millisecondsPerTick);
}

FlyingLabels::~FlyingLabels()
{
    delete _ui;
}

void FlyingLabels::alignGraphingAreaWithTimeline()
{
    int lo = _ui->timeSlider->leftOffset();
    int ro = _ui->timeSlider->rightOffset();
    _ui->graphingArea->timelineOffsets(lo, ro);
}

void FlyingLabels::timerTick()
{
    if (!_isPaused) {
        advanceTimeByOneTick();
    }
    _ui->timeLabel->setText( QString("%1").arg(currentTime(), 0, 'f', 2) );
    updatePlayPauseButton();
    qreal timeout = 0.25 * secondsPerTick;
    _labeling->updateStatistics(timeout);
    redraw();
}

void FlyingLabels::updateDrawnNetworkParts()
{
    _drawOptions[DrawVertices]     = _ui->showVerticesCheckbox->isChecked();
    _drawOptions[DrawEdges]        = _ui->showEdgesCheckbox->isChecked();
    _drawOptions[DrawRoutes]       = _ui->showRoutesCheckbox->isChecked();
    _drawOptions[DrawMovingPoints] = _ui->showPointsCheckbox->isChecked();
}

void FlyingLabels::updateDrawnLabelParts()
{
    _drawOptions[DrawLabelText]  = _ui->showLabelTextCheckbox->isChecked();
    _drawOptions[DrawLabelBBox]  = _ui->showLabelBBoxCheckbox->isChecked();
    _drawOptions[DrawLabelModel] = _ui->showLabelCandidatesCheckbox->isChecked();
    _drawOptions[DrawLabelCenter] = _drawOptions[DrawLabelModel];    
}

void FlyingLabels::displayedStatisticsChanged()
{
    bool showArea = _ui->graphFreeAreaButton->isChecked();
    bool showSpeeds = _ui->graphSpeedButton->isChecked();
    _drawOptions[ColorLabelsByFreeArea] = showArea;
    SummaryStatistic displayed = showArea ? FreeArea
                               : showSpeeds ? LabelSpeeds : FreeCount;
    _labeling->statistics().graphType(displayed);
}

void FlyingLabels::changeLabelFont()
{
    bool ok;
    QFont newFont = QFontDialog::getFont(&ok, _instance->labelFont(), this);
    if (ok) {
        _instance->labelFont(newFont);
    }
}

void FlyingLabels::updatePlayPauseButton()
{
    bool canBePaused = !_isPaused && !atEndTime();
    _ui->playPauseButton->setIcon( canBePaused ? _pauseIcon : _playIcon );
}

void FlyingLabels::playPausePressed()
{
    if (atEndTime()) {
        resetTime();
        _isPaused = false;
    } else {
        _isPaused = !_isPaused;
    }
    updatePlayPauseButton();
}

void FlyingLabels::trimmingChanged()
{
    bool enabled = _ui->trimmingCheckbox->isChecked();
    qreal amount = enabled ? _ui->trimmingSpinbox->value() : NO_TRIMMING;
    _ui->trimmingSpinbox->setEnabled(enabled);
    _labeling->trimming(amount);
}

void FlyingLabels::timeSliderMoved()
{
    if (!_isPaused) {
        _isPaused = true;
        updatePlayPauseButton();
    }
    _ui->timeLabel->setText(QString("%1").arg(currentTime(), 0, 'f', 2));
}

void FlyingLabels::labelModelChanged()
{
    int index = _ui->labelModel->currentIndex();
    switch (index) {
    case 0:
        _instance->newLabelModel<OneSliderModel>(LeftSlider);
        break;
    case 1:
        _instance->newLabelModel<OneSliderModel>(RightSlider);
        break;
    case 2:
        _instance->newLabelModel<OneSliderModel>(TopSlider);
        break;
    case 3:
        _instance->newLabelModel<OneSliderModel>(BottomSlider);
        break;
    case 4:
        _instance->newLabelModel<FourSliderModel>();
        break;
    case 5:
        _instance->newLabelModel<BehindSliderModel>();
        break;
    default:
        assert(false);
    }
}

void FlyingLabels::staticLabelingAlgorithmChanged()
{
    int index = _ui->staticLabelingAlgorithm->currentIndex();
    static SweepDirection dir[4] = {
        SweepDirection::LeftToRight(), SweepDirection::RightToLeft(),
        SweepDirection::TopToBottom(), SweepDirection::BottomToTop() };

    if (!_labeling) return;

    switch (index) {
    case 0: case 1: case 2: case 3: {
        _labeling->newStaticLabeler<GreedySweepLabeler>(dir[index]);
    } break;
    case 4: {
        _labeling->newStaticLabeler<FourGreedySweepsLabeler>();
    } break;
    default:
        assert(false);
    }
}

void FlyingLabels::interpolationMethodChanged()
{
    int index = _ui->interpolationMethod->currentIndex();
    bool enabled = false;
    qreal timestep = _ui->timestepSpinbox->value();
    qreal trimming = _ui->trimmingCheckbox->isChecked() ? _ui->trimmingSpinbox->value() : NO_TRIMMING;

    switch (index) {
    case 0: {
        _labeling.reset( new AlwaysRelabelInterpolator(*_instance) );
    } break;
    case 1: {
        _labeling.reset( new SlowestInterpolation(*_instance, timestep, trimming) );
        enabled = true;
    } break;
    default:
        assert(false);
        return;
    }

    _ui->timestepLabel->setEnabled(enabled);
    _ui->timestepSpinbox->setEnabled(enabled);
    _ui->trimmingLabel->setEnabled(enabled);
    _ui->trimmingCheckbox->setEnabled(enabled);
    _ui->trimmingSpinbox->setEnabled(enabled && _ui->trimmingCheckbox->isChecked());
    updateDrawnLabelParts();

    _labeling->statistics().attachGraphOutput(*_ui->graphingArea);
    staticLabelingAlgorithmChanged();  
}

void FlyingLabels::timestepChanged()
{
    qreal timestep = _ui->timestepSpinbox->value();
    assert(_labeling);
    _labeling->timestep( timestep );

    int ticks = (int)secondsToTicks(timestep);
    _ui->timeSlider->setPageStep(ticks);
    _ui->timeSlider->setTickInterval(ticks);
}

void FlyingLabels::simulationDurationChanged()
{
    qreal seconds = _ui->simulationDurationSpinbox->value();
    _instance->duration(seconds);
    _ui->timeSlider->setMaximum( (int)secondsToTicks(seconds) );
}

void FlyingLabels::replaceProblemInstance(QString const& filePath)
{
    std::unique_ptr<ProblemInstance> newInstance( new ProblemInstance(filePath) );
    std::unique_ptr<ProblemInstance> oldInstance( std::move(_instance) );

    oldInstance->unregisterObserver(*this);
    newInstance->registerObserver(*this);

    _instance = std::move(newInstance);
    oldInstance->notifyObserversOfReplacement(*_instance);

    updateGUIforReplacedRoutes();
    _ui->drawingArea->resetZoomAndTranslation();
}

void FlyingLabels::newClicked()
{
    try {
        replaceProblemInstance( QString() );
    } catch (InstanceReadError const& e) {
        _errorDialog.showMessage( QString("Error reading Ipe file template: %1").arg(e.what()) );
    }
}

void FlyingLabels::openClicked()
{
    QString filePath = QFileDialog::getOpenFileName(this, "Open File",
                                                    QString(),
                                                    "Ipe files (*.ipe *.xml);;All files (*.*)");
    if (filePath.isNull()) return;

    try {
        replaceProblemInstance(filePath);
    } catch (InstanceReadError const& e) {
        _errorDialog.showMessage( QString("Error reading Ipe file '%1': %2").arg(filePath).arg(e.what()) );
    }
    //_instance->routeNetwork().fixConsistencyProblems(); // TODO: should probably have a GUI option for this
}

void FlyingLabels::saveClicked()
{
    if (_instance->filePath().isEmpty()) {
        saveAsClicked();
        return;
    }
    try {
        _instance->saveToFile(_instance->filePath());
    } catch (InstanceWriteError const& e) {
        _errorDialog.showMessage( QString("Error writing Ipe file '%1': %2").arg(_instance->filePath()).arg(e.what()) );
    }
}

void FlyingLabels::saveAsClicked()
{
    QString filePath = QFileDialog::getSaveFileName(this, "Save File",
                                                    QString(),
                                                    "Ipe files (*.ipe *.xml);;All files (*.*)");
    if (filePath.isNull()) return;
    try {
        _instance->saveToFile(filePath);
    } catch (InstanceWriteError const& e) {
        _errorDialog.showMessage( QString("Error writing Ipe file '%1': %2").arg(filePath).arg(e.what()) );
    }
}

void FlyingLabels::revertClicked()
{
    try {
        replaceProblemInstance(_instance->filePath());
    } catch (InstanceReadError const& e) {
        _errorDialog.showMessage( QString("Error reading Ipe file '%1': %2").arg(_instance->filePath()).arg(e.what()) );
    }
}

void FlyingLabels::redraw()
{
    _ui->drawingArea->startDrawing();
    _instance->draw(_ui->drawingArea, _drawOptions);
    assert(_labeling);
    _labeling->drawAtTime(_ui->drawingArea, currentTime(), _drawOptions);
    _mouseHandler.draw(_ui->drawingArea);
    _ui->drawingArea->endDrawing();
}

qreal FlyingLabels::currentTime() const
{
    return ticksToSeconds(_ui->timeSlider->value());
}
void FlyingLabels::resetTime()
{
    _ui->timeSlider->setValue(0);
}
void FlyingLabels::advanceTimeByOneTick()
{
    _ui->timeSlider->setValue(_ui->timeSlider->value() + 1);
}
bool FlyingLabels::atEndTime() const
{
    return _ui->timeSlider->value() == _ui->timeSlider->maximum();
}

void FlyingLabels::mousePressEvent(QMouseEvent* event, QPointF const& cursor)
{
    _mouseHandler.mousePressEvent(event, cursor);
    redraw();
}

void FlyingLabels::mouseMoveEvent(QMouseEvent* event, QPointF const& cursor)
{
    _ui->statusBar->showMessage( QString("(%1, %2)").arg(cursor.x()).arg(cursor.y()) );
    _mouseHandler.mouseMoveEvent(event, cursor);
    redraw();
}

void FlyingLabels::mouseReleaseEvent(QMouseEvent* event, QPointF const& cursor)
{
    _mouseHandler.mouseReleaseEvent(event, cursor);
    redraw();
}

void FlyingLabels::changeEvent(QEvent* event)
{
    QMainWindow::changeEvent(event);
    switch (event->type()) {
    case QEvent::LanguageChange:
        _ui->retranslateUi(this);
        break;
    default:
        break;
    }
}

void FlyingLabels::resizeEvent(QResizeEvent* event)
{
    QMainWindow::resizeEvent(event);
    _labeling->statistics().redraw();
}

void FlyingLabels::showEvent(QShowEvent* event)
{
    QMainWindow::showEvent(event);
    _ui->timeLabel->setFixedSize( _ui->timeLabel->size() );
}

void FlyingLabels::routeSelectionChanged()
{
    int index = _ui->routeListWidget->currentRow();
    if (index < 0) return;
    auto route = _instance->routes().begin() + index;
    _ui->labelTemplateEdit->document()->setPlainText( route->labelTemplate() );
    _ui->airplaneSpeedSpinbox->setValue( route->airplaneSpeed() );
    _ui->interarrivalTimeSpinbox->setValue( route->averageInterarrivalTime() );
}

void FlyingLabels::labelTemplateChanged()
{
    int index = _ui->routeListWidget->currentRow();
    if (index < 0) return;
    auto route = _instance->routes().begin() + index;
    applyLabelTemplateToRoute(*route);
}

void FlyingLabels::airplaneSpeedChanged()
{
    int index = _ui->routeListWidget->currentRow();
    if (index < 0) return;
    auto route = _instance->routes().begin() + index;
    applyAirplaneSpeedToRoute(*route);
}

void FlyingLabels::interarrivalTimeChanged()
{
    int index = _ui->routeListWidget->currentRow();
    if (index < 0) return;
    auto route = _instance->routes().begin() + index;
    applyInterarrivalTimeToRoute(*route);
}

void FlyingLabels::arrivalDistributionChanged()
{
    int index = _ui->routeListWidget->currentRow();
    if (index < 0) return;
    auto route = _instance->routes().begin() + index;
    applyArrivalDistributionToRoute(*route);
}

void FlyingLabels::applyParametersToAllRoutes()
{
    for (AirplaneRoute& route : _instance->routes()) {
        applyParametersToRoute(route);
    }
}

void FlyingLabels::updateGUIforRoutes(bool completelyNew)
{
    auto routes = _instance->routes();
    int n = routes.size();
    if (completelyNew) {
        _ui->routeListWidget->clear();
    }
    int i = _ui->routeListWidget->count();
    if (i == n)
        return;

    auto newRoutes = routes; newRoutes.advance_begin(i);
    for (AirplaneRoute& route : newRoutes) {
        if (!completelyNew) {
            applyParametersToRoute(route);
        }
        new QListWidgetItem(QString("Route %1").arg(i+1), _ui->routeListWidget);
        ++i;
    }
    if (n > 0) {
        _ui->routeListWidget->setCurrentRow(n-1);
        if (completelyNew) {
            auto& route = routes.back();
            _ui->airplaneSpeedSpinbox->setValue( route.airplaneSpeed() );
            _ui->interarrivalTimeSpinbox->setValue( route.averageInterarrivalTime() );
            _ui->labelTemplateEdit->document()->setPlainText( route.labelTemplate() );
            _ui->arrivalDistribution->setCurrentIndex( route.usesPoissonProcess() ? 1 : 0 );
        }
    }
    _ui->simulationDurationSpinbox->setValue( std::ceil(_instance->duration()) );
}

void FlyingLabels::applyLabelTemplateToRoute(AirplaneRoute& route)
{
    QString newTemplate = _ui->labelTemplateEdit->document()->toPlainText();
    route.labelTemplate( newTemplate );
}

void FlyingLabels::applyAirplaneSpeedToRoute(AirplaneRoute& route)
{
    qreal newSpeed = _ui->airplaneSpeedSpinbox->value();
    route.airplaneSpeed( newSpeed );
}

void FlyingLabels::applyInterarrivalTimeToRoute(AirplaneRoute& route)
{
    qreal newInterarrivalTime = _ui->interarrivalTimeSpinbox->value();
    route.averageInterarrivalTime( newInterarrivalTime );
}

void FlyingLabels::applyArrivalDistributionToRoute(AirplaneRoute& route)
{
    int index = _ui->arrivalDistribution->currentIndex();
    switch (index) {
    case 0:
        route.usesPoissonProcess(false);
        break;
    case 1:
        route.usesPoissonProcess(true);
        break;
    default:
        assert(false);
    }
}

void FlyingLabels::applyParametersToRoute(AirplaneRoute& route)
{
    applyLabelTemplateToRoute(route);
    applyAirplaneSpeedToRoute(route);
    applyInterarrivalTimeToRoute(route);
    applyArrivalDistributionToRoute(route);
}


