#include "NemoInterface.h"

#include "QGCApplication.h"
#include "QGCLoggingCategory.h"
#include "QGCToolbox.h"
#include "SettingsFact.h"
#include "SettingsManager.h"
#include "WimaSettings.h"

#include <mutex>

#include <QJsonArray>
#include <QJsonObject>
#include <QTimer>
#include <QUrl>

#include "GenericSingelton.h"
#include "geometry/MeasurementArea.h"
#include "geometry/geometry.h"
#include "nemo_interface/MeasurementTile.h"
#include "nemo_interface/QNemoHeartbeat.h"
#include "nemo_interface/TaskDispatcher.h"

#include "ros_bridge/include/messages/geographic_msgs/geopoint.h"
#include "ros_bridge/include/messages/nemo_msgs/heartbeat.h"
#include "ros_bridge/include/messages/nemo_msgs/progress_array.h"
#include "ros_bridge/include/messages/nemo_msgs/tile.h"
#include "ros_bridge/include/messages/nemo_msgs/tile_array.h"
#include "rosbridge/rosbridge.h"

/*
 * Here some rules:
 * * not threas safe functions are marked with *NotSafe(...)
 * * If a function is not safe:
 *     * the call to it must be protected with a Lock.
 *     * not safe functions are not allowed to emit signals directly (danger of
 * deadlock).
 *     * if a not safe function needs to emit a signal, defere it with
 *       QTimer::singleShot().
 *     * not safe functions are allowed to call other not safe functions
 *     * it is a bad idea to wait inside a not safe function for a asynchronous
 * operation (potential deadlock)
 * * Functions that are not marked with *NotSafe(...) must be thread safe!
 */

#define INVM(context, fun)                                                     \
  {                                                                            \
    auto value = QMetaObject::invokeMethod(context, fun);                      \
    Q_ASSERT(value == true);                                                   \
    Q_UNUSED(value);                                                           \
  }

Q_DECLARE_METATYPE(ProgressArray)

QGC_LOGGING_CATEGORY(NemoInterfaceLog, "NemoInterfaceLog")

#define NO_HEARTBEAT_TIMEOUT 5000   // ms
#define RESTART_INTERVAl 600000     // ms == 10 min
#define RESTART_RETRY_INTERVAl 2000 // ms
#define SYNC_INTERVAL 10000         // ms
#define SYNC_RETRY_INTERVAL 2000    // ms

static constexpr auto maxResponseTime = std::chrono::milliseconds(20000);

static char const *progressTopic = "/nemo/progress";
static char const *heartbeatTopic = "/nemo/heartbeat";

static char const *addTilesService = "/nemo/add_tiles";
static char const *removeTilesService = "/nemo/remove_tiles";
static char const *clearTilesService = "/nemo/clear_tiles";
static char const *getAllTilesService = "/nemo/get_all_tiles";
// static char const *getTilesService = "/nemo/get_tiles";
// static char const *containsTilesService = "/nemo/contains_tiles";
// static char const *extractTilesService = "/nemo/extract_tiles";
// static char const *sizeService = "/nemo/size";
// static char const *emptyService = "/nemo/empty";
static char const *getProgressService = "/nemo/get_progress";
static char const *getAllProgressService = "/nemo/get_all_progress";
static char const *getVersionService = "/nemo/get_version";

static const std::vector<char const *> requiredServices{
    addTilesService,    removeTilesService,    clearTilesService,
    getAllTilesService, getAllProgressService, getProgressService,
    getVersionService};

static const std::vector<char const *> requiredTopics{progressTopic,
                                                      heartbeatTopic};

using hrc = std::chrono::high_resolution_clock;
using ROSBridgePtr = std::shared_ptr<Rosbridge>;

typedef ros_bridge::messages::nemo_msgs::tile::GenericTile<QGeoCoordinate,
                                                           QList>
    Tile;
typedef std::map<QString, std::shared_ptr<Tile>> TileMap;
typedef std::map<QString, std::shared_ptr<const Tile>> TileMapConst;
typedef ros_bridge::messages::nemo_msgs::heartbeat::Heartbeat Heartbeat;
typedef nemo_interface::TaskDispatcher Dispatcher;
typedef std::unique_lock<std::mutex> Lock;

class NemoInterface::Impl {
  enum class STATE {
    STOPPED,
    START_BRIDGE,
    WEBSOCKET_DETECTED,
    TRY_SETUP,
    USER_SYNC,
    SYS_SYNC,
    SYNC_ERROR,
    READY,
    WEBSOCKET_TIMEOUT,
    HEARTBEAT_TIMEOUT
  };

public:
  Impl(NemoInterface *p);
  ~Impl();

  void start();
  void stop();

  // Tile editing.
  // 	Functions that require communication to device.
  std::shared_future<QVariant> addTiles(const TilePtrArray &tileArray);
  std::shared_future<QVariant> removeTiles(const IDArray &idArray);
  std::shared_future<QVariant> clearTiles();

  // 	Functions that don't require communication to device.
  TileArray getTiles(const IDArray &idArray) const;
  TileArray getAllTiles() const;
  LogicalArray containsTiles(const IDArray &idArray) const;
  std::size_t size() const;
  bool empty() const;

  // Progress.
  ProgressArray getProgress() const;
  ProgressArray getProgress(const IDArray &idArray) const;

  NemoInterface::STATUS status() const;
  bool running() const; // thread safe
  bool ready() const;   // thread safe

  const QString &infoString() const;
  const QString &warningString() const;

private:
  void _doSetup();
  void _doActionNotSafe();
  void _synchronize();
  void _tryRestart();
  bool _isSynchronizedNotSafe() const;
  bool _isSynchronized() const;
  void _onHeartbeatTimeout();
  void _onRosbridgeStateChanged();
  bool _updateProgress(const ProgressArray &pArray);
  void _onHeartbeatReceived(const QNemoHeartbeat &hb);
  void _setInfoStringNotSafe(const QString &info);
  void _setWarningStringNotSafe(const QString &warning);
  void _setInfoString(const QString &info);
  void _setWarningString(const QString &warning);

  // state suff
  bool _userSync() const;
  bool _sysSync() const;
  bool _setStateNotSafe(STATE newState);
  static bool _readyNotSafe(STATE s);
  static bool _userSyncNotSafe(STATE s);
  static bool _sysSyncNotSafe(STATE s);
  static bool _runningNotSafe(STATE s);
  static NemoInterface::STATUS _status(STATE state);
  static QString _toString(STATE s);
  static QString _toString(NemoInterface::STATUS s);

  // impl functions
  bool _addTilesImpl(
      std::shared_ptr<QVector<std::shared_ptr<const Tile>>> pTileArray,
      std::shared_ptr<const IDArray> pIdArray);
  bool _removeTilesImpl(std::shared_ptr<const IDArray> pIdArray);
  bool _clearTilesImpl();

  // Worker functions.
  static bool
  _callAddTiles(const QVector<std::shared_ptr<const Tile>> &tileArray,
                Dispatcher &d, Rosbridge &rb);
  static bool _callClearTiles(Dispatcher &d, Rosbridge &rb);
  static ProgressArray _callGetProgress(const IDArray &pIdArray, Dispatcher &d,
                                        Rosbridge &rb);
  static ProgressArray _callGetAllProgress(Dispatcher &d, Rosbridge &rb);
  static QVector<std::shared_ptr<Tile>> _callGetAllTiles(Dispatcher &d,
                                                         Rosbridge &rb);
  static QString _callGetVersion(Dispatcher &d, Rosbridge &rb);
  static bool _callRemoveTiles(const IDArray &pIdArray, Dispatcher &d,
                               Rosbridge &rb);

  // functions to manipulate _remoteTiles
  bool _addTilesRemote(const QVector<std::shared_ptr<const Tile>> tileArray);
  bool _addTilesRemote(const QVector<std::shared_ptr<Tile>> &tileArray);
  void _removeTilesRemote(const IDArray &idArray);
  void _clearTilesRemote();
  void _clearTilesRemoteNotSafe();

  // static and const members
  static const char *_localVersion;
  NemoInterface *const _parent;

  // thread safe members
  ROSBridgePtr _pRosbridge;
  Dispatcher _dispatcher;

  // protected by mutex
  mutable std::mutex _m;
  STATE _state;
  TileMap _remoteTiles;
  TileMapConst _localTiles;
  QString _infoString;
  QString _warningString;

  // Members belonging to _parent->thread()
  QTimer _timeoutTimer;
  QTimer _syncTimer;
  QTimer _restartTimer;
};

const char *NemoInterface::Impl::_localVersion("V_1.0");

using StatusMap = std::map<NemoInterface::STATUS, QString>;
static StatusMap statusMap{
    std::make_pair<NemoInterface::STATUS, QString>(
        NemoInterface::STATUS::NOT_CONNECTED, "Not Connected"),
    std::make_pair<NemoInterface::STATUS, QString>(NemoInterface::STATUS::ERROR,
                                                   "ERROR"),
    std::make_pair<NemoInterface::STATUS, QString>(NemoInterface::STATUS::SYNC,
                                                   "Synchronizing"),
    std::make_pair<NemoInterface::STATUS, QString>(NemoInterface::STATUS::READY,
                                                   "Ready"),
    std::make_pair<NemoInterface::STATUS, QString>(
        NemoInterface::STATUS::TIMEOUT, "Timeout"),
    std::make_pair<NemoInterface::STATUS, QString>(
        NemoInterface::STATUS::WEBSOCKET_DETECTED, "Websocket Detected")};

NemoInterface::Impl::Impl(NemoInterface *p)
    : _parent(p), _state(STATE::STOPPED) {

  // ROS Bridge.
  WimaSettings *wimaSettings =
      qgcApp()->toolbox()->settingsManager()->wimaSettings();
  auto connectionStringFact = wimaSettings->rosbridgeConnectionString();
  auto setConnectionString = [connectionStringFact, this] {
    auto connectionString = connectionStringFact->rawValue().toString();

    bool wasRunning = this->running();
    this->stop();
    this->_pRosbridge = std::make_shared<Rosbridge>(
        QUrl(QString("ws://") + connectionString.toLocal8Bit().data()));
    if (wasRunning) {
      this->start();
    }
  };

  connect(connectionStringFact, &SettingsFact::rawValueChanged,
          setConnectionString);
  setConnectionString();

  // Heartbeat timeout.
  connect(&this->_timeoutTimer, &QTimer::timeout,
          std::bind(&Impl::_onHeartbeatTimeout, this));

  connect(this->_pRosbridge.get(), &Rosbridge::stateChanged, this->_parent,
          [this] { this->_onRosbridgeStateChanged(); });

  connect(&this->_restartTimer, &QTimer::timeout, this->_parent,
          [this] { this->_tryRestart(); });

  connect(&this->_syncTimer, &QTimer::timeout, this->_parent,
          [this] { this->_synchronize(); });

  static std::once_flag flag;
  std::call_once(flag, [] { qRegisterMetaType<ProgressArray>(); });
}

NemoInterface::Impl::~Impl() { this->_pRosbridge->stop(); }

void NemoInterface::Impl::start() {
  Lock lk(this->_m);
  if (!_runningNotSafe(this->_state)) {
    this->_setStateNotSafe(STATE::START_BRIDGE);
    this->_doActionNotSafe();
  }
}

void NemoInterface::Impl::stop() {
  Lock lk(this->_m);
  if (_runningNotSafe(this->_state)) {
    this->_setStateNotSafe(STATE::STOPPED);
    this->_doActionNotSafe();
  }
}

std::shared_future<QVariant>
NemoInterface::Impl::addTiles(const TilePtrArray &tileArray) {
  using namespace nemo_interface;

  // qDebug() << "addTiles called";

  if (tileArray.size() > 0) {

    // copy unknown tiles
    auto pTileArray = std::make_shared<QVector<std::shared_ptr<const Tile>>>();
    auto pIdArray = std::make_shared<IDArray>();

    Lock lk(this->_m);
    for (const auto *pTile : tileArray) {
      auto id = pTile->id();
      const auto it = this->_localTiles.find(id);
      Q_ASSERT(it == _localTiles.end());
      if (Q_LIKELY(it == _localTiles.end())) {
        auto pTileCopy =
            std::make_shared<const Tile>(pTile->coordinateList(), 0.0, id);
        _localTiles.insert(std::make_pair(id, pTileCopy));
        pTileArray->push_back(pTileCopy);
        pIdArray->push_back(id);
      } else {
        qCDebug(NemoInterfaceLog)
            << "addTiles(): tile with id: " << pTile->id() << "already added.";
      }
    }
    if (pTileArray->size() > 0) {
      lk.unlock();
      emit this->_parent->tilesChanged();
      lk.lock();
    }

    // ready for send?
    if (pTileArray->size() > 0 && (this->_readyNotSafe(this->_state) ||
                                   this->_userSyncNotSafe(this->_state))) {

      this->_setStateNotSafe(STATE::USER_SYNC);
      this->_doActionNotSafe();
      lk.unlock();

      // create task.
      auto pTask = std::make_unique<Task>([this, pTileArray, pIdArray] {
        auto ret = this->_addTilesImpl(pTileArray, pIdArray);

        if (ret) {
          Lock lk(this->_m);
          if (this->_isSynchronizedNotSafe()) {
            this->_setStateNotSafe(STATE::READY);
            this->_doActionNotSafe();
          }
        }
        return ret;
      });

      // dispatch command.
      auto ret = _dispatcher.dispatch(std::move(pTask));
      return ret.share();
    }
  }

  std::promise<QVariant> p;
  p.set_value(QVariant(false));
  return p.get_future();
}

std::shared_future<QVariant>
NemoInterface::Impl::removeTiles(const IDArray &idArray) {
  using namespace nemo_interface;

  // qDebug() << "removeTiles called";

  if (idArray.size() > 0) {

    // copy known ids
    auto pIdArray = std::make_shared<IDArray>();

    Lock lk(this->_m);
    for (const auto &id : idArray) {
      const auto it = this->_localTiles.find(id);
      Q_ASSERT(it != _localTiles.end());
      if (Q_LIKELY(it != _localTiles.end())) {
        _localTiles.erase(it);
        pIdArray->push_back(id);
      } else {
        qCDebug(NemoInterfaceLog) << "removeTiles(): unknown id: " << id << ".";
      }
    }
    if (pIdArray->size() > 0) {
      lk.unlock();
      emit this->_parent->tilesChanged();
      lk.lock();
    }

    // ready for send?
    if (pIdArray->size() > 0 && (this->_readyNotSafe(this->_state) ||
                                 this->_userSyncNotSafe(this->_state))) {

      this->_setStateNotSafe(STATE::USER_SYNC);
      this->_doActionNotSafe();
      lk.unlock();

      // create command.
      auto cmd = std::make_unique<Task>([this, pIdArray] {
        auto ret = this->_removeTilesImpl(pIdArray);

        if (ret) {
          Lock lk(this->_m);
          if (this->_isSynchronizedNotSafe()) {
            this->_setStateNotSafe(STATE::READY);
            this->_doActionNotSafe();
          }
        }

        return ret;
      });

      // dispatch command and return.
      auto ret = _dispatcher.dispatch(std::move(cmd));
      auto sfut = ret.share();
      return sfut;
    }
  }

  std::promise<QVariant> p;
  p.set_value(QVariant(false));
  return p.get_future();
}

std::shared_future<QVariant> NemoInterface::Impl::clearTiles() {
  using namespace nemo_interface;

  // qDebug() << "clearTiles called";

  // clear local tiles (_localTiles)
  Lock lk(this->_m);
  if (!_localTiles.empty()) {
    this->_localTiles.clear();
    lk.unlock();
    emit this->_parent->tilesChanged();
    lk.lock();
  }

  if (this->_readyNotSafe(this->_state) || this->_readyNotSafe(this->_state)) {

    this->_setStateNotSafe(STATE::USER_SYNC);
    this->_doActionNotSafe();
    lk.unlock();

    // create command.
    auto pTask = std::make_unique<Task>([this] {
      auto ret = this->_clearTilesImpl();

      if (ret) {
        Lock lk(this->_m);
        if (this->_isSynchronizedNotSafe()) {
          this->_setStateNotSafe(STATE::READY);
          this->_doActionNotSafe();
        }
      }

      return QVariant(ret);
    });

    // dispatch command and return.
    auto ret = _dispatcher.dispatch(std::move(pTask));
    return ret.share();
  } else {
    lk.unlock();
    std::promise<QVariant> p;
    p.set_value(QVariant(false));
    return p.get_future();
  }
}

TileArray NemoInterface::Impl::getTiles(const IDArray &idArray) const {
  TileArray tileArray;

  Lock lk(this->_m);
  if (this->ready()) {
    for (const auto &id : idArray) {
      const auto it = _remoteTiles.find(id);
      if (it != _remoteTiles.end()) {
        MeasurementTile copy;
        copy.setId(it->second->id());
        copy.setProgress(it->second->progress());
        copy.setPath(it->second->tile());
        tileArray.append(std::move(copy));
      }
    }
  } else {
    for (const auto &id : idArray) {
      const auto it = _localTiles.find(id);
      if (it != _localTiles.end()) {
        MeasurementTile copy;
        copy.setId(it->second->id());
        copy.setProgress(it->second->progress());
        copy.setPath(it->second->tile());
        tileArray.append(std::move(copy));
      }
    }
  }

  return tileArray;
}

TileArray NemoInterface::Impl::getAllTiles() const {
  TileArray tileArray;

  Lock lk(this->_m);
  if (this->ready()) {
    for (const auto &entry : _remoteTiles) {
      auto pTile = entry.second;
      MeasurementTile copy;
      copy.setId(pTile->id());
      copy.setProgress(pTile->progress());
      copy.setPath(pTile->tile());
      tileArray.append(std::move(copy));
    }

  } else {
    for (const auto &entry : _localTiles) {
      auto pTile = entry.second;
      MeasurementTile copy;
      copy.setId(pTile->id());
      copy.setProgress(pTile->progress());
      copy.setPath(pTile->tile());
      tileArray.append(std::move(copy));
    }
  }

  return tileArray;
}

LogicalArray NemoInterface::Impl::containsTiles(const IDArray &idArray) const {
  LogicalArray logicalArray;

  Lock lk(this->_m);
  for (const auto &id : idArray) {
    const auto &it = _localTiles.find(id);
    logicalArray.append(it != _localTiles.end());
  }

  return logicalArray;
}

std::size_t NemoInterface::Impl::size() const {

  Lock lk(this->_m);
  return _localTiles.size();
}

bool NemoInterface::Impl::empty() const {
  Lock lk(this->_m);
  return _localTiles.empty();
}

ProgressArray NemoInterface::Impl::getProgress() const {
  ProgressArray progressArray;

  Lock lk(this->_m);
  if (this->_isSynchronizedNotSafe()) {
    for (const auto &entry : _remoteTiles) {
      progressArray.append(
          LabeledProgress{entry.second->progress(), entry.second->id()});
    }
  } else {
    for (const auto &entry : _localTiles) {
      progressArray.append(
          LabeledProgress{entry.second->progress(), entry.second->id()});
    }
  }

  return progressArray;
}

ProgressArray NemoInterface::Impl::getProgress(const IDArray &idArray) const {
  ProgressArray progressArray;

  Lock lk(this->_m);
  if (this->_isSynchronizedNotSafe()) {
    for (const auto &id : idArray) {
      const auto it = _remoteTiles.find(id);
      if (it != _remoteTiles.end()) {
        progressArray.append(
            LabeledProgress{it->second->progress(), it->second->id()});
      }
    }
  } else {
    for (const auto &id : idArray) {
      const auto it = _localTiles.find(id);
      if (it != _localTiles.end()) {
        progressArray.append(
            LabeledProgress{it->second->progress(), it->second->id()});
      }
    }
  }

  return progressArray;
}

NemoInterface::STATUS NemoInterface::Impl::status() const {
  Lock lk(this->_m);
  return _status(this->_state);
}

bool NemoInterface::Impl::running() const {
  Lock lk1(this->_m);
  return _runningNotSafe(this->_state);
}

bool NemoInterface::Impl::ready() const {
  Lock lk1(this->_m);
  return _readyNotSafe(this->_state);
}

bool NemoInterface::Impl::_sysSync() const {
  Lock lk1(this->_m);
  return _sysSyncNotSafe(this->_state);
}

void NemoInterface::Impl::_onHeartbeatTimeout() {
  Lock lk1(this->_m);
  this->_setStateNotSafe(STATE::HEARTBEAT_TIMEOUT);
  this->_doActionNotSafe();
}

void NemoInterface::Impl::_onRosbridgeStateChanged() {
  auto rbState = this->_pRosbridge->state();
  if (rbState == Rosbridge::STATE::CONNECTED) {
    Lock lk1(this->_m);
    if (this->_state == STATE::START_BRIDGE ||
        this->_state == STATE::WEBSOCKET_TIMEOUT) {
      this->_setStateNotSafe(STATE::WEBSOCKET_DETECTED);
      this->_doActionNotSafe();
    }
  } else if (rbState == Rosbridge::STATE::TIMEOUT) {
    Lock lk1(this->_m);
    if (this->_state == STATE::TRY_SETUP || this->_state == STATE::READY ||
        this->_state == STATE::WEBSOCKET_DETECTED ||
        this->_state == STATE::HEARTBEAT_TIMEOUT) {
      this->_setStateNotSafe(STATE::WEBSOCKET_TIMEOUT);
      this->_doActionNotSafe();
    }
  }
}

bool NemoInterface::Impl::_userSync() const {
  Lock lk1(this->_m);
  return _userSyncNotSafe(this->_state);
}

const QString &NemoInterface::Impl::infoString() const {
  Lock lk1(this->_m);
  return _infoString;
}

const QString &NemoInterface::Impl::warningString() const {
  Lock lk1(this->_m);
  return _warningString;
}

bool NemoInterface::Impl::_updateProgress(const ProgressArray &pArray) {

  ProgressArray copy;
  bool error = false;

  Lock lk1(this->_m);
  for (auto itLP = pArray.begin(); itLP != pArray.end(); ++itLP) {

    auto it = _remoteTiles.find(itLP->id());

    if (Q_LIKELY(it != _remoteTiles.end())) {
      it->second->setProgress(itLP->progress());
      copy.push_back(*itLP);
    } else {
      qCDebug(NemoInterfaceLog)
          << "_updateProgress(): tile with id " << itLP->id() << " not found.";
      error = true;
    }
  }
  lk1.unlock();

  if (copy.size() > 0) {
    emit _parent->progressChanged(copy);
  }

  return !error;
}

void NemoInterface::Impl::_onHeartbeatReceived(const QNemoHeartbeat &) {
  INVM(this->_parent,
       ([this]() mutable { this->_timeoutTimer.start(NO_HEARTBEAT_TIMEOUT); }))

  Lock lk(this->_m);
  if (this->_state == STATE::TRY_SETUP ||
      this->_state == STATE::HEARTBEAT_TIMEOUT) {
    this->_setStateNotSafe(STATE::READY);
    this->_doActionNotSafe();
  }
}

void NemoInterface::Impl::_setInfoStringNotSafe(const QString &info) {
  if (_infoString != info) {
    _infoString = info;

    // emit signal later, can't emit while mutex locked!
    QTimer::singleShot(5, this->_parent,
                       [this] { emit this->_parent->infoStringChanged(); });
  }
}

void NemoInterface::Impl::_setWarningStringNotSafe(const QString &warning) {
  if (_warningString != warning) {
    _warningString = warning;

    // emit signal later, can't emit while mutex locked!
    QTimer::singleShot(5, this->_parent,
                       [this] { emit this->_parent->warningStringChanged(); });
  }
}

void NemoInterface::Impl::_setInfoString(const QString &info) {
  Lock lk(this->_m);
  _setInfoStringNotSafe(info);
}

void NemoInterface::Impl::_setWarningString(const QString &warning) {
  Lock lk(this->_m);
  _setWarningStringNotSafe(warning);
}

void NemoInterface::Impl::_doSetup() {
  // create task
  auto pTask = std::make_unique<nemo_interface::Task>([this] {
    auto rb = this->_pRosbridge;
    auto &disp = this->_dispatcher;

    // check if required services are available
    auto cond = [&disp] { return !disp.isInterruptionRequested(); };
    for (const auto &cc : requiredServices) {
      QString s(cc);

      this->_setInfoString("Waiting for required service " + s);

      auto available = rb->waitForService(cc, cond);
      if (!available) {
        return QVariant(false);
      }
    }

    // check if version is compatible{
    this->_setInfoString("Checking version.");
    auto version = _callGetVersion(disp, *rb);
    if (version != _localVersion) {
      this->_setWarningString(
          "Version checking failed. Local protocol version (" +
          QString(_localVersion) + ") does not match remote version (" +
          version + ").");
      return QVariant(false);
    }

    // check if required topics are available
    for (const auto &cc : requiredTopics) {
      QString s(cc);
      this->_setInfoString("Waiting for required topic " + s);
      auto available = rb->waitForTopic(cc, cond);
      if (!available) {
        return QVariant(false);
      }
    }

    // subscribe topics
    rb->subscribeTopic(progressTopic, [this](const QJsonObject &o) {
      ros_bridge::messages::nemo_msgs::progress_array::ProgressArray
          progressArray;
      if (ros_bridge::messages::nemo_msgs::progress_array::fromJson(
              o, progressArray)) {

        // correct range errors of progress
        for (auto &lp : progressArray.progress_array()) {
          bool rangeError = false;
          if (lp.progress() < 0) {
            lp.setProgress(0);
            rangeError = true;
          }
          if (lp.progress() > 100) {
            lp.setProgress(100);
            rangeError = true;
          }

          if (rangeError) {
            qCWarning(NemoInterfaceLog) << progressTopic
                                        << " progress out "
                                           "of range, value was set to: "
                                        << lp.progress();
          }
        }
        auto ret = this->_updateProgress(progressArray.progress_array());
        if (!ret)
          this->_setWarningString("Progress update failed.");
      } else {
        qCWarning(NemoInterfaceLog) << progressTopic
                                    << " not able to "
                                       "create ProgressArray form json: "
                                    << o;
      }
    });

    using namespace ros_bridge::messages;
    rb->subscribeTopic(heartbeatTopic, [this](const QJsonObject &o) {
      nemo_msgs::heartbeat::Heartbeat heartbeat;
      if (nemo_msgs::heartbeat::fromJson(o, heartbeat)) {
        this->_onHeartbeatReceived(heartbeat);
      } else {
        qCWarning(NemoInterfaceLog) << heartbeatTopic
                                    << " not able to "
                                       "create Heartbeat form json: "
                                    << o;
      }
    });

    // now ready
    INVM(this->_parent, ([this]() mutable {
           this->_timeoutTimer.start(NO_HEARTBEAT_TIMEOUT);
         }))
    _setInfoString("");
    _setWarningString("");
    return QVariant(true);
  });

  auto f = _dispatcher.dispatch(std::move(pTask));
  Q_UNUSED(f);
}

void NemoInterface::Impl::_synchronize() {
  Lock lk(this->_m);
  if (this->_state == STATE::READY || this->_state == STATE::SYNC_ERROR) {

    _setStateNotSafe(STATE::SYS_SYNC);
    _doActionNotSafe();

    // copy tiles.
    auto pTileArray = std::make_shared<QVector<std::shared_ptr<const Tile>>>();
    for (auto it = _localTiles.begin(); it != _localTiles.end(); ++it) {
      pTileArray->push_back(it->second);
    }
    lk.unlock();

    // create cmd.
    auto pTask = std::make_unique<nemo_interface::Task>([this, pTileArray] {
      auto rb = this->_pRosbridge;
      auto &disp = this->_dispatcher;
      // are _localTiles and _remoteTiles synced?

      auto remoteTiles = this->_callGetAllTiles(disp, *rb);

      // create tile map;
      std::map<QString, std::shared_ptr<Tile>> tempMap;
      for (auto &&pTile : remoteTiles) {
        auto it = tempMap.find(pTile->id());
        if (Q_LIKELY(it == tempMap.end())) {
          tempMap.insert(std::make_pair(pTile->id(), pTile));
        } else {
          qCDebug(NemoInterfaceLog)
              << "_synchronizeIfNeccessary(): remoteTiles contains "
                 "duplicate id";
        }
      }

      // compare with pTileArray
      bool synced = true;
      for (auto &&pTile : *pTileArray) {
        auto it = tempMap.find(pTile->id());
        if (it == tempMap.end() || it->second->tile() != pTile->tile()) {
          synced = false;
          break;
        }
      }

      if (!synced) {
        auto success = this->_clearTilesImpl();
        if (!success) {
          Lock lk(this->_m);
          _setStateNotSafe(STATE::SYNC_ERROR);
          _doActionNotSafe();
          return QVariant(false);
        }

        auto pIdArray = std::make_shared<IDArray>();
        for (auto &&pTile : *pTileArray) {
          pIdArray->push_back(pTile->id());
        }

        success = this->_addTilesImpl(pTileArray, pIdArray);

        if (!success) {
          Lock lk(this->_m);
          _setStateNotSafe(STATE::SYNC_ERROR);
          _doActionNotSafe();
          return QVariant(false);
        }
      } else {
        // update progress
        this->_clearTilesRemote();
        auto ret = this->_addTilesRemote(*pTileArray);

        if (ret) {
          ProgressArray progress;
          for (auto &&pTile : remoteTiles) {
            progress.push_back(LabeledProgress(pTile->progress(), pTile->id()));
          }
          ret = _updateProgress(progress);
        }

        if (!ret) {
          Lock lk(this->_m);
          this->_setStateNotSafe(STATE::SYNC_ERROR);
          this->_doActionNotSafe();
          lk.unlock();
          this->_setWarningString("Getting progress failed.");
          qCDebug(NemoInterfaceLog)
              << "_addTilesImpl(): _updateProgress failed: unknown id.";
          return QVariant(false);
        }
      }

      // check if local versions are still synced (user could habe modified
      // _localTiles).
      Lock lk(this->_m);
      if (_isSynchronizedNotSafe()) {
        _setStateNotSafe(STATE::READY);
        _doActionNotSafe();
      } else {
        INVM(this->_parent, [this] { this->_synchronize(); })
      }
      lk.unlock();

      return QVariant(true);
    });

    // dispatch command.
    auto ret = _dispatcher.dispatch(std::move(pTask));
    Q_UNUSED(ret);
    INVM(this->_parent, [this] { this->_syncTimer.start(SYNC_INTERVAL); })
  } else {
    INVM(this->_parent, [this] { this->_syncTimer.start(SYNC_RETRY_INTERVAL); })
  }
}

void NemoInterface::Impl::_tryRestart() {
  if (this->running()) {
    if (_dispatcher.idle()) {
      qDebug() << "_tryRestart: restarting";
      this->stop();
      this->start();
      _restartTimer.start(RESTART_INTERVAl);
    } else {
      _restartTimer.start(RESTART_RETRY_INTERVAl);
    }
  }
}

bool NemoInterface::Impl::_isSynchronizedNotSafe() const {
  return _localTiles.size() == _remoteTiles.size() &&
         std::equal(
             _localTiles.begin(), _localTiles.end(), _remoteTiles.begin(),
             [](const auto &a, const auto &b) { return a.first == b.first; });
}

bool NemoInterface::Impl::_isSynchronized() const {
  Lock lk(this->_m);
  return _isSynchronizedNotSafe();
}

void NemoInterface::Impl::_doActionNotSafe() {
  static bool resetDone = false;
  switch (this->_state) {
  case STATE::STOPPED: {
    std::promise<void> p;
    INVM(this->_parent, ([this]() mutable {
           this->_timeoutTimer.stop();
           this->_restartTimer.stop();
           this->_syncTimer.stop();
         }))

    if (this->_pRosbridge->state() != Rosbridge::STATE::STOPPED) {
      this->_pRosbridge->stop();
    }
    _dispatcher.stop();
    _dispatcher.clear();

    _setInfoStringNotSafe("");
    _setWarningStringNotSafe("");

  } break;

  case STATE::START_BRIDGE: {
    INVM(this->_parent,
         ([this]() mutable { this->_restartTimer.start(RESTART_INTERVAl); }))

    this->_pRosbridge->start();
  } break;
  case STATE::WEBSOCKET_DETECTED:
    resetDone = false;
    this->_setStateNotSafe(STATE::TRY_SETUP);
    this->_doActionNotSafe();
    break;
  case STATE::TRY_SETUP:
    this->_doSetup();
    break;
  case STATE::READY: {
    INVM(this->_parent,
         ([this]() mutable { this->_syncTimer.start(SYNC_INTERVAL); }))

    if (!_isSynchronizedNotSafe()) {
      // can't call this here directly.
      QTimer::singleShot(100, this->_parent, [this] { this->_synchronize(); });
    }

    _setInfoStringNotSafe("");
    _setWarningStringNotSafe("");
  } break;
  case STATE::USER_SYNC:
  case STATE::SYS_SYNC:
  case STATE::SYNC_ERROR:
    break;
  case STATE::HEARTBEAT_TIMEOUT: {
    INVM(this->_parent, ([this]() mutable { this->_syncTimer.stop(); }))

  } break;
  case STATE::WEBSOCKET_TIMEOUT: {
    INVM(this->_parent, ([this]() mutable {
           this->_timeoutTimer.stop();
           this->_syncTimer.stop();
         }))

    if (!resetDone) {
      resetDone = true;
      this->_pRosbridge->stop();
      this->_pRosbridge->start();
    }
    _dispatcher.stop();
    _dispatcher.clear();

    _setInfoStringNotSafe("");
    _setWarningStringNotSafe("");
  } break;
  };
}

bool NemoInterface::Impl::_callAddTiles(
    const QVector<std::shared_ptr<const Tile>> &tileArray, Dispatcher &d,
    Rosbridge &rb) {

  // qDebug() << "_callAddTiles called";

  // create json object
  QJsonArray jsonTileArray;
  for (auto &&tile : tileArray) {
    using namespace ros_bridge::messages;
    QJsonObject jsonTile;
    if (!nemo_msgs::tile::toJson(*tile, jsonTile)) {
      qCDebug(NemoInterfaceLog)
          << "addTiles(): not able to create json object: tile id: "
          << tile->id() << " progress: " << tile->progress()
          << " points: " << tile->tile();
    }
    jsonTileArray.append(std::move(jsonTile));
  } // for

  QJsonObject req;
  req["in_tile_array"] = std::move(jsonTileArray);

  // create response handler.
  auto promise_response = std::make_shared<std::promise<bool>>();
  auto future_response = promise_response->get_future();
  auto responseHandler = [promise_response](const QJsonObject &o) mutable {
    // check if transaction was successfull
    if (o.contains("success") && o["success"].isBool()) {
      promise_response->set_value(o["success"].toBool());
    } else {
      qCWarning(NemoInterfaceLog)
          << addTilesService << " no \"success\" key or wrong type: " << o;
      promise_response->set_value(false);
    }
  };

  // call service.
  rb.callService(addTilesService, responseHandler, req);

  // wait for response.
  auto tStart = hrc::now();
  bool abort = true;
  do {
    auto status = future_response.wait_for(std::chrono::milliseconds(100));
    if (status == std::future_status::ready) {
      abort = false;
      break;
    }
  } while (hrc::now() - tStart < maxResponseTime ||
           d.isInterruptionRequested());

  if (abort) {
    qCWarning(NemoInterfaceLog)
        << "addTiles(): Websocket not responding to request.";
    return false;
  }

  // transaction error?
  if (!future_response.get()) {
    return false;
  }

  // return success
  return true;
}

bool NemoInterface::Impl::_callRemoveTiles(const IDArray &pIdArray,
                                           Dispatcher &disp, Rosbridge &rb) {
  // qDebug() << "_callRemoveTiles called";

  // create json object
  QJsonArray jsonIdArray;
  for (auto &&id : pIdArray) {
    using namespace ros_bridge::messages;
    jsonIdArray.append(id);
  } // for
  QJsonObject req;
  req["ids"] = std::move(jsonIdArray);

  // create response handler.
  auto promise_response = std::make_shared<std::promise<bool>>();
  auto future_response = promise_response->get_future();
  auto responseHandler = [promise_response](const QJsonObject &o) mutable {
    // check if transaction was successfull
    QString msg = QJsonDocument(o).toJson(QJsonDocument::JsonFormat::Compact);
    if (o.contains("success") && o["success"].isBool()) {
      promise_response->set_value(o["success"].toBool());
    } else {
      qCWarning(NemoInterfaceLog)
          << removeTilesService << " no \"success\" key or wrong type: " << msg;
      promise_response->set_value(false);
    }
  };

  // call service.
  rb.callService(removeTilesService, responseHandler, req);

  // wait for response.
  auto tStart = hrc::now();
  bool abort = true;
  do {
    auto status = future_response.wait_for(std::chrono::milliseconds(100));
    if (status == std::future_status::ready) {
      abort = false;
      break;
    }
  } while (hrc::now() - tStart < maxResponseTime ||
           disp.isInterruptionRequested());

  if (abort) {
    qCWarning(NemoInterfaceLog)
        << "remove_tiles(): Websocket not responding to request.";
    return false;
  }

  // transaction error?
  if (!future_response.get()) {
    return false;
  }

  return true;
}

bool NemoInterface::Impl::_callClearTiles(Dispatcher &disp, Rosbridge &rb) {

  // create response handler.
  auto promise_response = std::make_shared<std::promise<bool>>();
  auto future_response = promise_response->get_future();
  auto responseHandler = [promise_response](const QJsonObject &) mutable {
    // check if transaction was successfull
    promise_response->set_value(true);
  };

  // call service.
  rb.callService(clearTilesService, responseHandler, QJsonObject());

  // wait for response.
  auto tStart = hrc::now();
  bool abort = true;
  do {
    auto status = future_response.wait_for(std::chrono::milliseconds(100));
    if (status == std::future_status::ready) {
      abort = false;
      break;
    }
  } while (hrc::now() - tStart < maxResponseTime ||
           disp.isInterruptionRequested());

  if (abort) {
    qCWarning(NemoInterfaceLog) << "Websocket not responding to request.";
    return false;
  }

  // transaction failed?
  if (!future_response.get()) {
    return false;
  }

  return true;
}

ProgressArray NemoInterface::Impl::_callGetProgress(const IDArray &pIdArray,
                                                    Dispatcher &disp,
                                                    Rosbridge &rb) {
  // qDebug() << "_callGetProgress called";

  // create json object
  QJsonArray jsonIdArray;
  for (auto &&id : pIdArray) {
    using namespace ros_bridge::messages;
    jsonIdArray.append(id);
  } // for
  QJsonObject req;
  req["ids"] = std::move(jsonIdArray);

  // create response handler.
  typedef std::shared_ptr<ProgressArray> ResponseType;
  auto promise_response = std::make_shared<std::promise<ResponseType>>();
  auto future_response = promise_response->get_future();
  auto responseHandler = [promise_response](const QJsonObject &o) mutable {
    // check if transaction was successfull
    ros_bridge::messages::nemo_msgs::progress_array::ProgressArray
        progressArrayMsg;
    if (ros_bridge::messages::nemo_msgs::progress_array::fromJson(
            o, progressArrayMsg)) {
      auto pArray = std::make_shared<ProgressArray>();
      *pArray = std::move(progressArrayMsg.progress_array());
      promise_response->set_value(pArray);
    } else {
      qCWarning(NemoInterfaceLog) << getProgressService
                                  << " error while creating ProgressArray "
                                     "from json.";
      promise_response->set_value(nullptr);
    }
  };

  // call service.
  rb.callService(getProgressService, responseHandler, req);

  // wait for response.
  auto tStart = hrc::now();
  bool abort = true;
  do {
    auto status = future_response.wait_for(std::chrono::milliseconds(100));
    if (status == std::future_status::ready) {
      abort = false;
      break;
    }
  } while (hrc::now() - tStart < maxResponseTime ||
           disp.isInterruptionRequested());

  if (abort) {
    qCWarning(NemoInterfaceLog)
        << "all_remove_tiles(): Websocket not responding to request.";
    return ProgressArray();
  }

  // return success
  return *future_response.get();
}

ProgressArray NemoInterface::Impl::_callGetAllProgress(Dispatcher &disp,
                                                       Rosbridge &rb) {
  // qDebug() << "_callGetAllProgress called";

  // create response handler.
  typedef std::shared_ptr<ProgressArray> ResponseType;
  auto promise_response = std::make_shared<std::promise<ResponseType>>();
  auto future_response = promise_response->get_future();
  auto responseHandler = [promise_response](const QJsonObject &o) mutable {
    // check if transaction was successfull
    ros_bridge::messages::nemo_msgs::progress_array::ProgressArray
        progressArrayMsg;
    if (ros_bridge::messages::nemo_msgs::progress_array::fromJson(
            o, progressArrayMsg)) {
      auto pArray = std::make_shared<ProgressArray>();
      *pArray = std::move(progressArrayMsg.progress_array());
      promise_response->set_value(pArray);
    } else {
      qCWarning(NemoInterfaceLog) << getAllProgressService
                                  << " error while creating ProgressArray "
                                     "from json. msg: "
                                  << o;
      promise_response->set_value(nullptr);
    }
  };

  // call service.
  rb.callService(getAllProgressService, responseHandler, QJsonObject());

  // wait for response.
  auto tStart = hrc::now();
  bool abort = true;
  do {
    auto status = future_response.wait_for(std::chrono::milliseconds(100));
    if (status == std::future_status::ready) {
      abort = false;
      break;
    }
  } while (hrc::now() - tStart < maxResponseTime ||
           disp.isInterruptionRequested());

  if (abort) {
    qCWarning(NemoInterfaceLog)
        << "_callGetAllProgress(): Websocket not responding to request.";
    return ProgressArray();
  }

  // transaction error?
  return *future_response.get();
}

QVector<std::shared_ptr<Tile>>
NemoInterface::Impl::_callGetAllTiles(Dispatcher &disp, Rosbridge &rb) {
  // qDebug() << "_callGetAllProgress called";

  // create response handler.
  typedef std::shared_ptr<QVector<std::shared_ptr<Tile>>> ResponseType;
  auto promise_response = std::make_shared<std::promise<ResponseType>>();
  auto future_response = promise_response->get_future();

  auto responseHandler = [promise_response](const QJsonObject &o) mutable {
    const char *const tileArrayKey = "tile_array";
    // check if transaction was successfull
    if (o.contains(tileArrayKey) && o[tileArrayKey].isArray()) {
      auto pArray = std::make_shared<QVector<std::shared_ptr<Tile>>>();
      const auto jsonArray = o[tileArrayKey].toArray();
      for (int i = 0; i < jsonArray.size(); ++i) {
        if (jsonArray[i].isObject()) {
          QJsonObject o = jsonArray[i].toObject();
          auto tile = std::make_shared<Tile>();
          if (ros_bridge::messages::nemo_msgs::tile::fromJson(o, *tile)) {
            pArray->push_back(tile);
          } else {
            qCWarning(NemoInterfaceLog)
                << getAllTilesService << " error while creating tile.";
            promise_response->set_value(nullptr);
          }
        } else {
          qCWarning(NemoInterfaceLog)
              << getAllTilesService << " json array does not contain objects.";
          promise_response->set_value(nullptr);
        }
      }
      // success!
      promise_response->set_value(pArray);
    } else {
      qCWarning(NemoInterfaceLog)
          << getAllTilesService << " no tile_array key or wrong type.";
      promise_response->set_value(nullptr);
    }
  };

  // call service.
  rb.callService(getAllTilesService, responseHandler, QJsonObject());

  // wait for response.
  auto tStart = hrc::now();
  bool abort = true;
  do {
    auto status = future_response.wait_for(std::chrono::milliseconds(100));
    if (status == std::future_status::ready) {
      abort = false;
      break;
    }
  } while (hrc::now() - tStart < maxResponseTime ||
           disp.isInterruptionRequested());

  if (abort) {
    qCWarning(NemoInterfaceLog)
        << "all_remove_tiles(): Websocket not responding to request.";
    return QVector<std::shared_ptr<Tile>>();
  }

  return *future_response.get();
}

QString NemoInterface::Impl::_callGetVersion(Dispatcher &d, Rosbridge &rb) {

  // create response handler.
  typedef QString ResponseType;
  auto promise_response = std::make_shared<std::promise<ResponseType>>();
  auto future_response = promise_response->get_future();

  auto responseHandler = [promise_response](const QJsonObject &o) mutable {
    const char *const versionKey = "version";
    // check if transaction was successfull
    if (o.contains(versionKey) && o[versionKey].isString()) {
      const auto version = o[versionKey].toString();
      promise_response->set_value(version);
    } else {
      qCWarning(NemoInterfaceLog)
          << getVersionService << " no version key or wrong type.";
      promise_response->set_value("error!");
    }
  };

  // call service.
  rb.callService(getVersionService, responseHandler, QJsonObject());

  // wait for response.
  auto tStart = hrc::now();
  bool abort = true;
  do {
    auto status = future_response.wait_for(std::chrono::milliseconds(100));
    if (status == std::future_status::ready) {
      abort = false;
      break;
    }
  } while (hrc::now() - tStart < maxResponseTime ||
           d.isInterruptionRequested());

  if (abort) {
    qCWarning(NemoInterfaceLog)
        << "all_remove_tiles(): Websocket not responding to request.";
    return "unknown_version";
  }

  // transaction error?
  auto version = future_response.get();
  if (version == "error!") {
    return "unknown_version";
  }

  // return success
  return version;
}

bool NemoInterface::Impl::_addTilesRemote(
    const QVector<std::shared_ptr<const Tile>> tileArray) {

  // qDebug() << "_addTilesRemote called";

  QVector<std::shared_ptr<Tile>> copy;
  for (auto &&pTile : tileArray) {
    copy.push_back(std::make_shared<Tile>(*pTile));
  }
  return _addTilesRemote(copy);
}

bool NemoInterface::Impl::_addTilesRemote(
    const QVector<std::shared_ptr<Tile>> &tileArray) {

  bool error = false;
  bool anyChange = false;

  Lock lk(this->_m);
  for (auto &&pTile : tileArray) {
    auto id = pTile->id();
    auto it = _remoteTiles.find(id);
    if (Q_LIKELY(it == _remoteTiles.end())) {
      auto ret = _remoteTiles.insert(std::make_pair(id, pTile));
      anyChange = true;
      Q_ASSERT(ret.second == true);
      Q_UNUSED(ret);
    } else {
      if (pTile->tile() != it->second->tile()) {
        qCWarning(NemoInterfaceLog)
            << "_addTilesRemote: tiles differ but have the same id.";
        error = true;
      }
    }
  }

  lk.unlock();
  if (anyChange > 0) {
    emit this->_parent->tilesChanged();
  }

  return !error;
}

void NemoInterface::Impl::_removeTilesRemote(const IDArray &idArray) {
  // qDebug() << "_removeTilesRemote called";
  bool anyChange = false;

  Lock lk(this->_m);
  for (auto &&id : idArray) {
    auto it = _remoteTiles.find(id);
    if (Q_LIKELY(it != _remoteTiles.end())) {
      _remoteTiles.erase(it);
      anyChange = true;
    } else {
      qCWarning(NemoInterfaceLog)
          << "_removeTilesRemote: tile with unknown id " << id << ".";
    }
  }

  if (anyChange) {
    emit this->_parent->tilesChanged();
  }
}

void NemoInterface::Impl::_clearTilesRemote() {
  Lock lk(this->_m);
  _clearTilesRemoteNotSafe();
}

void NemoInterface::Impl::_clearTilesRemoteNotSafe() {
  // qDebug() << "_clearTilesRemote called";
  if (_remoteTiles.size() > 0) {
    _remoteTiles.clear();
  }
}

bool NemoInterface::Impl::_setStateNotSafe(STATE newState) {
  if (newState != this->_state) {
    auto oldState = this->_state;
    this->_state = newState;

    qCDebug(NemoInterfaceLog)
        << "state: " << _toString(oldState) << " -> " << _toString(newState);
    auto oldStatus = _status(oldState);
    auto newStatus = _status(newState);
    if (oldStatus != newStatus) {
      // emit signal later
      QTimer::singleShot(5, this->_parent,
                         [this] { emit this->_parent->statusChanged(); });
    }

    if (_runningNotSafe(oldState) != _runningNotSafe(newState)) {
      // emit signal later
      QTimer::singleShot(5, this->_parent,
                         [this] { emit this->_parent->runningChanged(); });
    }

    return true;
  } else {
    return false;
  }
}

bool NemoInterface::Impl::_readyNotSafe(NemoInterface::Impl::STATE s) {
  return s == STATE::READY;
}

bool NemoInterface::Impl::_userSyncNotSafe(NemoInterface::Impl::STATE s) {
  return s == STATE::USER_SYNC;
}

bool NemoInterface::Impl::_sysSyncNotSafe(NemoInterface::Impl::STATE s) {
  return s == STATE::SYS_SYNC;
}

bool NemoInterface::Impl::_runningNotSafe(NemoInterface::Impl::STATE s) {
  return s != STATE::STOPPED;
}

NemoInterface::STATUS
NemoInterface::Impl::_status(NemoInterface::Impl::STATE state) {
  NemoInterface::STATUS status;
  switch (state) {
  case STATE::STOPPED:
    status = NemoInterface::STATUS::NOT_CONNECTED;
    break;
  case STATE::START_BRIDGE:
    status = NemoInterface::STATUS::NOT_CONNECTED;
    break;
  case STATE::WEBSOCKET_DETECTED:
    status = NemoInterface::STATUS::WEBSOCKET_DETECTED;
    break;
  case STATE::TRY_SETUP:
    status = NemoInterface::STATUS::WEBSOCKET_DETECTED;
    break;
  case STATE::READY:
    status = NemoInterface::STATUS::READY;
    break;
  case STATE::USER_SYNC:
  case STATE::SYS_SYNC:
    status = NemoInterface::STATUS::SYNC;
    break;
  case STATE::SYNC_ERROR:
    status = NemoInterface::STATUS::ERROR;
    break;
  case STATE::WEBSOCKET_TIMEOUT:
  case STATE::HEARTBEAT_TIMEOUT:
    status = NemoInterface::STATUS::TIMEOUT;
    break;
  }

  return status;
}

QString NemoInterface::Impl::_toString(NemoInterface::Impl::STATE s) {
  switch (s) {
  case STATE::STOPPED:
    return QString("STOPPED");
  case STATE::START_BRIDGE:
    return QString("START_BRIDGE");
  case STATE::WEBSOCKET_DETECTED:
    return QString("WEBSOCKET_DETECTED");
  case STATE::TRY_SETUP:
    return QString("TRY_SETUP");
  case STATE::READY:
    return QString("READY");
  case STATE::USER_SYNC:
    return QString("SYNC_USER");
  case STATE::SYS_SYNC:
    return QString("SYNC_SYS");
  case STATE::SYNC_ERROR:
    return QString("SYNC_ERROR");
  case STATE::WEBSOCKET_TIMEOUT:
    return QString("WEBSOCKET_TIMEOUT");
  case STATE::HEARTBEAT_TIMEOUT:
    return QString("HEARTBEAT_TIMEOUT");
  }
  return "unknown state!";
}

QString NemoInterface::Impl::_toString(NemoInterface::STATUS s) {
  switch (s) {
  case NemoInterface::STATUS::NOT_CONNECTED:
    return QString("NOT_CONNECTED");
  case NemoInterface::STATUS::ERROR:
    return QString("ERROR");
  case NemoInterface::STATUS::READY:
    return QString("READY");
  case NemoInterface::STATUS::TIMEOUT:
    return QString("TIMEOUT");
  case NemoInterface::STATUS::WEBSOCKET_DETECTED:
    return QString("WEBSOCKET_DETECTED");
  case NemoInterface::STATUS::SYNC:
    return QString("SYNC");
  }
  return "unknown state!";
}

bool NemoInterface::Impl::_addTilesImpl(
    std::shared_ptr<QVector<std::shared_ptr<const Tile>>> pTileArray,
    std::shared_ptr<const IDArray> pIdArray) {
  auto rb = this->_pRosbridge;
  auto &disp = this->_dispatcher;

  // add tiles
  bool success = this->_callAddTiles(*pTileArray, disp, *rb);
  if (!success) {
    this->_setWarningString(
        "Adding tiles failed. This might indicate a poor connection.");
    return false;
  }

  auto ret = this->_addTilesRemote(*pTileArray);
  if (!ret) {
    Lock lk(this->_m);
    this->_setStateNotSafe(STATE::SYNC_ERROR);
    this->_doActionNotSafe();
    lk.unlock();
    this->_setWarningString("Adding tiles failed.");
    qCDebug(NemoInterfaceLog) << "_addTilesImpl(): _addTilesRemote return "
                                 "false: different tiles with same id.";
    return false;
  }

  // fetch progress
  auto array = this->_callGetProgress(*pIdArray, disp, *rb);
  if (array.size() != pIdArray->size()) {
    Lock lk(this->_m);
    this->_setStateNotSafe(STATE::SYNC_ERROR);
    this->_doActionNotSafe();
    lk.unlock();
    this->_setWarningString("Getting progress failed. This might "
                            "indicate a poor connection.");
    return false;
  }
  ret = this->_updateProgress(array);

  if (!ret) {
    Lock lk(this->_m);
    this->_setStateNotSafe(STATE::SYNC_ERROR);
    this->_doActionNotSafe();
    lk.unlock();
    this->_setWarningString("Getting progress failed.");
    qCDebug(NemoInterfaceLog)
        << "_addTilesImpl(): _updateProgress failed: unknown id.";
    return false;
  }

  return true;
}

bool NemoInterface::Impl::_removeTilesImpl(
    std::shared_ptr<const IDArray> pIdArray) {
  auto rb = this->_pRosbridge;
  auto &disp = this->_dispatcher;

  auto success = this->_callRemoveTiles(*pIdArray, disp, *rb);
  if (!success) {
    Lock lk(this->_m);
    this->_setStateNotSafe(STATE::SYNC_ERROR);
    this->_doActionNotSafe();
    lk.unlock();
    this->_setWarningString("Removing tiles failed. This might "
                            "indicate a poor connection.");
    return false;
  }
  this->_removeTilesRemote(*pIdArray);

  return true;
}

bool NemoInterface::Impl::_clearTilesImpl() {
  auto rb = this->_pRosbridge;
  auto &disp = this->_dispatcher;

  auto success = this->_callClearTiles(disp, *rb);
  if (!success) {
    Lock lk(this->_m);
    this->_setStateNotSafe(STATE::SYNC_ERROR);
    this->_doActionNotSafe();
    lk.unlock();
    this->_setWarningString("Clear tiles failed. This might "
                            "indicate a poor connection.");
    return false;
  }
  this->_clearTilesRemote();

  return true;
}

// ===============================================================
// NemoInterface
NemoInterface::NemoInterface()
    : QObject(), pImpl(std::make_unique<NemoInterface::Impl>(this)) {}

NemoInterface *NemoInterface::createInstance() { return new NemoInterface(); }

NemoInterface *NemoInterface::instance() {
  return GenericSingelton<NemoInterface>::instance(
      NemoInterface::createInstance);
}

NemoInterface::~NemoInterface() {}

void NemoInterface::start() { this->pImpl->start(); }

void NemoInterface::stop() { this->pImpl->stop(); }

std::shared_future<QVariant>
NemoInterface::addTiles(const TileArray &tileArray) {
  TilePtrArray ptrArray;
  for (const auto &tile : tileArray) {
    ptrArray.push_back(const_cast<MeasurementTile *>(&tile));
  }
  return this->pImpl->addTiles(ptrArray);
}

std::shared_future<QVariant>
NemoInterface::addTiles(const TilePtrArray &tileArray) {
  return this->pImpl->addTiles(tileArray);
}

std::shared_future<QVariant>
NemoInterface::removeTiles(const IDArray &idArray) {
  return this->pImpl->removeTiles(idArray);
}

std::shared_future<QVariant> NemoInterface::clearTiles() {
  return this->pImpl->clearTiles();
}

TileArray NemoInterface::getTiles(const IDArray &idArray) const {
  return this->pImpl->getTiles(idArray);
}

TileArray NemoInterface::getAllTiles() const {
  return this->pImpl->getAllTiles();
}

LogicalArray NemoInterface::containsTiles(const IDArray &idArray) const {
  return this->pImpl->containsTiles(idArray);
}

std::size_t NemoInterface::size() const { return this->pImpl->size(); }

bool NemoInterface::empty() const { return this->pImpl->empty(); }

ProgressArray NemoInterface::getProgress() const {
  return this->pImpl->getProgress();
}

ProgressArray NemoInterface::getProgress(const IDArray &idArray) const {
  return this->pImpl->getProgress(idArray);
}

NemoInterface::STATUS NemoInterface::status() const {
  return this->pImpl->status();
}

QString NemoInterface::statusString() const {
  return statusMap.at(this->pImpl->status());
}

QString NemoInterface::infoString() const { return this->pImpl->infoString(); }

QString NemoInterface::warningString() const {
  return this->pImpl->warningString();
}

QString NemoInterface::editorQml() {
  return QStringLiteral("NemoInterface.qml");
}

bool NemoInterface::running() const { return this->pImpl->running(); }
