/*****************************************************************************
 *
 * This MobilityDB code is provided under The PostgreSQL License.
 * Copyright (c) 2016-2025, Université libre de Bruxelles and MobilityDB
 * contributors
 *
 * MobilityDB includes portions of PostGIS version 3 source code released
 * under the GNU General Public License (GPLv2 or later).
 * Copyright (c) 2001-2025, PostGIS contributors
 *
 * Permission to use, copy, modify, and distribute this software and its
 * documentation for any purpose, without fee, and without a written
 * agreement is hereby granted, provided that the above copyright notice and
 * this paragraph and the following two paragraphs appear in all copies.
 *
 * IN NO EVENT SHALL UNIVERSITE LIBRE DE BRUXELLES BE LIABLE TO ANY PARTY FOR
 * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING
 * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION,
 * EVEN IF UNIVERSITE LIBRE DE BRUXELLES HAS BEEN ADVISED OF THE POSSIBILITY
 * OF SUCH DAMAGE.
 *
 * UNIVERSITE LIBRE DE BRUXELLES SPECIFICALLY DISCLAIMS ANY WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
 * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON
 * AN "AS IS" BASIS, AND UNIVERSITE LIBRE DE BRUXELLES HAS NO OBLIGATIONS TO
 * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 *
 *****************************************************************************/

/**
 * @file
 * @brief A simple program that reads AIS data from a CSV file containing one
 * full day of observations provided by the Danish Maritime Authority in
 * https://web.ais.dk/aisdata/, constructs for each ship temporal values for
 * the trip and the SOG, and outputs for each ship the MMSI, the number
 * of records and instants used for contruct the temporal values, the distance
 * travelled and the time-weighted average of the SOG.
 *
 * Please notice that the `data` directory DOES NOT contain the input CSV file,
 * you must download it from the website above.
 *
 * Please read the assumptions made about the input CSV file in the file
 * `02_ais_read.c` in the same directory. The program uses a compilation
 * constraint that can be used to reduce the number of records in processed. 
 * Also, the program copes with minimal error correction, that is,
 * - It supposes that the observations have increasing timestamp values
 * - It verifies that the latitude, longitude, and SOG values read are in
 *   specified ranges, and
 * - It takes only the first observation when there are two or more
 *   observations for the same ship with equal timestamp values.
 *
 * The program can be build as follows
 * @code
 * gcc -Wall -g -I/usr/local/include -o ais_expand_skiplist ais_expand_skiplist.c -L/usr/local/lib -lmeos
 * @endcode
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h> /* For memset */
#include <time.h>
#include <meos.h>
#include <meos_geo.h>
/* The expandable functions are in the internal MEOS API */
#include <meos_internal.h>
#include <meos_internal_geo.h>

/*
 * You may fix the value MAX_NO_RECS to only process the given number of
 * records
 */
/* Maximum number of records read from the CSV file */
#define MAX_NO_RECS 20000000
/* Number of instants in a batch for printing a marker */
#define NO_RECS_BATCH 100000
/* Initial number of allocated instants for an input trip and SOG */
#define INITIAL_INSTS 64
/* Maximum length in characters of a record in the input CSV file */
#define MAX_LEN_LINE 1024
/* Maximum length in characters of a point in the input data */
#define MAX_LEN_POINT 64
/* Maximum length in characters of a timestamp in the input data */
#define MAX_LEN_TIMESTAMP 32
/* Maximum length in characters of all other strings in the input data */
#define MAX_LEN_STRING 64

typedef struct
{
  Timestamp T;
  long int MMSI;
  double Latitude;
  double Longitude;
  double SOG;
} AIS_record;

typedef struct
{
  long int MMSI;          /* Identifier of the trip */
  int no_records;         /* Number of input records for the trip */
  int no_trip_instants;   /* Number of input instants for the trip */
  int no_SOG_instants;    /* Number of input instants for the SOG */
  TSequence *trip;        /* Sequence accumulating the trip observations */
  TSequence *SOG;         /* Sequence accumulating the SOG observations */
} trip_record;

int
trip_comp_fn(void *left, void *right)
{
  trip_record *left1 = (trip_record *) left;
  trip_record *right1 = (trip_record *) right;
  return int64_cmp(left1->MMSI, right1->MMSI);
}

void *
trip_merge_fn(void *left, void *right)
{
  Temporal *seq = (Temporal *) left;
  TInstant *inst = (TInstant *) right;
  temporal_append_tinstant(seq, inst, LINEAR, 0, NULL, true);
  return left;
}

/**
 * @brief Specific free function for the skiplist that also frees the temporal
 * values in the record values
 */
void
rec_skiplist_free(SkipList *list)
{
  if (! list)
    return;
  if (list->extra)
    free(list->extra);
  if (list->freed)
    free(list->freed);
  if (list->elems)
  {
    /* Free the keys and values of the elements if they are not NULL */
    int cur = 0;
    while (cur != -1)
    {
      SkipListElem *elem = &list->elems[cur];
      if (elem->key)
        free(elem->key);
      if (elem->value)
      {
        trip_record *rec = (trip_record *) (elem->value);
        free(rec->trip);
        free(rec->SOG);
        free(elem->value);
      }
      cur = elem->next[0];
    }
    /* Free the element list */
    free(list->elems);
  }
  free(list);
  return;
}

/* Main program */
int main(void)
{
  /* Input buffer to read the CSV file */
  char line_buffer[MAX_LEN_LINE];
  /* Record storing one line read from of the CSV file*/
  AIS_record rec;
  /* Number of records read */
  int no_records = 0;
  /* Number of erroneous records */
  int no_err_records = 0;
  /* Iterator variables */
  int i;
  /* Exit value initialized to 1 (i.e., error) to quickly exit upon error */
  int exit_value = 1;

  /* Initialize MEOS */
  meos_initialize();
  meos_initialize_timezone("UTC");

  /* Get start time */
  clock_t t = clock();

  /* Initialize the skiplist for storing elements with no key and the value is
   * a trip_record containing the MMSI */
  SkipList *list = skiplist_make(0, sizeof(trip_record), &trip_comp_fn,
    &trip_merge_fn);

  /*
   * Open the input file
   * Substitute the full file path in the first argument of fopen
   */
  FILE *file = fopen("data/aisdk-2025-03-01.csv", "r");
  if (! file)
  {
    printf("Error opening input file\n");
    goto cleanup;
  }

  /* Read the first line of the file with the headers */
  fscanf(file, "%1023[^\n]\n", line_buffer);
  printf("Processing records\n");
  printf("  one '*' marker every %d records\n", NO_RECS_BATCH);
  /* Uncomment the next lines to display a marker each time and incomplete
   * record or an erroneous field has been read */
  // printf("  one '-' marker every incomplete or erroneous records\n");
  // printf("  one 'T' marker every record with duplicate timestamp and position\n");
  // printf("  one 'S' marker every record with duplicate timestamp and SGO\n");

  /* Continue reading the file */
  do
  {
    fscanf(file, "%1023[^\n]\n", line_buffer);
    if (ferror(file))
    {
      printf("\nError reading input file\n");
      fclose(file);
      goto cleanup;
    }

    no_records++;
    /* Print a marker every X records read */
    if (no_records % NO_RECS_BATCH == 0)
    {
      printf("*");
      fflush(stdout);
    }
    /* Break if maximum number of records read */
    if (no_records == MAX_NO_RECS)
      break;

    /* Initialize record to 0 */
    memset(&rec, 0, sizeof(rec));
    int field = 0;
    char *token = strtok(line_buffer, ",");
    bool has_t = false, has_mmsi = false, has_lat = false,
      has_long = false, has_sog = false;
    while (token)
    {
      if (strlen(token) != 0 && strcmp(token, "Unknown") != 0)
      {
        switch (field)
        {
          case 0:
            rec.T = timestamp_in(token, -1);
            if (rec.T != 0)
              has_t = true;
            break;
          case 2:
            rec.MMSI = strtoll(token, NULL, 0);
            /* The next line can be used to filter the records to a given MMSI */
            // if (rec.MMSI == -1961112608 )
            if (rec.MMSI != 0)
              has_mmsi = true;
            break;
          case 3:
            rec.Latitude = strtold(token, NULL);
            /* The next line ensures the validity of the latitude value */
            // if (rec.Latitude >= -90.0 && rec.Latitude <= 90.0)
            /* The next line ensures the validity of the latitude value given
             * that the input file reports observations around Denmark */
            if (rec.Latitude >= 40.18 && rec.Latitude <= 84.17)
              has_lat = true;
            break;
          case 4:
            rec.Longitude = strtold(token, NULL);
            /* The next line ensures the validity of the longitude value */
            // if (rec.Longitude >= -180.0 && rec.Longitude <= 180.0)
            /* The next line ensures the validity of the longitude value given
             * that the input file reports observations around Denmark */
            if (rec.Longitude >= -16.1 && rec.Longitude <= 32.88)
              has_long = true;
            break;
          case 7:
            rec.SOG = strtold(token, NULL);
            /* Speed over ground in 1/10 knot steps (0-102.2 knots)
             * See https://www.navcen.uscg.gov/ais-class-a-reports */
            if (rec.SOG >= 0 && rec.SOG <= 1022)
              has_sog = true;
            break;
          default:
            break;
        }
      }
      token = strtok(NULL, ",");
      field++;
      if (field > 7)
        break;
    }

    if (! has_t || ! has_mmsi || ! ( ( has_lat && has_long ) || has_sog) )
    {
      /* Uncomment the next lines to display a marker each time
       * an incomplete record or an erroneous field has been read */
      // printf("-");
      // fflush(stdout);
      no_err_records++;
      continue;
    }

    /* Find the place to store the new instant */
    trip_record newrec, *newrec_p;
    newrec_p = &newrec;
    memset(&newrec, 0, list->value_size);
    newrec.MMSI = rec.MMSI;
    int pos = skiplist_search(list, NULL, &newrec);
    /* If not found */
    if (pos < 0)
    {
      skiplist_splice(list, NULL, (void **) &newrec_p, 1, NULL, false,
        KEYVALUE);
      pos = skiplist_search(list, NULL, (void *)(&newrec));
    }
    SkipListElem *e = (SkipListElem *) &list->elems[pos];
    trip_record *rec1 = (trip_record *) e->value;
    rec1->MMSI = rec.MMSI;

    /*
     * Create the instants and store them in the arrays of the ship.
     * In the input file it is assumed that
     * - The coordinates are given in the WGS84 geographic coordinate system
     * - The timestamps are given in GMT time zone
     */
    TInstant *inst;
    TSequence *new_seq;
    const TInstant *last;
    /* Create an Trip instant from the record */
    if (has_lat && has_long)
    {
      GSERIALIZED *gs = geogpoint_make2d(4326, rec.Longitude, rec.Latitude);
      inst = tpointinst_make(gs, rec.T);
      free(gs);
      /* Ensure there is still space for storing the temporal point instant */
      if (! rec1->trip)
      {
        rec1->trip = tsequence_make_exp(&inst, 1, INITIAL_INSTS, true, true,
          LINEAR, false);
        if (! rec1->trip)
        {
          printf("\nMSSI: %ld, there is no more memory to expand the trip\n",
            rec1->MMSI);
          fclose(file);
          goto cleanup;
        }
        rec1->no_records++;
        rec1->no_trip_instants++;
      }
      /* Ignore the instant if has the same timestamp as the last one */
      last = TSEQUENCE_INST_N(rec1->trip, rec1->trip->count - 1);
      if (last->t == inst->t)
      {
        free(inst);
        /* Uncomment the next lines to display a marker each time a Trip
         * observation has the same timestamp as the previous one */
        // printf("T");
        // fflush(stdout);
      }
      else
      {
        new_seq = (TSequence *) tsequence_append_tinstant(rec1->trip, inst,
          0.0, NULL, true);
        /* Uncomment the next lines to display debug messages showing how the
         * the data structures are expanded */
        // if (rec1->trip && rec1->trip != new_seq)
        // {
          // printf("MMSI: %ld ", rec1->MMSI);
          // printf("Trip %d -> %d ", new_seq->maxcount / 2, new_seq->maxcount);
          // fflush(stdout);
        // }
        rec1->trip = new_seq;
        rec1->no_trip_instants++;
      }
    }
    /* Create an SOG instant from the record */
    if (has_sog)
    {
      inst = (TInstant *) tfloatinst_make(rec.SOG, rec.T);
      /* Ensure there is still space for storing the temporal float instant */
      if (rec1->SOG == NULL)
      {
        /* Uncomment the next lines to display debug messages showing how
         * the data structures are expanded */
        // printf("MMSI: %ld ", rec1->MMSI);
        // printf("Speed %d -> %d ", rec1->no_SOG_instants,
          // rec1->no_SOG_instants * 2);
        // fflush(stdout);
        rec1->SOG = tsequence_make_exp(&inst, 1, INITIAL_INSTS, true, true,
          LINEAR, false);
        if (rec1->SOG == NULL)
        {
          printf("\nMSSI: %ld, there is no more memory to expand the speed\n",
            rec1->MMSI);
          fclose(file);
          goto cleanup;
        }
      }
      /* Ignore the instant if has the same timestamp as the last one */
      last = TSEQUENCE_INST_N(rec1->SOG, rec1->SOG->count - 1);
      if (last->t == inst->t)
      {
        free(inst);
        // Uncomment the next lines to display a marker each time a SOG
        // observation has the same timestamp as the previous one
        // printf("S");
        // fflush(stdout);
      }
      else
      {
        new_seq = (TSequence *) tsequence_append_tinstant(rec1->SOG, inst,
          0.0, NULL, true);
        /* Uncomment the next lines to display debug messages showing how the
         * the data structures are expanded */
        // if (rec1->SOG && rec1->SOG != new_seq)
        // {
          // printf("MMSI: %ld ", rec1->MMSI);
          // printf("SOG %d -> %d ", new_seq->maxcount / 2, new_seq->maxcount);
          // fflush(stdout);
        // }
        rec1->SOG = new_seq;
        rec1->no_SOG_instants++;
      }
    }
  } while (! feof(file));

  /* Close the file */
  fclose(file);

  /* Get the values in the skiplist */
  void **values = skiplist_values(list);

  /* Construct the trips */
  printf("\n-----------------------------------------------------------------------------\n");
  printf("|   MMSI    |   #Rec  | #TrInst |  #SInst |     Distance    |     Speed     |\n");
  printf("-----------------------------------------------------------------------------\n");
  for (i = 0; i < list->length; i++)
  {
    trip_record *rec = (trip_record *) values[i];
    printf("| %.9ld |   %5d |   %5d |   %5d |", rec->MMSI,
      rec->no_records, rec->no_trip_instants, rec->no_SOG_instants);
    if (rec->trip)
    {
      printf(" %15.6lf |", tpointseq_length(rec->trip));
    }
    else
      printf("        ---      |");

    if (rec->SOG)
    {
      printf(" %13.6lf |\n", tnumberseq_twavg(rec->SOG));
    }
    else
      printf("       ---     |\n");
  }
  printf("-----------------------------------------------------------------------------\n");
  printf("\n%d records read.\n%d erroneous records ignored.\n", no_records,
    no_err_records);
  printf("%d trips read.\n", list->length);

  /* Calculate the elapsed time */
  t = clock() - t;
  double time_taken = ((double) t) / CLOCKS_PER_SEC;
  printf("The program took %f seconds to execute\n", time_taken);

  /* State that the program executed successfully */
  exit_value = 0;

/* Clean up */
cleanup:

 /* Free memory */
  rec_skiplist_free(list);

  /* Finalize MEOS */
  meos_finalize();

  return exit_value;
}
