/*
 * This file is part of the Owl Positioning System (OwlPS) project.
 * It is subject to the copyright notice and license terms in the
 * COPYRIGHT.t2t file found in the top-level directory of this
 * distribution and at
 *   https://code.lm7.fr/mcy/owlps/src/master/COPYRIGHT.t2t
 * No part of the OwlPS Project, including this file, may be copied,
 * modified, propagated, or distributed except according to the terms
 * contained in the COPYRIGHT.t2t file; the COPYRIGHT.t2t file must be
 * distributed along with this file, either separately or by replacing
 * this notice by the COPYRIGHT.t2t file's contents.
 */


#include "measurement.hh"
#include "posexcept.hh"
#include "configuration.hh"

#include <iostream>
#include <sstream>

using namespace std ;



/* *** Constructors *** */


/**
 * Note that values pointed by #cp are not deleted.
 */
Measurement::~Measurement()
{
  ss_list.clear() ;
}



/* *** Read accessors *** */


/**
 * @returns The SS of the packet number `pkt_id.`
 * @returns 0 if the packet ID does not exist in the SS list.
 */
ss_t Measurement::get_ss(const pkt_id_t pkt_id) const
{
  auto ss = ss_list.find(pkt_id) ;
  if (ss == ss_list.end())
    return 0 ;
  return ss->second ;
}



/* *** Write accessors *** */


/**
 * @param packet_id The sequence number of the packet in the request,
 * as sent by the mobile.
 * @param ss_dbm The signal strength to add to #ss_list (in dBm).
 */
void Measurement::
add_ss(const pkt_id_t packet_id, const ss_t ss_dbm)
{
  // Add the new value (in dBm) along with the packet identifier:
  pair<pkt_id_t, ss_t> packet(make_pair(packet_id, ss_dbm)) ;
  if (! ss_list.insert(packet).second)
    {
      cerr
          << "Cannot insert the packet (#" << packet_id << ", "
          << static_cast<int_fast16_t>(ss_dbm) << " dBm)\n" ;
      return ;
    }

  // Update the average & variance with the new value:
  update_average(ss_dbm) ;
}


/**
 * Add an SS list to the current Measurement's SS list.
 * If the new SS list contains a packet with the same ID, the old one
 * is overwritten.
 */
void Measurement::
add_ss_list(const map<pkt_id_t, ss_t> &_ss_list)
{
  // We cannot use insert() here because we want to overwrite the
  // previous values with the same ID, if any.
  for (auto i = _ss_list.begin() ; i != _ss_list.end() ; ++i)
    ss_list[i->first] = i->second ;

  recalculate_average() ;
}


/**
 * Merge consists of adding the SS values of `source` to #ss_list. It
 * is possible only if the #cp of the two Measurement are identical.
 * Since add_ss_list() is used, if `source` contains packets with IDs
 * that already exist, the old ones will be overwritten.
 *
 * @throw cannot_merge if the CP of the two Measurement are different.
 */
void Measurement::merge(const Measurement &source)
{
  if (cp != source.cp)
    throw cannot_merge(
      "error when trying to merge measurements, CPs are different") ;

  add_ss_list(source.ss_list) ;
}


/**
 * - #cp is not deleted, only initialised to nullptr.
 * - #ss_list is cleared.
 * - average and variance variables are reset to 0.
 */
void Measurement::clear()
{
  ss_list.clear() ;
  average_dbm = 0 ;
  average_mw = 0 ;
  variance_mw = 0 ;
  variance_dbm = 0 ;
  variance_mw_m2 = 0 ;
  variance_dbm_m2 = 0 ;
  variance_size = 0 ;
  cp = nullptr ;
}



/* *** Operations *** */


/**
 * @returns The similarity score. A small number means an important
 * similarity, a big number means an important dissimilarity. The
 * scale of this score depends on the similarity method used (option
 * positioning.ss-similarity); the similarity scores are therefore
 * not comparable across the various methods.
 */
float Measurement::similarity(const Measurement &source) const
{
  assert(! ss_list.empty()) ;
  assert(! source.ss_list.empty()) ;

  string algorithm(
    Configuration::string_value("positioning.ss-similarity")) ;

  if (algorithm == "mean")
    return ss_square_distance(source) ;

  if (algorithm == "interval")
    {
      unsigned int nb = nb_in_interval(source, get_std_deviation_mw()) ;
      if (nb != 0)
        return 1 / nb ;
      return 2 ;
    }
  /* Note: this score ranges from 0 (excluded) to 1 if at least one
   * packet was found within the interval. It is equals to 2 if no
   * packet was found in the interval. */

  if (algorithm == "interval2")
    {
      float std_dev = get_std_deviation_mw() ;
      unsigned int interval1 = nb_in_interval(source, 0.674 * std_dev) ;
      unsigned int interval2 = nb_in_interval(source, std_dev) ;
      /* Explanation: with a normal distribution, we normally have:
       *   - 68% of the values within the interval
       *     [mean-std.dev.;mean+std.dev.], and
       *   - 50% of the value within the interval
       *     [mean-.674*std.dev.;mean+.674*std.dev.] */

      float percent1 = interval1 * 100 / source.get_nb_ss() ;
      float percent2 = interval2 * 100 / source.get_nb_ss() ;

      return (percent1 - 50) * (percent1 - 50) +
             (percent2 - 68) * (percent2 - 68) ;
      /* Note: this distance (score) ranges from 0 (with
       * percent1==50 and percent2==68) to 7124 (with percent1
       * and percent2==0). */
    }

  throw bad_configuration(
    "Bad SS similarity algorithm name \""+ algorithm +"\"!") ;
}


unsigned int Measurement::
nb_in_interval(const Measurement &source, const float bound) const
{
  unsigned int nb_values = 0 ;

  for (auto ss = source.ss_list.begin() ; ss != source.ss_list.end() ;
       ++ss)
    if (PosUtil::is_in_interval(average_dbm, bound, ss->second))
      ++nb_values ;

  return nb_values ;
}


void Measurement::recalculate_average()
{
  average_dbm = 0 ;
  average_mw = 0 ;
  variance_mw = 0 ;
  variance_dbm = 0 ;
  variance_mw_m2 = 0 ;
  variance_dbm_m2 = 0 ;
  variance_size = 0 ;

  for (auto i = ss_list.begin() ; i != ss_list.end() ; ++i)
    update_average(i->second) ;
}


void Measurement::update_average(const ss_t ss_dbm)
{
  ++variance_size ;
  assert(variance_size <= ss_list.size()) ;

  // Convert the new SS in mW:
  float ss_mw = pow(10, static_cast<float>(ss_dbm) / 10.0) ;

  // Update the average:
  float delta_mw = ss_mw - average_mw ;
  average_mw += delta_mw / variance_size ;
  average_dbm = 10.0 * log10(average_mw) ;
  float delta_dbm = ss_dbm - average_dbm ;

  // Update the variance:
  variance_mw_m2 += delta_mw * (ss_mw - average_mw) ;
  variance_dbm_m2 += delta_dbm * (ss_dbm - average_dbm) ;
  if (variance_size > 1)
    {
      variance_mw = variance_mw_m2 / (variance_size - 1) ;
      variance_dbm = variance_dbm_m2 / (variance_size - 1) ;
    }
}



/* *** Operators *** */


Measurement& Measurement::operator=(const Measurement &m)
{
  if (this == &m)
    return *this ;

  cp = m.cp ;
  ss_list = m.ss_list ;
  average_dbm = m.average_dbm ;
  average_mw = m.average_mw ;
  variance_mw = m.variance_mw ;
  variance_dbm = m.variance_mw ;
  variance_mw_m2 = m.variance_mw_m2 ;
  variance_dbm_m2 = m.variance_mw_m2 ;
  variance_size = m.variance_size ;

  return *this ;
}


bool Measurement::operator==(const Measurement &m) const
{
  if (this == &m)
    return true ;

  return
    cp == m.cp &&
    ss_list == m.ss_list ;
}



const string Measurement::to_csv() const
{
  ostringstream csv_line ;

  if (ss_list.empty())
    return "" ;

  string mac_cp("") ;
  if (cp)
    mac_cp = cp->get_mac_addr() ;

  for (auto i = ss_list.begin() ; i != ss_list.end() ; ++i)
    {
      if (i != ss_list.begin())
        csv_line << ';' ;
      csv_line
          << mac_cp << ';'
          << i->first << ';'
          << static_cast<int_fast16_t>(i->second) ;
    }

  return csv_line.str() ;
}



ostream &operator<<(ostream &os, const Measurement &m)
{
  // MAC address
  os
      << "CP: " << (m.cp ? m.cp->get_mac_addr() : "Unknown_CP")
      << ": " ;

  // List of SS
  if (m.ss_list.empty())
    os << "No values" ;
  else
    for (auto i = m.ss_list.begin() ; i != m.ss_list.end() ; ++i)
      {
        if (i != m.ss_list.begin())
          os << ';' ;
        os << static_cast<int_fast16_t>(i->second)
           << '(' << i->first << ')' ;
      }
  os << " [AVG_dBm=" << m.average_dbm
     << ";VAR_dBm=" << m.variance_dbm
     << ";STD_dBm=" << m.get_std_deviation_dbm()
     << ";AVG_mW=" << m.average_mw
     << ";VAR_mW=" << m.variance_mw
     << ";STD_mW=" << m.get_std_deviation_mw()
     << "]" ;

  return os ;
}
