#include "AreaData.h"

#include "geometry/MeasurementArea.h"
#include "geometry/SafeArea.h"

#include "QGCLoggingCategory.h"
#include "QGCQGeoCoordinate.h"

QGC_LOGGING_CATEGORY(AreaDataLog, "AreaDataLog")

AreaData::AreaData(QObject *parent) : QObject(parent) {}

AreaData::~AreaData() {}

AreaData::AreaData(const AreaData &other, QObject *parent) : QObject(parent) {
  if (!copyAreaList(other._areaList, _areaList, this)) {
    qCWarning(AreaDataLog) << "AreaData(): not able to copy other._areaList";
  } else {
    _origin = other._origin;
  }
}

AreaData &AreaData::operator=(const AreaData &other) {
  if (!copyAreaList(other._areaList, _areaList, this)) {
    qCWarning(AreaDataLog) << "operator=(): not able to copy other._areaList";
  } else {
    _origin = other._origin;
  }
  return *this;
}

bool AreaData::insert(GeoArea *areaData) {
  {
    SafeArea *area = qobject_cast<SafeArea *>(areaData);
    if (area != nullptr) {
      if (Q_LIKELY(!this->_areaList.contains(area))) {
        _areaList.append(area);
        emit areaList();
        return true;
      } else {
        return false;
      }
    }
  }

  {
    MeasurementArea *area = qobject_cast<MeasurementArea *>(areaData);
    if (area != nullptr) {
      if (Q_LIKELY(!this->_areaList.contains(area))) {
        _areaList.append(area);
        emit areaList();
        return true;
      } else {
        return false;
      }
    }
  }

  return false;
}

void AreaData::remove(GeoArea *areaData) {
  int index = _areaList.indexOf(areaData);
  if (index >= 0) {
    QObject *obj = _areaList.removeAt(index);

    _setOrigin(_newOrigin());

    if (obj->parent() == nullptr) {
      obj->deleteLater();
    }

    emit areaListChanged();
  }
}

void AreaData::clear() {
  if (_areaList.count() > 0) {
    for (int i = 0; i < _areaList.count(); ++i) {
      remove(_areaList.value<GeoArea *>(i));
    }
    emit areaListChanged();
  }
}

QmlObjectListModel *AreaData::areaList() { return &_areaList; }

const QmlObjectListModel *AreaData::areaList() const { return &_areaList; }

const QGeoCoordinate &AreaData::origin() const { return _origin; }

bool AreaData::isValid() const {
  qWarning("AreaData::isValid(): impl. incomplete.");
  auto *measurementArea = getGeoArea<const MeasurementArea *>(_areaList);
  auto *safeArea = getGeoArea<const SafeArea *>(_areaList);
  return measurementArea != nullptr && safeArea != nullptr &&
         measurementArea->count() >= 3 && safeArea->count() >= 3 &&
         _origin.isValid();
}

bool AreaData::tryMakeValid() {
  qWarning("AreaData::tryMakeValid(): impl. missing.");
  return true;
}

bool AreaData::initialize(const QGeoCoordinate &bottomLeft,
                          const QGeoCoordinate &topRight) {
  // bottomLeft and topRight define the bounding box.
  if (bottomLeft.isValid() && topRight.isValid() && bottomLeft != topRight) {
    auto *measurementArea = getGeoArea<MeasurementArea *>(_areaList);
    auto *safeArea = getGeoArea<SafeArea *>(_areaList);

    if (safeArea == nullptr) {
      if (!insert(new SafeArea())) {
        qCCritical(AreaDataLog)
            << "initialize(): safeArea == nullptr, but insert() failed.";
        return false;
      }
    }

    if (measurementArea == nullptr) {
      if (!insert(new MeasurementArea())) {
        qCCritical(AreaDataLog) << "initialize(): measurementArea == nullptr, "
                                   "but insert() failed.";
        return false;
      }
    }

    // Fit safe area to bounding box.
    safeArea->clear();
    safeArea->appendVertex(bottomLeft);
    safeArea->appendVertex(
        QGeoCoordinate(topRight.latitude(), bottomLeft.longitude()));
    safeArea->appendVertex(topRight);
    safeArea->appendVertex(
        QGeoCoordinate(bottomLeft.latitude(), topRight.longitude()));

    // Put measurement area inside safeArea;
    measurementArea->clear();
    measurementArea->appendVertex(QGeoCoordinate(
        0.8 * bottomLeft.latitude() + 0.2 * topRight.latitude(),
        0.8 * bottomLeft.longitude() + 0.2 * topRight.longitude()));
    measurementArea->appendVertex(QGeoCoordinate(
        0.2 * bottomLeft.latitude() + 0.8 * topRight.latitude(),
        0.8 * bottomLeft.longitude() + 0.2 * topRight.longitude()));
    measurementArea->appendVertex(QGeoCoordinate(
        0.2 * bottomLeft.latitude() + 0.8 * topRight.latitude(),
        0.2 * bottomLeft.longitude() + 0.8 * topRight.longitude()));
    measurementArea->appendVertex(QGeoCoordinate(
        0.8 * bottomLeft.latitude() + 0.2 * topRight.latitude(),
        0.2 * bottomLeft.longitude() + 0.8 * topRight.longitude()));
    return true;
  } else {
    qCWarning(AreaDataLog)
        << "initialize(): bounding box invaldid (bottomLeft, topRight) "
        << bottomLeft << "," << topRight;
    return false;
  }
}

bool AreaData::initialized() {
  auto *measurementArea = getGeoArea<MeasurementArea *>(_areaList);
  auto *safeArea = getGeoArea<SafeArea *>(_areaList);
  return measurementArea != nullptr && safeArea != nullptr &&
         measurementArea->count() >= 3 && safeArea->count() >= 3;
}

bool AreaData::operator==(const AreaData &other) const {
  if (_areaList.count() == other._areaList.count()) {
    for (int i = 0; i < _areaList.count(); ++i) {
      if (_areaList[i] != other._areaList[i]) {
        return false;
      }
    }
    return true;
  } else {
    return false;
  }
}
bool AreaData::operator!=(const AreaData &other) const {
  return !(*this == other);
}

void AreaData::_setOrigin(const QGeoCoordinate &origin) {
  if (this->_origin != origin) {
    this->_origin = origin;
    emit originChanged();
  }
}

QGeoCoordinate AreaData::_newOrigin() {
  auto *measurementArea = getGeoArea<MeasurementArea *>(_areaList);
  auto *safeArea = getGeoArea<SafeArea *>(_areaList);
  if (measurementArea != nullptr && measurementArea->pathModel().count() > 0) {
    QGCQGeoCoordinate *ori =
        measurementArea->pathModel().value<QGCQGeoCoordinate *>(0);
    if (ori != nullptr && ori->coordinate().isValid()) {
      return ori->coordinate();
    }
  }

  if (safeArea != nullptr && safeArea->pathModel().count() > 0) {
    QGCQGeoCoordinate *ori =
        measurementArea->pathModel().value<QGCQGeoCoordinate *>(0);
    if (ori != nullptr && ori->coordinate().isValid()) {
      return ori->coordinate();
    }
  }

  return QGeoCoordinate();
}
