/*
 * 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.
 *
 ***********************************************************************
 *
 * This is the main source file of libowlps-resultreader.
 */


#define _GNU_SOURCE // for strndup()


#include "owlps-resultreader.h"

#include <string.h>
#include <stdlib.h>
#include <unistd.h>

#include <assert.h>


#define CSV_DELIMITER ";"


/**
 * Receives a result from the socket of file descriptor `sockfd`, fills
 * an `owl_result` structure with the received information, and returns a
 * pointer to it.
 *
 * Note that the new owl_result is allocated with `malloc()` and must be
 * deleted using `owl_free_result()`.
 *
 * In case of error, a message is printed, except if `owl_run` (from
 * owlps.h) is `false`, and `NULL` is returned.
 *
 * @returns A pointer to a new `owl_result`.
 * @returns `NULL` in case of error.
 */
owl_result* owl_receive_position(const int sockfd)
{
  ssize_t nread ; // recvfrom return value
  char csv[OWL_CSV_RESULT_STRLEN] ; // Read string

  nread = recvfrom(sockfd, &csv, OWL_CSV_RESULT_STRLEN,
                   0, NULL, NULL) ;

  if (nread <= 0)
    {
      if (owl_run)
        perror("No request received from listener") ;
      return NULL ;
    }

  return owl_fill_result(csv) ;
}


/**
 * Splits the `csv` string received from OwlPS Positioner and stores
 * the fields in a new `owl_result`, and returns a pointer to it (or
 * `NULL` in case of error). Because this function uses strsep(), the
 * `csv` string is modified; it should be copied in the calling function
 * if preservation of the original value is desired.
 *
 * Note that the new `owl_result` is allocated with `malloc()` and must
 * be deleted using `owl_free_result()`.
 *
 * Handled CSV format:
 *
 *     Mobile_MAC;Request_type;Request_timestamp;Algorithm;X;Y;Z;Error;Area
 *
 * The Request_timestamp format is:
 *
 *     seconds.nanoseconds
 *
 * The *Area* field can be empty.
 *
 * @returns A pointer to a new `owl_result`.
 * @returns `NULL` in case of error.
 */
owl_result* owl_fill_result(char *csv)
{
  char *csv_field = NULL ;
  long longfield ; // Return value of owl_read_long_field()
  owl_result *result = NULL ;

  result = malloc(sizeof(*result)) ;
  if (! result)
    {
      perror("Cannot allocate memory") ;
      return NULL ;
    }
  memset(result, 0, sizeof(*result)) ;

  /* Mobile MAC address */
  csv_field = strsep(&csv, CSV_DELIMITER) ;
  if (! csv_field)
    {
      fprintf(stderr,
              "Error reading the mobile's MAC address from the CSV"
              " string (empty string?)!\n") ;
      goto error ;
    }
  if (strnlen(csv_field, OWL_ETHER_ADDR_STRLEN)
      != OWL_ETHER_ADDR_STRLEN - 1)
    {
      csv_field[OWL_ETHER_ADDR_STRLEN-1] = '\0' ;
      fprintf(stderr,
              "The string \"%s\" is not a valid MAC address!\n",
              csv_field) ;
      goto error ;
    }
  strncpy(result->mobile_mac_addr, csv_field, OWL_ETHER_ADDR_STRLEN) ;

  /* Request type */
  if (owl_read_long_field(&csv, CSV_DELIMITER, &longfield))
    {
      fprintf(stderr,
              "Error reading the request type from the CSV string!\n") ;
      goto error ;
    }
  if (! OWL_IS_REQUEST_TYPE(longfield))
    {
      fprintf(stderr,
              "The request type read (%ld) is invalid!\n", longfield) ;
      goto error ;
    }
  result->request_type = longfield ;

  /* Timestamp */
  // Seconds
  if (owl_read_long_field(&csv, ".", &longfield))
    {
      fprintf(stderr,
              "Error reading the timestamp (seconds) from the CSV"
              " string!\n") ;
      goto error ;
    }
  result->mobile_timestamp.tv_sec = longfield ;
  // Nanoseconds
  if (owl_read_long_field(&csv, CSV_DELIMITER, &longfield))
    {
      fprintf(stderr,
              "Error reading the timestamp (nanoseconds) from the CSV"
              " string!\n") ;
      goto error ;
    }
  result->mobile_timestamp.tv_nsec = longfield ;

  /* Algorithm results */
  do
    {
      owl_algorithm_result *current_algo =
        owl_fill_algorithm_result(&csv) ;
      if (current_algo == NULL)
        {
          fprintf(stderr, "Error reading the algorithm #%d!\n",
                  result->nb_results + 1) ;
          break ;
        }
      // Insert the current algorithm at the begining of the list
      current_algo->next = result->results ;
      result->results = current_algo ;
      ++result->nb_results ;
    }
  while (csv) ;

  return result ; // Success

 error:
  owl_free_result(result) ;
  return NULL ;
}


/**
 * Splits the `csv` string, stores the fields in a new
 * `owl_algorithm_result`, and returns a pointer to it (or `NULL`
 * in case of error). Because this function uses strsep(), the
 * `csv` string is modified; it should be copied in the calling function
 * if preservation of the original value is desired.
 *
 * Note that the new owl_algorithm_result is allocated with `malloc()`
 * and must be deleted using `owl_free_algorithm_result()`.
 *
 * Handled CSV format:
 *
 *     Algorithm;X;Y;Z;Error;Area
 *
 * The *Area* field can be empty.
 *
 * @returns A pointer to a new `owl_algorithm_result`.
 * @returns `NULL` in case of error.
 */
owl_algorithm_result* owl_fill_algorithm_result(char **csv)
{
  owl_algorithm_result *algo ;
  char *csv_field = NULL ;

  algo = malloc(sizeof(owl_algorithm_result)) ;
  if (! algo)
    {
      perror("Cannot allocate memory") ;
      return NULL ;
    }
  memset(algo, 0, sizeof(*algo)) ;

  // Algorithm name
  csv_field = strsep(csv, CSV_DELIMITER) ;
  if (! csv_field)
    {
      fprintf(stderr,
              "Error reading the algorithm name from the CSV string!\n") ;
      goto error ;
    }
  algo->algorithm =
    strndup(csv_field, OWL_ALGORITHM_STRLEN) ;

  // X coordinate
  if (owl_read_float_field(csv, &algo->x))
    {
      fprintf(stderr,
              "Error reading the X coordinate from the CSV string!\n") ;
      goto error ;
    }

  // Y coordinate
  if (owl_read_float_field(csv, &algo->y))
    {
      fprintf(stderr,
              "Error reading the Y coordinate from the CSV string!\n") ;
      goto error ;
    }

  // Z coordinate
  if (owl_read_float_field(csv, &algo->z))
    {
      fprintf(stderr,
              "Error reading the Z coordinate from the CSV string!\n") ;
      goto error ;
    }

  // Distance error
  if (owl_read_float_field(csv, &algo->error))
    {
      fprintf(stderr,
              "Error reading the distance error from the CSV string!\n") ;
      goto error ;
    }

  // Area name (optional)
  csv_field = strsep(csv, CSV_DELIMITER) ;
  if (csv_field)
    algo->area = strndup(csv_field, OWL_AREA_STRLEN) ;

  return algo ; // Success

 error:
  owl_free_algorithm_result(algo) ;
  return NULL ;
}


/**
 * Reads the first field from the `csv` string as a long integer and
 * stores it in `ret`. The fields of `csv` are separated by the symbols
 * in the `delim` string. `csv` will be modified to point to the next
 * field.
 * Upon error, the value pointed by `ret` may or may not be modified.
 *
 * @param[in,out] csv The CSV string.
 * @param[in] delim The characters delimiting the fields in the string.
 * @param[out] ret The integer value read from the field.
 *
 * @returns 0 in case of success.
 * @returns 1 if the field could not be read.
 * @returns 2 if the field could be read but did not contain a valid
 * value.
 */
int owl_read_long_field(char **const csv, const char *const delim,
                         long *const ret)
{
  char *endptr = NULL, *csv_field = NULL ;

  csv_field = strsep(csv, delim) ;
  if (! csv_field)
    return 1 ;

  *ret = strtol(csv_field, &endptr, 10) ;
  if (endptr == csv_field)
    return 2 ;

  return 0 ;
}


/**
 * Reads the first field from the `csv` string as a float and stores it
 * in `ret`. `csv` will be modified to point to the next field.
 * Upon error, the value pointed by `ret` may or may not be modified.
 *
 * @param[in,out] csv The CSV string.
 * @param[out] ret The float value read from the field.
 *
 * @returns 0 in case of success.
 * @returns 1 if the field could not be read.
 * @returns 2 if the field could be read but did not contain a valid
 * value.
 */
int owl_read_float_field(char **const csv, float *const ret)
{
  char *endptr = NULL, *csv_field = NULL ;

  csv_field = strsep(csv, CSV_DELIMITER) ;
  if (! csv_field)
    return 1 ;

  *ret = strtof(csv_field, &endptr) ;
  if (endptr == csv_field)
    return 2 ;

  return 0 ;
}


/**
 * Converts an `owl_result` back to a CSV string.
 * `dst` must be an allocated string of at least `OWL_CSV_RESULT_STRLEN`
 * characters.
 *
 * CSV format:
 *
 *     Mobile_MAC;Request_type;Request_timestamp;Nb_algo;Algo_1;…;Algo_n
 *
 * *Nb_algo* is the number of algorithms in the result.
 * The format of *Algo_i* is documented in the documentation of the
 * owl_algorithm_result_to_csv() function.
 */
void owl_result_to_csv(char dst[OWL_CSV_RESULT_STRLEN],
                       const owl_result *const src)
{
  size_t dst_len ;
  char timestamp_str[OWL_TIMESTAMP_STRLEN] ;

  assert(src) ;

  owl_timestamp_to_string(&src->mobile_timestamp, timestamp_str) ;
  snprintf(dst, OWL_CSV_RESULT_REQUEST_STRLEN,
           "%s;%"PRIu8";%s;%u",
           src->mobile_mac_addr,
           src->request_type,
           timestamp_str,
           src->nb_results) ;
  dst_len = strlen(dst) ;

  owl_algorithm_result *algo = src->results ;
  while (algo)
    {
      char algo_str[OWL_CSV_ALGORITHM_RESULT_STRLEN] ;
      owl_algorithm_result_to_csv(algo_str, algo) ;
      dst[dst_len++] = ';' ;
      strncpy(dst + dst_len, algo_str, OWL_CSV_ALGORITHM_RESULT_STRLEN) ;
      dst_len += strlen(algo_str) ;
      algo = algo->next ;
    }
}


/**
 * Converts an `owl_algorithm_result` back to a CSV string.
 * `dst` must be an allocated string of at least
 * `OWL_CSV_ALGORITHM_RESULT_STRLEN` characters.
 *
 * CSV format:
 *
 *     Algorithm_name;X;Y;Z;Error;Area_name
 *
 * *Error* is the distance from the true coordinates of the mobile, if
 * known; if unknown, *Error* is set to -1.
 * *Area_name* is the name of the area or room in which the mobile is
 * (may be empty).
 */
void
owl_algorithm_result_to_csv(char dst[OWL_CSV_ALGORITHM_RESULT_STRLEN],
                            const owl_algorithm_result *const src)
{
  assert(src) ;

  snprintf(dst, OWL_CSV_ALGORITHM_RESULT_STRLEN,
           "%s;%f;%f;%f;%f;%s",
           src->algorithm,
           src->x,
           src->y,
           src->z,
           src->error,
           src->area ? src->area : "") ;
}


/**
 * Converts an `owl_result` back to a CSV string, in a simplified format:
 * only the *first* algorithm in the result's algorithm list will be
 * included in the string.
 * `dst` must be an allocated string of at least
 * `OWL_CSV_RESULT_SIMPLE_STRLEN` characters.
 *
 * CSV format:
 *
 *     Mobile_MAC;First_algorithm
 *
 * *First_algorithm* is the first algorithm in `src->results`. Its format
 * is documented in the documentation of the
 * owl_algorithm_result_to_csv_simple() function.
 */
void owl_result_to_csv_simple(char dst[OWL_CSV_RESULT_SIMPLE_STRLEN],
                              const owl_result *const src)
{
  size_t dst_len ;
  char algo_str[OWL_CSV_ALGORITHM_RESULT_SIMPLE_STRLEN] ;

  assert(src) ;

  strncpy(dst, src->mobile_mac_addr, OWL_ETHER_ADDR_STRLEN) ;
  dst[OWL_ETHER_ADDR_STRLEN - 1] = ';' ;
  dst_len = OWL_ETHER_ADDR_STRLEN ;

  if (! src->results)
    return ;

  owl_algorithm_result_to_csv_simple(algo_str, src->results) ;
  strncpy(dst + dst_len, algo_str,
          OWL_CSV_ALGORITHM_RESULT_SIMPLE_STRLEN) ;
}


/**
 * Converts an `owl_algorithm_result` back to a CSV string, in a
 * simplified format.
 * `dst` must be an allocated string of at least
 * `OWL_CSV_ALGORITHM_RESULT_SIMPLE_STRLEN` characters.
 *
 * CSV format:
 *
 *     X;Y;Z;Area_name
 *
 * *Area_name* is the name of the area or room in which the mobile is
 * (may be empty).
 */
void owl_algorithm_result_to_csv_simple
(char dst[OWL_CSV_ALGORITHM_RESULT_SIMPLE_STRLEN],
 const owl_algorithm_result *const src)
{
  assert(src) ;

  snprintf(dst, OWL_CSV_ALGORITHM_RESULT_SIMPLE_STRLEN,
           "%f;%f;%f;%s",
           src->x,
           src->y,
           src->z,
           src->area ? src->area : "") ;
}


/**
 * Prints an `owl_result` to the given stream, using `fprintf()`.
 * `src` must not be `NULL`.
 */
void owl_fprint_result(FILE *const stream, const owl_result *const src)
{
  char timestamp_str[OWL_TIMESTAMP_STRLEN] ;

  assert(src) ;

  owl_timestamp_to_string(&src->mobile_timestamp, timestamp_str) ;
  fprintf(stream,
          "Mobile MAC: %s\n"
          "Request type: %"PRIu8"\n"
          "Mobile timestamp: %s\n"
          "%u results:\n"
          ,
          src->mobile_mac_addr,
          src->request_type,
          timestamp_str,
          src->nb_results) ;

  owl_algorithm_result *algo = src->results ;
  while (algo)
    {
      owl_fprint_algorithm_result(stream, algo) ;
      algo = algo->next ;
    }
}


/**
 * Prints an `owl_algorithm_result` to the given stream, using
 * `fprintf()`.
 * `src` must not be `NULL`.
 */
void owl_fprint_algorithm_result(FILE *const stream,
                                 const owl_algorithm_result *const src)
{
  assert(src) ;

  fprintf(stream,
          "* Algorithm: %s\n"
          "  X: %f\n"
          "  Y: %f\n"
          "  Z: %f\n"
          "  Error: %f\n"
          "  Area: %s\n"
          ,
          src->algorithm,
          src->x,
          src->y,
          src->z,
          src->error,
          src->area ? src->area : ""
          ) ;
}



/**
 * Frees the memory allocated for an `owl_result`. The `results` field
 * of `result` *must* be defined, either to `NULL` or to a valid memory
 * block allocated with `malloc()`.
 * Note that `result` will not be set to `NULL`.
 */
void owl_free_result(owl_result *const result)
{
  if (! result)
    return ;
  while (result->results)
    {
      owl_algorithm_result *algo = result->results ;
      result->results = algo->next ;
      owl_free_algorithm_result(algo) ;
    }
  free(result) ;
}


/**
 * Frees the memory allocated for a single `owl_algorithm_result` (*not*
 * recursively). The `algorithm` and `area` fields of `algo` *must* be
 * defined, either to `NULL` or to a valid memory block allocated with
 * `malloc()`.
 * Note that `algo` will not be set to `NULL`.
 */
void owl_free_algorithm_result(owl_algorithm_result *const algo)
{
  if (! algo)
    return ;
  free(algo->algorithm) ;
  free(algo->area) ;
  free(algo) ;
}
