/* +------------------------------------------------------------------------+
   |                     Mobile Robot Programming Toolkit (MRPT)            |
   |                          https://www.mrpt.org/                         |
   |                                                                        |
   | Copyright (c) 2005-2024, Individual contributors, see AUTHORS file     |
   | See: https://www.mrpt.org/Authors - All rights reserved.               |
   | Released under BSD License. See: https://www.mrpt.org/License          |
   +------------------------------------------------------------------------+ */

#include "obs-precomp.h"  // Precompiled headers
//
#include <mrpt/core/round.h>
#include <mrpt/math/CMatrixF.h>
#include <mrpt/math/wrap2pi.h>
#include <mrpt/obs/CObservation2DRangeScan.h>
#include <mrpt/poses/CPosePDF.h>
#include <mrpt/serialization/CArchive.h>

#if MRPT_HAS_MATLAB
#include <mexplus.h>
#endif

using namespace std;
using namespace mrpt::obs;
using namespace mrpt::poses;
using namespace mrpt::math;

// This must be added to any CSerializable class implementation file.
IMPLEMENTS_SERIALIZABLE(CObservation2DRangeScan, CObservation, mrpt::obs)

uint8_t CObservation2DRangeScan::serializeGetVersion() const { return 7; }
void CObservation2DRangeScan::serializeTo(mrpt::serialization::CArchive& out) const
{
  // The data
  out << aperture << rightToLeft << maxRange << sensorPose;
  uint32_t N = m_scan.size();
  out << N;
  ASSERT_EQUAL_(m_validRange.size(), m_scan.size());
  if (N)
  {
    out.WriteBufferFixEndianness(&m_scan[0], N);
    out.WriteBuffer(&m_validRange[0], sizeof(m_validRange[0]) * N);
  }
  out << stdError;
  out << timestamp;
  out << beamAperture;

  out << sensorLabel;

  out << deltaPitch;

  out << hasIntensity();
  if (hasIntensity()) out.WriteBufferFixEndianness(&m_intensity[0], N);
}

void CObservation2DRangeScan::truncateByDistanceAndAngle(
    float min_distance, float max_angle, float min_height, float max_height, float h)
{
  // FILTER OUT INVALID POINTS!!
  CPose3D pose;
  unsigned int k = 0;
  const auto nPts = m_scan.size();

  auto itValid = m_validRange.begin();
  for (auto itScan = m_scan.begin(); itScan != m_scan.end(); itScan++, itValid++, k++)
  {
    const auto ang = std::abs(k * aperture / nPts - aperture * 0.5f);
    float x = (*itScan) * cos(ang);

    if (min_height != 0 || max_height != 0)
    {
      ASSERT_(max_height > min_height);
      if (*itScan < min_distance || ang > max_angle || x > h - min_height || x < h - max_height)
        *itValid = false;
    }  // end if
    else if (*itScan < min_distance || ang > max_angle)
      *itValid = false;
  }
}

void CObservation2DRangeScan::serializeFrom(mrpt::serialization::CArchive& in, uint8_t version)
{
  switch (version)
  {
    case 0:
    case 1:
    case 2:
    case 3:
    {
      CMatrixF covSensorPose;
      in >> aperture >> rightToLeft >> maxRange >> sensorPose >> covSensorPose;
      uint32_t N, i;

      in >> N;

      this->resizeScan(N);
      if (N) in.ReadBufferFixEndianness(&m_scan[0], N);

      if (version >= 1)
      {  // Load validRange:
        if (N) in.ReadBuffer(&m_validRange[0], sizeof(m_validRange[0]) * N);
      }
      else
      {
        // validRange: Default values: If distance is not maxRange
        for (i = 0; i < N; i++) m_validRange[i] = m_scan[i] < maxRange;
      }

      if (version >= 2)
      {
        in >> stdError;
      }
      else
      {
        stdError = 0.01f;
      }

      if (version >= 3)
      {
        in >> timestamp;
      }

      // Default values for newer versions:
      beamAperture = DEG2RAD(0.25f);

      deltaPitch = 0;
      sensorLabel = "";
    }
    break;

    case 4:
    case 5:
    case 6:
    case 7:
    {
      uint32_t N;

      CMatrixF covSensorPose;
      in >> aperture >> rightToLeft >> maxRange >> sensorPose;

      if (version < 6)  // covSensorPose was removed in version 6
        in >> covSensorPose;

      in >> N;
      this->resizeScan(N);
      if (N)
      {
        in.ReadBufferFixEndianness(&m_scan[0], N);
        in.ReadBuffer(&m_validRange[0], sizeof(m_validRange[0]) * N);
      }
      in >> stdError;
      in.ReadBufferFixEndianness(&timestamp, 1);
      in >> beamAperture;

      if (version >= 5)
      {
        in >> sensorLabel;
        in >> deltaPitch;
      }
      else
      {
        sensorLabel = "";
        deltaPitch = 0;
      }
      if (version >= 7)
      {
        bool hasIntensity;
        in >> hasIntensity;
        setScanHasIntensity(hasIntensity);
        if (hasIntensity && N)
        {
          in.ReadBufferFixEndianness(&m_intensity[0], N);
        }
      }
    }
    break;
    default:
      MRPT_THROW_UNKNOWN_SERIALIZATION_VERSION(version);
  };

  m_cachedMap.reset();
}

/*---------------------------------------------------------------
  Implements the writing to a mxArray for Matlab
 ---------------------------------------------------------------*/
#if MRPT_HAS_MATLAB
// Add to implement mexplus::from template specialization
IMPLEMENTS_MEXPLUS_FROM(mrpt::obs::CObservation2DRangeScan)
#endif

mxArray* CObservation2DRangeScan::writeToMatlab() const
{
#if MRPT_HAS_MATLAB
  const char* fields[] = {
      "class",  // Data common to any MRPT class
      "ts",
      "sensorLabel",  // Data common to any observation
      "scan",
      "validRange",
      "intensity"  // Received raw data
      "aperture",
      "rightToLeft",
      "maxRange",  // Scan plane geometry and properties
      "stdError",
      "beamAperture",
      "deltaPitch",  // Ray properties
      "pose",        // Sensor pose
      "map"};        // Points map
  mexplus::MxArray obs_struct(mexplus::MxArray::Struct(sizeof(fields) / sizeof(fields[0]), fields));

  obs_struct.set("class", this->GetRuntimeClass()->className);

  obs_struct.set("ts", mrpt::Clock::toDouble(timestamp));
  obs_struct.set("sensorLabel", this->sensorLabel);

  obs_struct.set("scan", this->m_scan);
  // TODO: "validRange should be a vector<bool> for Matlab instead of
  // vector<char>" ==> JLBC: It cannot be done like that since serializing
  // "bool" is not cross-platform safe, while "char" is...
  obs_struct.set("validRange", this->m_validRange);
  obs_struct.set("intensity", this->m_intensity);
  obs_struct.set("aperture", this->aperture);
  obs_struct.set("rightToLeft", this->rightToLeft);
  obs_struct.set("maxRange", this->maxRange);
  obs_struct.set("stdError", this->stdError);
  obs_struct.set("beamAperture", this->beamAperture);
  obs_struct.set("deltaPitch", this->deltaPitch);
  obs_struct.set("pose", this->sensorPose);
  // TODO: obs_struct.set("map", ...)
  return obs_struct.release();
#else
  THROW_EXCEPTION("MRPT built without MATLAB/Mex support");
#endif
}

/*---------------------------------------------------------------
            isPlanarScan
 ---------------------------------------------------------------*/
bool CObservation2DRangeScan::isPlanarScan(const double tolerance) const
{
  return sensorPose.isHorizontal(tolerance);
}

bool CObservation2DRangeScan::hasIntensity() const { return m_has_intensity; }
void CObservation2DRangeScan::setScanHasIntensity(bool setHasIntensityFlag)
{
  m_has_intensity = setHasIntensityFlag;
}

/*---------------------------------------------------------------
            filterByExclusionAreas
 ---------------------------------------------------------------*/
void CObservation2DRangeScan::filterByExclusionAreas(const TListExclusionAreasWithRanges& areas)
{
  if (areas.empty()) return;

  MRPT_START

  float Ang, dA;
  const size_t sizeRangeScan = m_scan.size();

  ASSERT_EQUAL_(m_scan.size(), m_validRange.size());

  if (!sizeRangeScan) return;

  if (rightToLeft)
  {
    Ang = -0.5f * aperture;
    dA = aperture / (sizeRangeScan - 1);
  }
  else
  {
    Ang = +0.5f * aperture;
    dA = -aperture / (sizeRangeScan - 1);
  }

  auto valid_it = m_validRange.begin();
  for (auto scan_it = m_scan.begin(); scan_it != m_scan.end(); scan_it++, valid_it++)
  {
    if (!*valid_it)
    {
      Ang += dA;
      continue;  // Already it's invalid
    }

    // Compute point in 2D, local to the laser center:
    const auto Lx = *scan_it * cos(Ang);
    const auto Ly = *scan_it * sin(Ang);
    Ang += dA;

    // To real 3D pose:
    double Gx, Gy, Gz;
    this->sensorPose.composePoint(Lx, Ly, 0, Gx, Gy, Gz);

    // Filter by X,Y:
    for (const auto& area : areas)
    {
      if (area.first.PointIntoPolygon(Gx, Gy) &&
          (Gz >= area.second.first && Gz <= area.second.second))
      {
        *valid_it = false;
        break;  // Go for next point
      }
    }  // for each area
  }    // for each point

  MRPT_END
}

float CObservation2DRangeScan::getScanAngle(size_t idx) const
{
  float Ang = -0.5f * aperture, dA = aperture / (m_scan.size() - 1);
  ASSERT_LT_(idx, m_scan.size());

  if (!rightToLeft)
  {
    Ang = -Ang;
    dA = -dA;
  }
  return Ang + dA * idx;
}

/*---------------------------------------------------------------
            filterByExclusionAreas
 ---------------------------------------------------------------*/
void CObservation2DRangeScan::filterByExclusionAreas(const std::vector<mrpt::math::CPolygon>& areas)
{
  if (areas.empty()) return;

  TListExclusionAreasWithRanges lst;
  for (const auto& area : areas)
  {
    TListExclusionAreasWithRanges::value_type dat;
    dat.first = area;
    dat.second.first = -std::numeric_limits<double>::max();
    dat.second.second = std::numeric_limits<double>::max();

    lst.push_back(dat);
  }
  filterByExclusionAreas(lst);
}

/*---------------------------------------------------------------
            filterByExclusionAngles
 ---------------------------------------------------------------*/
void CObservation2DRangeScan::filterByExclusionAngles(
    const std::vector<std::pair<double, double>>& angles)
{
  if (angles.empty()) return;

  MRPT_START

  double Ang, dA;
  const size_t sizeRangeScan = m_scan.size();

  ASSERT_EQUAL_(m_scan.size(), m_validRange.size());

  if (!sizeRangeScan) return;

  if (rightToLeft)
  {
    Ang = -0.5 * aperture;
    dA = aperture / (sizeRangeScan - 1);
  }
  else
  {
    Ang = +0.5 * aperture;
    dA = -aperture / (sizeRangeScan - 1);
  }

  // For each forbiden angle range:
  for (const auto& angle : angles)
  {
    int ap_idx_ini = mrpt::round(mrpt::math::wrapTo2Pi(angle.first - Ang) / dA);
    int ap_idx_end = mrpt::round(mrpt::math::wrapTo2Pi(angle.second - Ang) / dA);

    if (ap_idx_ini < 0) ap_idx_ini = 0;
    if (ap_idx_end < 0) ap_idx_end = 0;

    if (ap_idx_ini > static_cast<int>(sizeRangeScan)) ap_idx_ini = sizeRangeScan - 1;
    if (ap_idx_end > static_cast<int>(sizeRangeScan)) ap_idx_end = sizeRangeScan - 1;

    const size_t idx_ini = ap_idx_ini;
    const size_t idx_end = ap_idx_end;

    if (idx_end >= idx_ini)
    {
      for (size_t i = idx_ini; i <= idx_end; i++) m_validRange[i] = false;
    }
    else
    {
      for (size_t i = 0; i < idx_end; i++) m_validRange[i] = false;

      for (size_t i = idx_ini; i < sizeRangeScan; i++) m_validRange[i] = false;
    }
  }

  MRPT_END
}

namespace mrpt::obs
{
// Tricky way to call to a library that depends on us, a sort of "run-time"
// linking: ptr_internal_build_points_map_from_scan2D is a functor in
// "mrpt-obs", set by "mrpt-maps" at its startup.
using scan2pts_functor = void (*)(
    const mrpt::obs::CObservation2DRangeScan& obs,
    mrpt::maps::CMetricMap::Ptr& out_map,
    const void* insertOps);

extern scan2pts_functor ptr_internal_build_points_map_from_scan2D;
scan2pts_functor ptr_internal_build_points_map_from_scan2D = nullptr;

void internal_set_build_points_map_from_scan2D(scan2pts_functor fn)
{
  ptr_internal_build_points_map_from_scan2D = fn;
}
}  // namespace mrpt::obs

/*---------------------------------------------------------------
            internal_buildAuxPointsMap
  ---------------------------------------------------------------*/
void CObservation2DRangeScan::internal_buildAuxPointsMap(const void* options) const
{
  if (!ptr_internal_build_points_map_from_scan2D)
    throw std::runtime_error(
        "[CObservation2DRangeScan::buildAuxPointsMap] ERROR: This function "
        "needs linking against mrpt-maps.\n");

  (*ptr_internal_build_points_map_from_scan2D)(*this, m_cachedMap, options);
}

/** Fill out a T2DScanProperties structure with the parameters of this scan */
void CObservation2DRangeScan::getScanProperties(T2DScanProperties& p) const
{
  p.nRays = m_scan.size();
  p.aperture = this->aperture;
  p.rightToLeft = this->rightToLeft;
}

bool mrpt::obs::operator<(const T2DScanProperties& a, const T2DScanProperties& b)
{
  if (a.nRays != b.nRays) return a.nRays < b.nRays;
  if (a.aperture != b.aperture) return a.aperture < b.aperture;
  if (a.rightToLeft != b.rightToLeft) return a.rightToLeft;
  return false;
}

void CObservation2DRangeScan::getDescriptionAsText(std::ostream& o) const
{
  CObservation::getDescriptionAsText(o);
  o << "Homogeneous matrix for the sensor's 3D pose, relative to robot "
       "base:\n";
  o << sensorPose.getHomogeneousMatrixVal<CMatrixDouble44>() << "\n" << sensorPose << "\n";

  o << format("Samples direction: %s\n", (rightToLeft) ? "Right->Left" : "Left->Right");
  o << "Points in the scan: " << m_scan.size() << "\n";
  o << format("Estimated sensor 'sigma': %f\n", stdError);
  o << format("Increment in pitch during the scan: %f deg\n", RAD2DEG(deltaPitch));

  size_t i, inval = 0;
  for (i = 0; i < m_scan.size(); i++)
    if (!m_validRange[i]) inval++;
  o << format("Invalid points in the scan: %u\n", (unsigned)inval);

  o << format("Sensor maximum range: %.02f m\n", maxRange);
  o << format("Sensor field-of-view (\"aperture\"): %.01f deg\n", RAD2DEG(aperture));

  o << "Raw scan values: [";
  for (i = 0; i < m_scan.size(); i++) o << format("%.03f ", m_scan[i]);
  o << "]\n";

  o << "Raw valid-scan values: [";
  for (i = 0; i < m_validRange.size(); i++) o << format("%u ", m_validRange[i] ? 1 : 0);
  o << "]\n\n";

  if (hasIntensity())
  {
    o << "Raw intensity values: [";
    for (i = 0; i < m_intensity.size(); i++) o << format("%d ", m_intensity[i]);
    o << "]\n\n";
  }
}

const float& CObservation2DRangeScan::getScanRange(size_t i) const
{
  ASSERT_LT_(i, m_scan.size());
  return m_scan[i];
}
float& CObservation2DRangeScan::getScanRange(size_t i)
{
  ASSERT_LT_(i, m_scan.size());
  return m_scan[i];
}

void CObservation2DRangeScan::setScanRange(size_t i, const float val)
{
  ASSERT_LT_(i, m_scan.size());
  m_scan[i] = val;
}

const int32_t& CObservation2DRangeScan::getScanIntensity(size_t i) const
{
  ASSERT_LT_(i, m_intensity.size());
  return m_intensity[i];
}
int32_t& CObservation2DRangeScan::getScanIntensity(size_t i)
{
  ASSERT_LT_(i, m_intensity.size());
  return m_intensity[i];
}
void CObservation2DRangeScan::setScanIntensity(size_t i, const int val)
{
  ASSERT_LT_(i, m_intensity.size());
  m_intensity[i] = val;
}

bool CObservation2DRangeScan::getScanRangeValidity(size_t i) const
{
  ASSERT_LT_(i, m_validRange.size());
  return m_validRange[i] != 0;
}
void CObservation2DRangeScan::setScanRangeValidity(size_t i, const bool val)
{
  ASSERT_LT_(i, m_validRange.size());
  m_validRange[i] = val ? 1 : 0;
}

void CObservation2DRangeScan::resizeScan(size_t len)
{
  m_scan.resize(len);
  m_intensity.resize(len);
  m_validRange.resize(len);
}

void CObservation2DRangeScan::resizeScanAndAssign(
    size_t len, const float rangeVal, const bool rangeValidity, const int32_t rangeIntensity)
{
  m_scan.assign(len, rangeVal);
  m_validRange.assign(len, rangeValidity);
  m_intensity.assign(len, rangeIntensity);
}

size_t CObservation2DRangeScan::getScanSize() const { return m_scan.size(); }
void CObservation2DRangeScan::loadFromVectors(
    size_t nRays, const float* scanRanges, const char* scanValidity)
{
  ASSERT_(scanRanges);
  ASSERT_(scanValidity);
  resizeScan(nRays);
  for (size_t i = 0; i < nRays; i++)
  {
    m_scan[i] = scanRanges[i];
    m_validRange[i] = scanValidity[i];
  }
}

// See base class docs:
std::string CObservation2DRangeScan::exportTxtHeader() const
{
  std::string ret = "RANGES[i] ... VALID[i]";
  if (hasIntensity()) ret += " ... INTENSITY[i]";
  return ret;
}

std::string CObservation2DRangeScan::exportTxtDataRow() const
{
  std::stringstream o;
  for (size_t i = 0; i < m_scan.size(); i++) o << format("%.03f ", m_scan[i]);
  o << "    ";

  for (size_t i = 0; i < m_validRange.size(); i++) o << format("%u ", m_validRange[i] ? 1 : 0);
  o << "    ";

  if (hasIntensity())
  {
    for (size_t i = 0; i < m_intensity.size(); i++) o << format("%d ", m_intensity[i]);
  }
  return o.str();
}
