/*
 * 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 OwlPS UDP-to-HTTP, a program that
 * allows a client to retrieve results from OwlPS Positioner through
 * HTTP.
 */


#include "owlps-udp-to-http.h"

#include <signal.h>
#include <semaphore.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
#include <strings.h>
#include <string.h>
#include <netdb.h>

#include <assert.h>

/* Options */
// TODO: the size of this structure could be reduced by reordering its fields.
struct {
  bool verbose ;
  // 7 bytes alignment
  uint_fast16_t result_port ;
  uint_fast16_t http_port ;
  int nb_connections ;
  // 4 bytes alignment
} options = {
  true, // verbose
  OWL_DEFAULT_RESULT_PORT, // result_port
  DEFAULT_HTTP_PORT, // http_port
  DEFAULT_NB_CONNECTIONS // nb_connections
} ;

char *program_name = NULL ;

results_list *results = NULL ;
unsigned int nb_results = 0 ;
sem_t lock_results ;

int udp_sockfd = -1 ;
int tcp_sockfd = -1 ;

char *answer = NULL ; // Answer to send to the client
size_t answer_strlen = 0 ; // Total size of the answer string
size_t answer_buflen = 0 ; // Size of the answer allocated buffer


int main(int argc, char *argv[])
{
  struct sigaction action ; // Signal handler structure
  int ret = 0 ; // Program return value
  pthread_t tcp_server_thread ;

  owl_run = true ;

  program_name = argv[0] ;
  parse_command_line(argc, argv) ;

  /* Set up signal handlers */
  action.sa_flags = 0 ;
  sigemptyset(&action.sa_mask) ;
  action.sa_handler = owl_sigint_handler ;
  sigaction(SIGINT, &action, NULL) ;
  action.sa_handler = owl_sigterm_handler ;
  sigaction(SIGTERM, &action, NULL) ;

  /* Set up the semaphore */
  sem_init(&lock_results, 0, 1) ;

  /* Prepare the TCP socket */
  ret = init_tcp_socket() ;
  if (ret)
    goto exit ;

  /* Launch the TCP thread */
  ret = pthread_create(&tcp_server_thread, NULL,
                       &tcp_server, NULL) ;
  if (ret)
    {
      perror("Cannot create the TCP server thread") ;
      ret = OWL_ERR_THREAD_CREATE ;
      goto exit ;
    }

  /* Main loop */
  ret = receive_udp() ;

  /* Stop the TCP thread */
  // We must cancel the thread because it can be blocked on the
  // recv() call:
  if (pthread_cancel(tcp_server_thread))
    perror("Cannot cancel the TCP server thread") ;
  if (pthread_join(tcp_server_thread, NULL))
    perror("Cannot join the TCP server thread") ;

 exit:
  /* Close sockets */
  if (tcp_sockfd >= 0)
    if (close(tcp_sockfd))
      perror("Error closing the TCP socket") ;
  if (udp_sockfd >= 0)
    if (close(udp_sockfd))
      perror("Error closing the UDP socket") ;

  /* Last cleaning */
  free(answer) ;
  free_results_list() ;
  sem_destroy(&lock_results) ;

  if (options.verbose)
    fprintf(stderr, "%s: exiting.\n", program_name) ;

  return ret ;
}


void parse_command_line(const int argc, char *const *argv)
{
  parse_options(argc, argv) ;
  check_configuration() ;

  if (options.verbose)
    print_configuration() ;
}


void parse_options(const int argc, char *const *argv)
{
  int opt ;

  while ((opt = getopt(argc, argv, OPTIONS)) != -1)
    {
      switch (opt)
        {
        case 'h' :
          print_usage() ;
          exit(0) ;
        case 'l' :
          options.result_port = strtoul(optarg, NULL, 10) ;
          break ;
        case 't' :
          options.http_port = strtoul(optarg, NULL, 10) ;
          break ;
        case 'q' :
          options.verbose = false ;
          break ;
        case 'v' :
          options.verbose = true ;
          break ;
        case 'V' :
          print_version() ;
          exit(0) ;
        default :
          print_usage() ;
          exit(OWL_ERR_BAD_USAGE) ;
        }
    }
}


void check_configuration()
{
  if (options.result_port > 65535)
    {
      fprintf(stderr, "Warning! result_port too high: using default"
              " value (%d).\n", OWL_DEFAULT_RESULT_PORT) ;
      options.result_port = OWL_DEFAULT_RESULT_PORT ;
    }

  if (options.http_port > 65535)
    {
      fprintf(stderr, "Warning! http_port too high: using default"
              " value (%d).\n", DEFAULT_HTTP_PORT) ;
      options.http_port = DEFAULT_HTTP_PORT ;
    }
}


void print_configuration()
{
  fprintf(stderr, "Options:\n"
          "\tVerbose: %s\n"
          "\tResult port: %"PRIuFAST16"\n"
          "\tHTTP port: %"PRIuFAST16"\n"
          ,
          OWL_BOOL_TO_STRING(options.verbose),
          options.result_port,
          options.http_port
          ) ;
}



/*
 * Opens the UDP socket and reads from it the results sent by OwlPS
 * Positioning.
 * Returns a non-zero value in case of error.
 */
int receive_udp()
{
  owl_result *result ;

  /* Open the UDP socket */
  udp_sockfd = owl_create_udp_listening_socket(options.result_port) ;
  if (udp_sockfd < 0)
    return OWL_ERR_SOCKET_CREATE ;

  /* UDP read loop */
  while (owl_run)
    {
      result = owl_receive_position(udp_sockfd) ;
      if (result == NULL)
        return OWL_ERR_SOCKET_RECV ;
      owl_print_result(result) ;
      store_result(result) ;
      printf("--------------\n") ;
    }

  return 0 ;
}


/*
 * Adds a new owl_result to the results' list.
 */
void store_result(owl_result *const new_result)
{
  sem_wait(&lock_results) ;

  // The results' list does not exist yet
  if (! results)
    {
      results = malloc(sizeof(results_list)) ;
      if (! results)
        {
          perror("Cannot allocate memory") ;
          owl_run = false ;
          goto end ;
        }
      ++nb_results ;
      results->result = new_result ;
      results->next = NULL ;
    }

  // The results' list contains at least 1 element
  else
    {
      // Search for an existing result with the same mobile's MAC
      results_list *res = results ;
      while (res != NULL)
        {
          char *mac = res->result->mobile_mac_addr ;
          if (strncmp(mac, new_result->mobile_mac_addr,
                      OWL_ETHER_ADDR_STRLEN) == 0)
            break ;
          res = res->next ;
        }

      if (res == NULL) // Not found, adding an element
        {
          res = malloc(sizeof(results_list)) ;
          if (! res)
            {
              perror("Cannot allocate memory") ;
              owl_run = false ;
              goto end ;
            }
          ++nb_results ;
          res->next = results ;
          results = res ;
        }
      else // Found, clearing it
        owl_free_result(res->result) ;

      res->result = new_result ;
    }

 end:
  sem_post(&lock_results) ;
}


/*
 * Opens the TCP socket.
 * Returns a non-zero value in case of error.
 */
int init_tcp_socket()
{
  char http_port_str[6] ;
  struct addrinfo
    gai_hints,
    *gai_results = NULL,
    *gai_res = NULL ;
  int gai_ret ; // Return value of getaddrinfo()

  /* Get the server information */
  sprintf(http_port_str, "%"PRIuFAST16, options.http_port) ;
  memset(&gai_hints, 0, sizeof(struct addrinfo)) ;
  gai_hints.ai_family = AF_UNSPEC ; // IPv4 or IPv6
  gai_hints.ai_socktype = SOCK_STREAM ;
  gai_hints.ai_flags = AI_PASSIVE ;
  gai_ret = getaddrinfo(NULL, http_port_str, &gai_hints, &gai_results) ;
  if (gai_ret)
    {
      fprintf(stderr, "TCP socket creation failed: getaddrinfo(): %s\n",
              gai_strerror(gai_ret)) ;
      return OWL_ERR_SOCKET_CREATE ;
    }

  /* Create the TCP socket:
   * loop until both socket() and bind() succeed */
  for (gai_res = gai_results ; gai_res != NULL ;
       gai_res = gai_res->ai_next)
    {
      tcp_sockfd = socket(gai_res->ai_family, gai_res->ai_socktype,
                          gai_res->ai_protocol) ;
      if (tcp_sockfd == -1)
        continue ;

      if (! bind(tcp_sockfd, gai_res->ai_addr, gai_res->ai_addrlen))
        break ; // Success!

      close(tcp_sockfd) ;
      tcp_sockfd = -1 ;
    }

  if (gai_res == NULL)
    {
      fprintf(stderr,
              "TCP socket creation failed: socket() or bind().\n") ;
      return OWL_ERR_SOCKET_CREATE ;
    }

  return 0 ;
}


/*
 * Waits for requests from HTTP clients.
 */
void* tcp_server(void *const NULL_value)
{
  int newsockfd ;
  ssize_t nbytes ; // recv/send return value
  char client_message[CLIENT_MESSAGE_STRLEN] ;
  char client_request[CLIENT_REQUEST_STRLEN] ;
  int request_id ;

  listen(tcp_sockfd, options.nb_connections) ;

  // Prepare the answer, assuming there is only 1 full result (an error
  // message will also fit)
  answer_buflen = ANSWER_HDR_STRLEN + OWL_CSV_RESULT_STRLEN ;
  answer = malloc(answer_buflen) ;
  if (! answer_buflen)
    {
      perror("Cannot allocate memory") ;
      owl_run = false ;
      return NULL ;
    }
  strncpy(answer, ANSWER_HDR, ANSWER_HDR_STRLEN) ;

  while (owl_run)
    {
      newsockfd = accept(tcp_sockfd, NULL, NULL) ;
      if (newsockfd < 0)
        {
          perror("Error accepting a connection on the TCP socket") ;
          continue ;
        }

      nbytes =
        recv(newsockfd, client_message, CLIENT_MESSAGE_STRLEN, 0) ;
      if (nbytes < 0)
        {
          perror("Error reading from the TCP socket") ;
          close(newsockfd) ;
          continue ;
        }
      client_message[nbytes] = '\0' ;

      if (options.verbose)
        printf("Got a message from the client:\n"
               "\"%s\"\n", client_message) ;

      request_id =
        extract_request_from_message(client_request, client_message) ;
      prepare_answer(request_id) ;

      if (options.verbose)
        printf("Answer to send:\n\"%s\"\n", answer) ;

      /* Send the answer */
      nbytes = send(newsockfd, answer, answer_strlen, 0) ;
      if (nbytes < 0)
        perror("Error sending answer to the TCP socket") ;

      close(newsockfd) ;
    }

  pthread_exit(NULL_value) ;
}


/*
 * Reads a message and search for a request in it.
 * Returns the identifier of a request if found, or 0 if not found.
 * The text of the request is stored in 'client_request'.
 */
int
extract_request_from_message(char client_request[CLIENT_REQUEST_STRLEN],
                             const char *const client_message)
{
  char *token ;

  token = strchr(client_message, '=') ;
  if (! token)
    return 0 ;
  ++token ;
  token = strsep(&token, " ") ;

  if (strncmp(SIMPLE_RESULTS_REQUEST, token,
              strlen(SIMPLE_RESULTS_REQUEST)) == 0)
    {
      strncpy(client_request, SIMPLE_RESULTS_REQUEST,
              CLIENT_REQUEST_STRLEN) ;
      return SIMPLE_RESULTS_ID ;
    }

  if (strncmp(RESULTS_REQUEST, token,
              strlen(RESULTS_REQUEST)) == 0)
    {
      strncpy(client_request, RESULTS_REQUEST,
              CLIENT_REQUEST_STRLEN) ;
      return RESULTS_ID ;
    }

  return 0 ; // No known request found
}


/*
 * Prepare the answer string to send to the TCP client.
 */
void prepare_answer(const int request_id)
{
  // Reset the answer's length:
  answer_strlen = ANSWER_HDR_STRLEN ;

  switch (request_id)
    {
    case RESULTS_ID:
      strncpy(answer + answer_strlen, RESULTS_ANSWER,
              strlen(RESULTS_ANSWER)) ;
      answer_strlen += strlen(RESULTS_ANSWER) ;

      sem_wait(&lock_results) ;

      if (! results)
        {
          char answer_end[] = ";NOK;NoResults" ;
          strncpy(answer + answer_strlen, answer_end,
                  strlen(answer_end)) ;
          answer_strlen += strlen(answer_end) ;
        }

      else
        {
          results_list *result ;
          char answer_begin[10] ;
          size_t answer_begin_len ;

          snprintf(answer_begin, 10, ";OK;%u", nb_results) ;
          answer_begin_len = strlen(answer_begin) ;
          strncpy(answer + answer_strlen, answer_begin,
                  answer_begin_len) ;
          answer_strlen += answer_begin_len ;

          realloc_answer(answer_strlen +
                         nb_results * OWL_CSV_RESULT_STRLEN) ;

          result = results ;
          while (result != NULL)
            {
              char result_str[OWL_CSV_RESULT_STRLEN] ;
              size_t result_len ;
              owl_result_to_csv(result_str, result->result) ;
              result_len = strlen(result_str) ;
              answer[answer_strlen++] = ';' ;
              assert(answer_strlen<answer_buflen) ;
              strncpy(answer + answer_strlen, result_str,
                      result_len) ;
              answer_strlen += result_len ;
              result = result->next ;
            }
        }

      sem_post(&lock_results) ;

      break ;

    case SIMPLE_RESULTS_ID:
      strncpy(answer + answer_strlen, SIMPLE_RESULTS_ANSWER,
              strlen(SIMPLE_RESULTS_ANSWER)) ;
      answer_strlen += strlen(SIMPLE_RESULTS_ANSWER) ;

      sem_wait(&lock_results) ;

      if (! results)
        {
          char answer_end[] = ";NOK;NoResults" ;
          strncpy(answer + answer_strlen, answer_end,
                  strlen(answer_end)) ;
          answer_strlen += strlen(answer_end) ;
        }

      else
        {
          results_list *result ;
          char answer_begin[10] ;
          size_t answer_begin_len ;

          snprintf(answer_begin, 10, ";OK;%u", nb_results) ;
          answer_begin_len = strlen(answer_begin) ;
          strncpy(answer + answer_strlen, answer_begin,
                  answer_begin_len) ;
          answer_strlen += answer_begin_len ;

          realloc_answer(answer_strlen +
                         nb_results * OWL_CSV_RESULT_SIMPLE_STRLEN) ;

          result = results ;
          while (result != NULL)
            {
              char result_str[OWL_CSV_RESULT_SIMPLE_STRLEN] ;
              size_t result_len ;
              owl_result_to_csv_simple(result_str, result->result) ;
              result_len = strlen(result_str) ;
              answer[answer_strlen++] = ';' ;
              strncpy(answer + answer_strlen, result_str,
                      result_len) ;
              answer_strlen += result_len ;
              result = result->next ;
            }
        }

      sem_post(&lock_results) ;

      break ;

    default:
      {
        char answer_end[] = "UnknownRequest;NOK" ;
        strncpy(answer + ANSWER_HDR_STRLEN, answer_end,
                strlen(answer_end)) ;
        answer_strlen += strlen(answer_end) ;
      }
    }

  answer[answer_strlen] = '\0' ;
}


/*
 * Realloc the answer buffer to the if needed: grows it if new_size is
 * greater than the current size, shrink it if the current size is
 * greater than the double of new_size.
 */
void realloc_answer(const size_t new_size)
{
  if (new_size > answer_buflen)
    answer_buflen = new_size ;
  else if (answer_buflen / 2 >= new_size)
    answer_buflen /= 2 ;
  else
    return ;

  answer = realloc(answer, answer_buflen) ;
  if (! answer)
    abort() ;
}


/*
 * Frees the memory allocated for the results' list.
 */
void free_results_list()
{
  results_list *tmp_res ;
  while (results != NULL)
    {
      owl_free_result(results->result) ;
      tmp_res = results ;
      results = results->next ;
      free(tmp_res) ;
    }
  nb_results = 0 ;
}



/*
 * Prints the usage message on the standard output.
 *
 * /!\ Don't forget to update the documentation when modifying something
 *     here!
 */
void print_usage()
{
  printf("Usage:\n"
         "\t%s"
         " [-v | -q]"
         " [-l result_port]"
         " [-t http_port]\n"

         "Options:\n"
         "\t-h\t\tPrint this help.\n"
         "\t-V\t\tPrint version information.\n"
         "\t-v\t\tTurn on verbose mode (default).\n"
         "\t-q\t\tDo not print informational messages.\n"
         "\t-l result_port\tPort on which the results are received"
         " (default: %d).\n"
         "\t-t http_port\tPort on which the HTTP server listens"
         " (default: %d).\n"
         ,
         program_name,
         OWL_DEFAULT_RESULT_PORT,
         DEFAULT_HTTP_PORT
         ) ;
}


void print_version()
{
  printf("This is OwlPS UDP-to-HTTP, part of the Owl Positioning System"
         " project.\n"
         "Version: %s.\n",
#ifdef OWLPS_VERSION
         OWLPS_VERSION
#else // OWLPS_VERSION
         "unknown version"
#endif // OWLPS_VERSION
         ) ;
}
