/*
 * This file is part of ArmarX.
 *
 * ArmarX is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 as
 * published by the Free Software Foundation.
 *
 * ArmarX is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 *
 * \package    RobotAPI::gui-plugins::ArVizWidgetController
 * \author     Fabian Paus ( fabian dot paus at kit dot edu )
 * \date       2019
 * \copyright  http://www.gnu.org/licenses/gpl-2.0.txt
 *             GNU General Public License
 */

#include "ArVizWidgetController.h"
#include <RobotAPI/components/ArViz/Coin/VisualizationObject.h>
#include <RobotAPI/components/ArViz/Coin/VisualizationRobot.h>

#include <string>

#include <ArmarXCore/core/ArmarXManager.h>

#include <QLineEdit>
#include <QMessageBox>
#include <QTimer>

#include <ArmarXCore/observers/variant/Variant.h>


#include <QFileDialog>
#include  <boost/algorithm/string/predicate.hpp>

#define ENABLE_INTROSPECTION 1


namespace armarx
{
    struct ArVizWidgetBatchCallback : IceUtil::Shared
    {
        struct ArVizWidgetController* this_;

        void onSuccess(viz::data::RecordingBatch const& batch)
        {
            this_->onGetBatchAsync(batch);
        }

        void onFailure(Ice::Exception const& ex)
        {
            ARMARX_WARNING << "Failed to get batch async.\nReason:"
                           << ex;
        }
    };

    ArVizWidgetController::ArVizWidgetController()
    {
        using This = ArVizWidgetController;

        widget.setupUi(getWidget());

        updateTimer = new QTimer(this);
        connect(updateTimer, &QTimer::timeout, this, QOverload<>::of(&This::onUpdate));

        timingObserverTimer = new QTimer(this);
        connect(timingObserverTimer, &QTimer::timeout, this, QOverload<>::of(&This::onTimingObserverUpdate));

        replayTimer = new QTimer(this);
        connect(replayTimer, &QTimer::timeout, this, QOverload<>::of(&This::onReplayTimerTick));

        connect(widget.layerTree, &QTreeWidget::itemChanged, this, &This::layerTreeChanged);
        connect(widget.expandButton, &QPushButton::clicked, this, &This::onExpandAll);
        connect(widget.collapseButton, &QPushButton::clicked, this, &This::onCollapseAll);
        connect(widget.filterEdit, &QLineEdit::textChanged, this, &This::onFilterTextChanged);
        connect(widget.hideAllButton, &QPushButton::clicked, this, &This::onHideAll);
        connect(widget.showAllButton, &QPushButton::clicked, this, &This::onShowAll);
        connect(widget.hideFilteredButton, &QPushButton::clicked, this, &This::onHideFiltered);
        connect(widget.showFilteredButton, &QPushButton::clicked, this, &This::onShowFiltered);

        connect(widget.recordingStartButton, &QPushButton::clicked, this, &This::onStartRecording);
        connect(widget.recordingStopButton, &QPushButton::clicked, this, &This::onStopRecording);

        connect(widget.refreshRecordingsButton, &QPushButton::clicked, this, &This::onRefreshRecordings);
        connect(widget.recordingList, &QListWidget::currentItemChanged, this, &This::onRecordingSelectionChanged);

        connect(widget.replayRevisionSpinBox, QOverload<int>::of(&QSpinBox::valueChanged), this, &This::onReplaySpinChanged);
        connect(widget.replayRevisionSlider, QOverload<int>::of(&QSlider::valueChanged), this, &This::onReplaySliderChanged);

        connect(widget.replayStartButton, &QPushButton::clicked, this, &This::onReplayStart);
        connect(widget.replayStopButton, &QPushButton::clicked, this, &This::onReplayStop);
        connect(widget.replayTimedButton, &QPushButton::toggled, this, &This::onReplayTimedStart);

        connect(widget.exportToVRMLButton, &QPushButton::clicked, this, &This::exportToVRML);

        connect(this, &This::connectGui, this, &This::onConnectGui, Qt::QueuedConnection);
        connect(this, &This::disconnectGui, this, &This::onDisconnectGui, Qt::QueuedConnection);

        // Layer info tree.

        connect(widget.layerTree, &QTreeWidget::currentItemChanged, this, &This::updateSelectedLayer);

#if ENABLE_INTROSPECTION
        connect(widget.layerInfoTreeGroupBox, &QGroupBox::toggled, &layerInfoTree, &LayerInfoTree::setEnabled);
        connect(widget.defaultShowLimitSpinBox, qOverload<int>(&QSpinBox::valueChanged),
                &layerInfoTree, &LayerInfoTree::setMaxElementCountDefault);
        layerInfoTree.setMaxElementCountDefault(widget.defaultShowLimitSpinBox->value());

        layerInfoTree.setWidget(widget.layerInfoTree);
        layerInfoTree.setEnabled(widget.layerInfoTreeGroupBox->isChecked());
        layerInfoTree.registerVisualizerCallbacks(visualizer);

#endif


        // We need a callback from the visualizer, when the layers have changed
        // So we can update the tree accordingly
        visualizer.layersChangedCallback = [this](std::vector<viz::CoinLayerID> const & layers)
        {
            layersChanged(layers);
        };

        replayTimer->start(33);
    }

    ArVizWidgetController::~ArVizWidgetController()
    {

    }

    void ArVizWidgetController::onInitComponent()
    {
        if (storageName.size() > 0)
        {
            usingProxy(storageName);
        }
        if (debugObserverName.size() > 0)
        {
            usingProxy(debugObserverName);
        }

        callbackData = new ArVizWidgetBatchCallback();
        callbackData->this_ = this;
        callback = viz::newCallback_StorageInterface_getRecordingBatch(
                       callbackData,
                       &ArVizWidgetBatchCallback::onSuccess,
                       &ArVizWidgetBatchCallback::onFailure);
    }

    void ArVizWidgetController::onExitComponent()
    {
        armarx::viz::coin::clearObjectCache();
        armarx::viz::coin::clearRobotCache();
    }

    void ArVizWidgetController::onConnectComponent()
    {
        getProxy(storage, storageName);
        // DebugObserver is optional (check for null on every call)
        if (!debugObserverName.empty())
        {
            getProxy(debugObserver, debugObserverName, false, "", false);
        }

        lastTiming = visualizer.getTiming();
        visualizer.startAsync(storage);

        // Changes to UI elements are only allowed in the GUI thread
        emit connectGui();
    }

    void ArVizWidgetController::onDisconnectComponent()
    {
        visualizer.stop();

        // Changes to UI elements are only allowed in the GUI thread
        emit disconnectGui();
    }

    void ArVizWidgetController::onConnectGui()
    {
        onRefreshRecordings();
        currentRecordingSelected = false;
        changeMode(ArVizWidgetMode::Live);

        timingObserverTimer->start(33);
        updateTimer->start(33);
    }

    void ArVizWidgetController::onDisconnectGui()
    {
        timingObserverTimer->stop();
        changeMode(ArVizWidgetMode::NotConnected);
    }

    void ArVizWidgetController::layerTreeChanged(QTreeWidgetItem* item, int /*column*/)
    {
        // Iterate over all items and activate/deactivate layers accordingly
        int componentCount = widget.layerTree->topLevelItemCount();
        for (int compIndex = 0; compIndex < componentCount; ++compIndex)
        {
            QTreeWidgetItem* componentItem = widget.layerTree->topLevelItem(compIndex);
            std::string component = componentItem->text(0).toStdString();
            Qt::CheckState componentCheck = componentItem->checkState(0);
            int layerCount = componentItem->childCount();

            if (componentItem == item)
            {
                // The parent was selected or deselected, so all children should be set accordingly
                ARMARX_VERBOSE << "Setting all children of " << component << " to " << (componentCheck == Qt::Checked);
                for (int layerIndex = 0; layerIndex < layerCount; ++layerIndex)
                {
                    QTreeWidgetItem* layerItem = componentItem->child(layerIndex);
                    if (layerItem->checkState(0) != componentCheck)
                    {
                        layerItem->setCheckState(0, componentCheck);
                    }
                }
                return;
            }

            for (int layerIndex = 0; layerIndex < layerCount; ++layerIndex)
            {
                QTreeWidgetItem* layerItem = componentItem->child(layerIndex);
                std::string layer = layerItem->text(0).toStdString();
                bool layerVisible = (layerItem->checkState(0) == Qt::Checked);
                ARMARX_VERBOSE << "Layer: " << layer << ", Visible: " << layerVisible;

                viz::CoinLayerID layerID(component, layer);
                visualizer.showLayer(layerID, layerVisible);
            }
        }
    }

    void ArVizWidgetController::layersChanged(std::vector<viz::CoinLayerID> const& layers)
    {
        QTreeWidget* tree = widget.layerTree;

        std::map<std::string, QTreeWidgetItem*> currentComponents;
        std::map<viz::CoinLayerID, QTreeWidgetItem*> currentLayers;
        int componentCount = widget.layerTree->topLevelItemCount();
        for (int compIndex = 0; compIndex < componentCount; ++compIndex)
        {
            QTreeWidgetItem* componentItem = widget.layerTree->topLevelItem(compIndex);
            std::string component = componentItem->text(0).toStdString();
            currentComponents.emplace(component, componentItem);

            int layerCount = componentItem->childCount();
            for (int layerIndex = 0; layerIndex < layerCount; ++layerIndex)
            {
                QTreeWidgetItem* layerItem = componentItem->child(layerIndex);
                std::string layer = layerItem->text(0).toStdString();

                viz::CoinLayerID layerID(component, layer);
                currentLayers.emplace(layerID, layerItem);
            }
        }

        const QList<QTreeWidgetItem*> selectedItems = widget.layerTree->selectedItems();

        // We need to determine which layers are new and where to append them
        QTreeWidgetItem* currentItem = nullptr;
        bool componentWasNew = false;
        std::string currentComponent;
        for (auto& entry : layers)
        {
            std::string const& component = entry.first;
            if (component != currentComponent)
            {
                auto iter = currentComponents.find(component);
                if (iter == currentComponents.end())
                {
                    // Create a new item
                    currentItem = new QTreeWidgetItem(tree);
                    currentItem->setText(0, QString::fromStdString(component));
                    // A new item is visible by default
                    currentItem->setCheckState(0, Qt::Checked);

                    componentWasNew = true;
                }
                else
                {
                    // Item exists already
                    currentItem = iter->second;

                    componentWasNew = false;
                }

                currentComponent = component;
            }

            auto iter = currentLayers.find(entry);
            if (iter == currentLayers.end())
            {
                // Create a new item
                std::string const& layer = entry.second;
                QTreeWidgetItem* layerItem = new QTreeWidgetItem;
                layerItem->setText(0, QString::fromStdString(layer));
                // A new item is visible by default
                layerItem->setCheckState(0, Qt::Checked);

                if (currentItem)
                {
                    currentItem->addChild(layerItem);

                    if (componentWasNew)
                    {
                        // Initially expand new items
                        tree->expandItem(currentItem);
                    }
                }
                else
                {
                    ARMARX_WARNING << deactivateSpam(10, entry.first + entry.second)
                                   << "Invalid component/layer ID: "
                                   << entry.first << " / " << entry.second;
                }
            }
            else
            {
                // Item exists already ==> nothing to be done
            }
        }
    }

    void ArVizWidgetController::updateSelectedLayer(QTreeWidgetItem* current, QTreeWidgetItem* previous)
    {
#if ENABLE_INTROSPECTION
        (void) previous;

        if (!current->parent())
        {
            // A component was selected.
            return;
        }

        // A layer was selected.
        viz::CoinLayerID id(current->parent()->text(0).toStdString(), current->text(0).toStdString());

        viz::CoinLayer* layer = visualizer.layers.findLayer(id);
        if (layer == nullptr)
        {
            ARMARX_WARNING << "Could not find layer (" << id.first << " / " << id.second << ") in Visualizer.";
        }
        else
        {
            layerInfoTree.setSelectedLayer(id, &visualizer.layers);
        }

#endif
    }

    void ArVizWidgetController::onCollapseAll(bool)
    {
        widget.layerTree->collapseAll();
    }

    void ArVizWidgetController::onExpandAll(bool)
    {
        widget.layerTree->expandAll();
    }

    void ArVizWidgetController::onHideAll(bool)
    {
        showAllLayers(false);
    }

    void ArVizWidgetController::onShowAll(bool)
    {
        showAllLayers(true);
    }

    void ArVizWidgetController::onHideFiltered(bool)
    {
        showFilteredLayers(false);
    }

    void ArVizWidgetController::onShowFiltered(bool)
    {
        showFilteredLayers(true);
    }

    void ArVizWidgetController::onFilterTextChanged(const QString& filter)
    {
        // Now we need to show these matches and hide the other items
        // Is there a better way? Probably, via QSortFilterProxyModel...
        QRegExp rx(filter, Qt::CaseInsensitive, QRegExp::Wildcard);
        for (QTreeWidgetItemIterator iter(widget.layerTree); *iter; ++iter)
        {
            QTreeWidgetItem* item = *iter;
            QString itemText = item->text(0);
            bool matches = filter.size() == 0 || itemText.contains(rx);
            item->setHidden(!matches);
            if (matches && item->parent())
            {
                // Make parent visible if a child is visible
                item->parent()->setHidden(false);
            }
        }
    }

    void ArVizWidgetController::showAllLayers(bool visible)
    {
        widget.layerTree->blockSignals(true);

        for (QTreeWidgetItemIterator iter(widget.layerTree); *iter; ++iter)
        {
            QTreeWidgetItem* item = *iter;
            item->setCheckState(0, visible ? Qt::Checked : Qt::Unchecked);
        }

        widget.layerTree->blockSignals(false);

        // Update shown/hidden layers
        layerTreeChanged(nullptr, 0);
    }

    void ArVizWidgetController::showFilteredLayers(bool visible)
    {
        widget.layerTree->blockSignals(true);

        QString filter = widget.filterEdit->text();
        QRegExp rx(filter, Qt::CaseInsensitive, QRegExp::Wildcard);

        for (QTreeWidgetItemIterator iter(widget.layerTree); *iter; ++iter)
        {
            QTreeWidgetItem* item = *iter;
            QString itemText = item->text(0);
            bool matches = filter.size() == 0 || itemText.contains(rx);
            if (matches)
            {
                item->setCheckState(0, visible ? Qt::Checked : Qt::Unchecked);
            }
        }

        widget.layerTree->blockSignals(false);

        // Update shown/hidden layers
        layerTreeChanged(nullptr, 0);
    }

    void ArVizWidgetController::onUpdate()
    {
        visualizer.update();
    }

    void ArVizWidgetController::onTimingObserverUpdate()
    {
        viz::CoinVisualizer_UpdateTiming timing = visualizer.getTiming();
        //if (timing.counter > lastTiming.counter)
        {
            if (debugObserver)
            {
                timingMap["0. pull (ms)"] = new Variant(timing.pull.toMilliSecondsDouble());
                timingMap["1. apply (ms)"] = new Variant(timing.applyTotal.total.toMilliSecondsDouble());
                timingMap["1.1 apply, addLayer (ms)"] = new Variant(timing.applyTotal.addLayer.toMilliSecondsDouble());
                timingMap["1.2 apply, updateElements (ms)"] = new Variant(timing.applyTotal.updateElements.toMilliSecondsDouble());
                timingMap["1.3 apply, removeElements (ms)"] = new Variant(timing.applyTotal.removeElements.toMilliSecondsDouble());
                timingMap["2. layers (ms)"] = new Variant(timing.layersChanged.toMilliSecondsDouble());
                timingMap["3. wait duration (ms)"] = new Variant(timing.waitDuration.toMilliSecondsDouble());
                timingMap["4. update toggle"] = new Variant(timing.updateToggle);
                timingMap["total (ms)"] = new Variant(timing.total.toMilliSecondsDouble());

                timings.push_back(timing.total.toMilliSecondsDouble());
                int numTimings = 20;
                if ((int)timings.size() > numTimings)
                {
                    timings.erase(timings.begin());
                }
                double averageTime = std::accumulate(timings.begin(), timings.end(), 0.0) / numTimings;
                timingMap["total avg (ms)"] = new Variant(averageTime);

                debugObserver->begin_setDebugChannel("ArViz_Timing", timingMap);
            }
        }
    }

    void ArVizWidgetController::onStartRecording()
    {
        std::string recordingID = widget.recordingIdTextBox->text().toStdString();

        std::string runningID = storage->startRecording(recordingID);

        changeMode(ArVizWidgetMode::Recording);

        if (runningID.size() > 0)
        {
            std::string message = "There is already a recording running with ID '"
                                  + runningID + "'. \n"
                                  + "You have to stop the running recording first";
            QMessageBox::information(widget.tabWidget,
                                     "Recording running",
                                     QString::fromStdString(message));



            return;
        }
    }

    void ArVizWidgetController::onStopRecording()
    {
        storage->stopRecording();

        onRefreshRecordings();
        changeMode(ArVizWidgetMode::Live);
    }

    void ArVizWidgetController::onRefreshRecordings()
    {
        allRecordings = storage->getAllRecordings();
        std::sort(allRecordings.begin(), allRecordings.end(),
                  [](viz::data::Recording const & lhs, viz::data::Recording const & rhs)
        {
            return lhs.id < rhs.id;
        });

        std::string currentText;
        QListWidgetItem* currentItem = widget.recordingList->currentItem();
        if (currentItem)
        {
            currentText = currentItem->text().toStdString();
        }

        widget.recordingList->clear();
        int currentTextIndex = -1;
        int index = 0;
        for (auto& recording : allRecordings)
        {
            if (recording.id == currentText)
            {
                currentTextIndex = index;
            }
            widget.recordingList->addItem(QString::fromStdString(recording.id));
            ++index;
        }

        if (currentTextIndex > 0)
        {
            widget.recordingList->setCurrentRow(currentTextIndex);
        }
    }

    void ArVizWidgetController::onRecordingSelectionChanged(QListWidgetItem* current, QListWidgetItem* previous)
    {
        if (!current)
        {
            return;
        }

        std::string selectedID = current->text().toStdString();
        for (viz::data::Recording const& recording : allRecordings)
        {
            if (recording.id == selectedID)
            {
                selectRecording(recording);
                break;
            }
        }
    }

    static std::string timestampToString(long timestampInMicroSeconds, bool showMS = false)
    {
        IceUtil::Time time = IceUtil::Time::microSeconds(timestampInMicroSeconds);
        std::string timeString = time.toDateTime();
        if (!showMS)
        {
            timeString = timeString.substr(0, timeString.size() - 4);
        };
        return timeString;
    }

    void ArVizWidgetController::onReplaySpinChanged(int newValue)
    {
        widget.replayRevisionSlider->setValue(newValue);
    }

    void ArVizWidgetController::onReplaySliderChanged(int newValue)
    {
        if (currentRevision != newValue)
        {
            long timestamp = replayToRevision(newValue);
            if (timestamp > 0)
            {
                currentRevision = newValue;
                currentTimestamp = timestamp;
                widget.replayRevisionSpinBox->setValue(newValue);


                widget.replayTimestampLabel->setText(QString::fromStdString(
                        timestampToString(timestamp, true)
                                                     ));
            }
            else
            {
                widget.replayRevisionSlider->setValue(currentRevision);
            }
        }
    }

    void ArVizWidgetController::selectRecording(const viz::data::Recording& recording)
    {
        // Update recording description
        widget.recordingIdLabel->setText(QString::fromStdString(recording.id));

        widget.recordingRevisionLabel->setText(QString::fromStdString(
                std::to_string(recording.firstRevision) +
                " - " +
                std::to_string(recording.lastRevision)));

        std::string firstTimeString = timestampToString(recording.firstTimestampInMicroSeconds);
        std::string lastTimeString = timestampToString(recording.lastTimestampInMicroSeconds);

        widget.recordingTimestampLabel->setText(QString::fromStdString(
                firstTimeString +
                " - " +
                lastTimeString));

        IceUtil::Time duration = IceUtil::Time::microSeconds(
                                     recording.lastTimestampInMicroSeconds -
                                     recording.firstTimestampInMicroSeconds);
        int durationSeconds = duration.toSeconds();
        widget.recordingDurationLabel->setText(QString::fromStdString(
                std::to_string(durationSeconds) + " s"));

        widget.recordingBatchesLabel->setText(QString::fromStdString(
                std::to_string(recording.batchHeaders.size())
                                              ));

        // Update replay control
        currentRecording = recording;
        currentRecordingSelected = true;
        enableWidgetAccordingToMode();
    }

    void ArVizWidgetController::onReplayStart(bool)
    {
        if (!currentRecordingSelected)
        {
            ARMARX_WARNING << "No recording selected, so no replay can be started";
            return;
        }
        {
            std::unique_lock<std::mutex> lock(recordingBatchCacheMutex);
            recordingBatchCache.clear();
        }

        visualizer.stop();

        changeMode(ArVizWidgetMode::ReplayingManual);

        widget.replayRevisionSpinBox->blockSignals(true);
        widget.replayRevisionSpinBox->setMinimum(currentRecording.firstRevision);
        widget.replayRevisionSpinBox->setMaximum(currentRecording.lastRevision);
        widget.replayRevisionSpinBox->setValue(currentRecording.firstRevision);
        widget.replayRevisionSpinBox->blockSignals(false);

        widget.replayRevisionSlider->blockSignals(true);
        widget.replayRevisionSlider->setMinimum(currentRecording.firstRevision);
        widget.replayRevisionSlider->setMaximum(currentRecording.lastRevision);
        widget.replayRevisionSlider->setValue(currentRecording.firstRevision);
        widget.replayRevisionSlider->blockSignals(false);

        currentRevision = -1;
        onReplaySliderChanged(widget.replayRevisionSlider->value());
    }

    void ArVizWidgetController::onReplayStop(bool)
    {
        visualizer.startAsync(storage);

        changeMode(ArVizWidgetMode::Live);
    }

    long ArVizWidgetController::replayToRevision(long revision)
    {
        if (mode != ArVizWidgetMode::ReplayingManual && mode != ArVizWidgetMode::ReplayingTimed)
        {
            ARMARX_WARNING << "Cannot call replayToRevision, when not in replaying mode.\n"
                           << "Actual mode: "  << int(mode);
            return -1;
        }

        viz::data::RecordingBatchHeader* matchingBatchHeader = nullptr;
        for (auto& batchHeader : currentRecording.batchHeaders)
        {
            if (batchHeader.firstRevision <= revision &&
                revision <= batchHeader.lastRevision)
            {
                matchingBatchHeader = &batchHeader;
                break;
            }
        }
        if (matchingBatchHeader == nullptr)
        {
            ARMARX_WARNING << "Could not find batch for revision: " << revision;
            return -1;
        }

        viz::data::RecordingBatch const& batch = getRecordingBatch(matchingBatchHeader->index);

        ARMARX_VERBOSE << "Replaying to revision : " << revision
                       << "\nGot batch: " << batch.header.firstRevision << " - " << batch.header.lastRevision
                       << "\nUpdates: " << batch.updates.size()
                       << "\nInitial state: " << batch.initialState.size();


        auto revisionLess = [](viz::data::TimestampedLayerUpdate const & lhs, viz::data::TimestampedLayerUpdate const & rhs)
        {
            return lhs.revision < rhs.revision;
        };
        viz::data::TimestampedLayerUpdate pivot;
        pivot.revision = revision;
        auto updateBegin = std::lower_bound(batch.updates.begin(), batch.updates.end(), pivot, revisionLess);
        auto updateEnd = std::upper_bound(updateBegin, batch.updates.end(), pivot, revisionLess);

        // TODO: Optimize: Only start from the last update position
        std::map<std::string, viz::data::LayerUpdate const*> updates;

        for (auto& update : batch.initialState)
        {
            updates[update.update.name] = &update.update;
        }
        for (auto updateIter = batch.updates.begin(); updateIter != updateEnd; ++updateIter)
        {
            updates[updateIter->update.name] = &updateIter->update;
        }

        auto layerIDsBefore = visualizer.getLayerIDs();
        for (auto& pair : updates)
        {
            visualizer.apply(*pair.second);
        }
        auto layerIDsAfter = visualizer.getLayerIDs();
        if (layerIDsAfter != layerIDsBefore)
        {
            visualizer.emitLayersChanged(layerIDsAfter);
        }

        return updateBegin->timestampInMicroseconds;
    }

    long ArVizWidgetController::getRevisionForTimestamp(long timestamp)
    {
        if (mode != ArVizWidgetMode::ReplayingManual && mode != ArVizWidgetMode::ReplayingTimed)
        {
            ARMARX_WARNING << "Cannot call replayToTimestamp, when not in replaying mode.\n"
                           << "Actual mode: "  << int(mode);
            return -1;
        }

        if (timestamp < currentRecording.firstTimestampInMicroSeconds)
        {
            ARMARX_INFO << "Requested timestamp is earlier than recording: " << timestampToString(timestamp);
            return -1;
        }

        viz::data::RecordingBatchHeader* matchingBatchHeader = nullptr;
        for (auto& batchHeader : currentRecording.batchHeaders)
        {
            matchingBatchHeader = &batchHeader;
            if (timestamp <= batchHeader.lastTimestampInMicroSeconds)
            {
                break;
            }
        }

        viz::data::RecordingBatch const& batch = getRecordingBatch(matchingBatchHeader->index);

        auto timestampLess = [](viz::data::TimestampedLayerUpdate const & lhs, viz::data::TimestampedLayerUpdate const & rhs)
        {
            return lhs.timestampInMicroseconds < rhs.timestampInMicroseconds;
        };
        viz::data::TimestampedLayerUpdate pivot;
        pivot.timestampInMicroseconds = timestamp;
        auto updateEnd = std::lower_bound(batch.updates.begin(), batch.updates.end(), pivot, timestampLess);
        if (updateEnd == batch.updates.end())
        {
            return -2;
        }
        if (updateEnd != batch.updates.begin())
        {
            // lower_bound gives the first entry with a later timestamp then the goal
            // So we should only apply updates before this point
            updateEnd -= 1;
        }

        long revisionBeforeTimestamp = updateEnd->revision;
        return revisionBeforeTimestamp;
    }

    void ArVizWidgetController::onReplayTimedStart(bool checked)
    {
        if (checked)
        {

            changeMode(ArVizWidgetMode::ReplayingTimed);
            replayCurrentTimestamp = currentTimestamp;
        }
        else
        {
            changeMode(ArVizWidgetMode::ReplayingManual);
        }
    }

    void ArVizWidgetController::onReplayTimerTick()
    {
        if (mode == ArVizWidgetMode::ReplayingTimed)
        {
            double replaySpeed = widget.replaySpeedSpinBox->value();

            long currentRealTime = IceUtil::Time::now().toMicroSeconds();
            long elapsedRealTime = currentRealTime - lastReplayRealTime;

            replayCurrentTimestamp += elapsedRealTime * replaySpeed;

            long revision = getRevisionForTimestamp(replayCurrentTimestamp);
            if (revision == -2)
            {
                if (widget.replayLoopbackCheckBox->checkState() == Qt::Checked)
                {
                    replayCurrentTimestamp = currentRecording.firstTimestampInMicroSeconds;
                }
                else
                {
                    revision = currentRecording.lastRevision;
                }
            }
            if (revision >= 0)
            {
                widget.replayRevisionSlider->setValue(revision);
            }
        }

        lastReplayRealTime = IceUtil::Time::now().toMicroSeconds();
    }

    void ArVizWidgetController::changeMode(ArVizWidgetMode newMode)
    {
        mode = newMode;

        enableWidgetAccordingToMode();
    }

    void ArVizWidgetController::enableWidgetAccordingToMode()
    {
        switch (mode)
        {
            case ArVizWidgetMode::NotConnected:
            {
                widget.recordingStartButton->setDisabled(true);
                widget.recordingStopButton->setDisabled(true);
                widget.replayStartButton->setDisabled(true);
                widget.replayStopButton->setDisabled(true);
                widget.replayControlGroupBox->setDisabled(true);
            }
            break;
            case ArVizWidgetMode::Live:
            {
                widget.recordingStartButton->setDisabled(false);
                widget.recordingStopButton->setDisabled(true);
                widget.replayStartButton->setDisabled(false);
                widget.replayStopButton->setDisabled(true);
                widget.replayControlGroupBox->setDisabled(true);
                widget.recordingList->setDisabled(false);
            }
            break;
            case ArVizWidgetMode::Recording:
            {
                widget.recordingStartButton->setDisabled(true);
                widget.recordingStopButton->setDisabled(false);
                widget.replayStartButton->setDisabled(true);
                widget.replayStopButton->setDisabled(true);
                widget.replayControlGroupBox->setDisabled(true);
            }
            break;
            case ArVizWidgetMode::ReplayingManual:
            {
                widget.recordingStartButton->setDisabled(true);
                widget.recordingStopButton->setDisabled(true);
                widget.replayStartButton->setDisabled(true);
                widget.replayStopButton->setDisabled(false);
                widget.replayControlGroupBox->setDisabled(false);
                widget.replayRevisionSlider->setDisabled(false);
                widget.replayRevisionSpinBox->setDisabled(false);
                widget.recordingList->setDisabled(true);
            }
            break;
            case ArVizWidgetMode::ReplayingTimed:
            {
                widget.recordingStartButton->setDisabled(true);
                widget.recordingStopButton->setDisabled(true);
                widget.replayStartButton->setDisabled(true);
                widget.replayStopButton->setDisabled(false);
                widget.replayControlGroupBox->setDisabled(false);
                widget.replayRevisionSlider->setDisabled(true);
                widget.replayRevisionSpinBox->setDisabled(true);
                widget.recordingList->setDisabled(true);
            }
            break;
        }

        if (!currentRecordingSelected)
        {
            widget.replayStartButton->setDisabled(true);
            widget.replayStopButton->setDisabled(true);
        }
    }

    void ArVizWidgetController::onGetBatchAsync(const viz::data::RecordingBatch& batch)
    {
        // We received a batch asynchronously ==> Update the cache
        ARMARX_INFO << "Received async batch: " << batch.header.index;
        std::unique_lock<std::mutex> lock(recordingBatchCacheMutex);

        auto& entry = recordingBatchCache[batch.header.index];
        entry.data = batch;
        entry.lastUsed = IceUtil::Time::now();

        limitRecordingBatchCacheSize();

        recordingWaitingForBatchIndex = -1;
    }

    viz::data::RecordingBatch const& ArVizWidgetController::getRecordingBatch(long index)
    {
        ARMARX_TRACE;

        IceUtil::Time now = IceUtil::Time::now();

        std::unique_lock<std::mutex> lock(recordingBatchCacheMutex);

        auto iter = recordingBatchCache.find(index);
        if (iter != recordingBatchCache.end())
        {
            // Start prefetching neighbouring batches
            bool asyncPrefetchIsRunning = callbackResult && !callbackResult->isCompleted();
            if (!asyncPrefetchIsRunning && recordingWaitingForBatchIndex == -1)
            {
                if (index + 1 < long(currentRecording.batchHeaders.size())
                    && recordingBatchCache.count(index + 1) == 0)
                {
                    //                    ARMARX_WARNING << "after begin_getRecordingBatch: " << (index + 1)
                    //                                   << " waiting for " << recordingWaitingForBatchIndex;
                    callbackResult = storage->begin_getRecordingBatch(currentRecording.id, index + 1, callback);
                    recordingWaitingForBatchIndex = index + 1;
                    ARMARX_INFO << "Now waiting for " << recordingWaitingForBatchIndex;
                }
                else if (index > 0 && recordingBatchCache.count(index - 1) == 0)
                {
                    //                    ARMARX_WARNING << "before begin_getRecordingBatch: " << (index - 1)
                    //                                   << " waiting for " << recordingWaitingForBatchIndex;
                    callbackResult = storage->begin_getRecordingBatch(currentRecording.id, index - 1, callback);
                    recordingWaitingForBatchIndex = index - 1;
                }

            }

            TimestampedRecordingBatch& entry = iter->second;
            entry.lastUsed = now;
            return entry.data;
        }

        // Maybe there has already been sent a asynchronous request to get
        if (index == recordingWaitingForBatchIndex)
        {
            lock.unlock();
            ARMARX_INFO << "Waiting to receive async batch: " << index;
            // Wait until request completes
            while (recordingWaitingForBatchIndex != -1)
            {
                QCoreApplication::processEvents();
            }
            return getRecordingBatch(index);
        }

        ARMARX_INFO << "Batch #" << index << " is not in the cache. Getting synchronously, blocking GUI...";

        // Entry is not in the cache, we have to get it from ArVizStorage
        auto& newEntry = recordingBatchCache[index];
        newEntry.lastUsed = now;
        newEntry.data = storage->getRecordingBatch(currentRecording.id, index);

        limitRecordingBatchCacheSize();

        return newEntry.data;
    }

    void ArVizWidgetController::limitRecordingBatchCacheSize()
    {
        if (recordingBatchCache.size() > recordingBatchCacheMaxSize)
        {
            // Remove the entry with the oldest last used timestamp
            auto oldestIter = recordingBatchCache.begin();
            for (auto iter = recordingBatchCache.begin();
                 iter != recordingBatchCache.end(); ++iter)
            {
                TimestampedRecordingBatch& entry = iter->second;
                if (entry.lastUsed < oldestIter->second.lastUsed)
                {
                    oldestIter = iter;
                }
            }

            recordingBatchCache.erase(oldestIter);
        }
    }

    SoNode* ArVizWidgetController::getScene()
    {
        return visualizer.root;
    }

    static const std::string CONFIG_KEY_STORAGE = "Storage";
    static const std::string CONFIG_KEY_DEBUG_OBSERVER = "DebugObserver";

    void ArVizWidgetController::loadSettings(QSettings* settings)
    {
        storageName = settings->value(QString::fromStdString(CONFIG_KEY_STORAGE),
                                      "ArVizStorage").toString().toStdString();
        debugObserverName = settings->value(QString::fromStdString(CONFIG_KEY_DEBUG_OBSERVER),
                                            "DebugObserver").toString().toStdString();
    }

    void ArVizWidgetController::saveSettings(QSettings* settings)
    {
        settings->setValue(QString::fromStdString(CONFIG_KEY_STORAGE),
                           QString::fromStdString(storageName));
        settings->setValue(QString::fromStdString(CONFIG_KEY_DEBUG_OBSERVER),
                           QString::fromStdString(debugObserverName));
    }

    QPointer<QDialog> ArVizWidgetController::getConfigDialog(QWidget* parent)
    {
        if (!configDialog)
        {
            configDialog = new SimpleConfigDialog(parent);
            configDialog->addProxyFinder<armarx::viz::StorageInterfacePrx>({CONFIG_KEY_STORAGE, "ArViz Storage", "ArViz*"});
            configDialog->addProxyFinder<armarx::DebugObserverInterfacePrx>({CONFIG_KEY_DEBUG_OBSERVER, "Debug observer", "DebugObserver"});
        }
        return qobject_cast<QDialog*>(configDialog);
    }

    void ArVizWidgetController::configured()
    {
        if (configDialog)
        {
            storageName = configDialog->getProxyName(CONFIG_KEY_STORAGE);
            debugObserverName = configDialog->getProxyName(CONFIG_KEY_DEBUG_OBSERVER);
        }
    }


    void ArVizWidgetController::exportToVRML()
    {

        QString fi = QFileDialog::getSaveFileName(Q_NULLPTR, tr("VRML 2.0 File"), QString(), tr("VRML Files (*.wrl)"));
        std::string s = std::string(fi.toLatin1());

        if (s.empty())
        {
            return;
        }
        if (!boost::algorithm::ends_with(s, ".wrl"))
        {
            s += ".wrl";
        }

        visualizer.exportToVRML(s);
    }
}