/*
 * FAI/IGC data format translation.
 *
 * Refer to Appendix 1 of
 * http://www.fai.org:81/gliding/gnss/tech_spec_gnss.asp for the
 * specification of the IGC data format.  This translation code was
 * written when the latest amendment list for the specification was AL6.
 *
 * Copyright (C) 2004 Chris Jones
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the
 * Free Software Foundation; either version 2 of the License, or (at your
 * option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */

#include <cmath>                     // for fabs, lround
#include <cstdio>                    // for sscanf, snprintf, fputs, printf, stdout, putchar, size_t
#include <cstdlib>                   // for labs, ldiv, ldiv_t, abs
#include <cstring>                   // for strcmp, strlen, strtok, strcat, strchr, strcpy, strncat
#include <ctime>                     // for gmtime, ctime
#include <iterator>                  // for reverse_iterator, operator==, prev, next

#include <QtCore/QByteArray>         // for QByteArray
#include <QtCore/QList>              // for QList<>::const_iterator
#include <QtCore/QStaticStringData>  // for QStaticStringData
#include <QtCore/QString>            // for QString, operator+, QStringLiteral
#include <QtCore/QtGlobal>           // for foreach, qPrintable

#include "defs.h"
#include "cet_util.h"                // for cet_convert_init
#include "gbfile.h"                  // for gbfprintf, gbfclose, gbfopen, gbfputs, gbfgetstr, gbfile
#include "src/core/datetime.h"       // for DateTime
#include "src/core/optional.h"       // for optional


static gbfile* file_in, *file_out;
static char manufacturer[4];
static const route_head* head;
static char* timeadj = nullptr;
static int lineno;

#define MYNAME "IGC"
#define MAXRECLEN 79		// Includes null terminator and CR/LF
#define MAXDESCLEN 1024
#define PRESTRKNAME "PRESALTTRK"
#define GNSSTRKNAME "GNSSALTTRK"
#define HDRMAGIC "IGCHDRS"
#define HDRDELIM "~"
#define DATEMAGIC "IGCDATE"

/*
 * IGC record types.
 * These appear as the first char in each record.
 */
enum igc_rec_type_t {
  rec_manuf_id = 'A',		// FR manufacturer and identification
  rec_fix = 'B',		// Fix
  rec_task = 'C',		// Task/declaration
  rec_diff_gps = 'D',		// Differential GPS
  rec_event = 'E',		// Event
  rec_constel = 'F',		// Constellation
  rec_security = 'G',		// Security
  rec_header = 'H',		// File header
  rec_fix_defn = 'I',		// List of extension data included at end of each fix (B) record
  rec_extn_defn = 'J',	// List of data included in each extension (K) record
  rec_extn_data = 'K',	// Extension data
  rec_log_book = 'L',		// Logbook/comments

  // M..Z are spare

  rec_none = 0,		// No record
  rec_bad = 1,		// Bad record
};

/*
 * See if two lat/lon pairs are approximately equal.
 * @param  lat1  The latitude of coordinate pair 1
 * @param  lon1  The longitude of coordinate pair 1
 * @param  lat2  The latitude of coordinate pair 2
 * @param  lon2  The longitude of coordinate pair 2
 * @retval  1  The coordinates are approximately equal
 * @retval  0  The coordinates are significantly different
 */
static unsigned char coords_match(double lat1, double lon1, double lat2, double lon2)
{
  return (fabs(lat1 - lat2) < 0.0001 && fabs(lon1 - lon2) < 0.0001) ? 1 : 0;
}

/*************************************************************************************************
 * Input file processing
 */

/*
 * Get an IGC record from the input file
 * @param  rec  Caller allocated storage for the record.  At least MAXRECLEN chars must be allocated.
 * @return the record type.  rec_none on EOF, rec_bad on fgets() or parse error.
 */
static igc_rec_type_t get_record(char** rec)
{
  char* c;
retry:
  *rec = c = gbfgetstr(file_in);
  if ((lineno++ == 0) && file_in->unicode) {
    cet_convert_init(CET_CHARSET_UTF8, 1);
  }
  if (c == nullptr) {
    return rec_none;
  }

  size_t len = strlen(c);

  /* Trackwiev writes (bogus) blank links between each record */
  if (len == 0) {
    goto retry;
  }

  if (len < 3 || c[0] < 'A' || c[0] > 'Z') {
    warning(MYNAME " bad input record: '%s'\n", c);
    return rec_bad;
  }
  return (igc_rec_type_t) c[0];
}

static void rd_init(const QString& fname)
{
  char* ibuf;

  file_in = gbfopen(fname, "r", MYNAME);
  lineno = 0;
  // File must begin with a manufacturer/ID record
  if (get_record(&ibuf) != rec_manuf_id || sscanf(ibuf, "A%3[A-Z]", manufacturer) != 1) {
    fatal(MYNAME ": %s is not an IGC file\n", qPrintable(fname));
  }
}

static void rd_deinit()
{
  gbfclose(file_in);
}

enum state_t { id, takeoff, start, turnpoint, finish, landing };
inline state_t& operator++(state_t& s) // prefix
{
  return s = static_cast<state_t>(s + 1);
}
inline state_t operator++(state_t& s, int) // postfix
{
  state_t ret(s);
  s = ++s;
  return ret;
}

/**
 * Handle pre- or post-flight task declarations.
 * A route is created for each set of waypoints in a task declaration.
 * @param rec A single task record
 */
static void igc_task_rec(const char* rec)
{
  static char flight_date[7];
  static unsigned int num_tp, tp_ct;
  static route_head* rte_head;
  static time_t creation;

  char task_num[5];
  char task_desc[MAXRECLEN];
  unsigned int lat_deg, lat_min, lat_frac;
  unsigned int lon_deg, lon_min, lon_frac;
  char lat_hemi[2], lon_hemi[2];
  char short_name[8];
  char tmp_str[MAXRECLEN];
  struct tm tm;
  static state_t state = id;

  // First task record identifies the task to follow
  if (id == state) {
    task_desc[0] = '\0';
    if (sscanf(rec, "C%2u%2u%2u%2u%2u%2u%6[0-9]%4c%2u%78[^\r]\r\n",
               &tm.tm_mday, &tm.tm_mon, &tm.tm_year,
               &tm.tm_hour, &tm.tm_min, &tm.tm_sec,
               flight_date, task_num, &num_tp, task_desc) < 9) {
      fatal(MYNAME ": task id (C) record parse error\n'%s'", rec);
    }
    task_num[4] = '\0';
    tm.tm_mon -= 1;
    if (tm.tm_year < 70) {
      tm.tm_year += 100;
    }
    tm.tm_isdst = 0;
    creation = mkgmtime(&tm);

    // Create a route to store the task data in.
    rte_head = new route_head;
    rte_head->rte_name = task_num;
    rte_head->rte_desc = QStringLiteral(DATEMAGIC) + flight_date + QStringLiteral(": ") + task_desc;
    route_add_head(rte_head);
    ++state;
    return;
  }
  // Get the waypoint
  tmp_str[0] = '\0';
  if (sscanf(rec, "C%2u%2u%3u%1[NS]%3u%2u%3u%1[WE]%78[^\r]\r\n",
             &lat_deg, &lat_min, &lat_frac, lat_hemi,
             &lon_deg, &lon_min, &lon_frac, lon_hemi, tmp_str) < 8) {
    fatal(MYNAME ": task waypoint (C) record parse error\n%s", rec);
  }

  auto* wpt = new Waypoint;
  wpt->latitude = ('N' == lat_hemi[0] ? 1 : -1) *
                  (lat_deg + (lat_min * 1000 + lat_frac) / 1000.0 / 60);

  wpt->longitude = ('E' == lon_hemi[0] ? 1 : -1) *
                   (lon_deg + (lon_min * 1000 + lon_frac) / 1000.0 / 60);

  wpt->SetCreationTime(creation);
  wpt->description = tmp_str;

  // Name the waypoint according to the order of the task record
  switch (state) {
  case takeoff:
    snprintf(short_name, 8, "TAKEOFF");
    ++state;
    break;

  case start:
    snprintf(short_name, 8, "START");
    tp_ct = 0;
    ++state;
    break;

  case turnpoint:
    if (++tp_ct == num_tp) {
      ++state;
    }
    snprintf(short_name, 8, "TURN%02u", tp_ct);
    break;

  case finish:
    snprintf(short_name, 8, "FINISH");
    ++state;
    break;

  case landing:
    snprintf(short_name, 8, "LANDING");
    state = id;
    break;

  default:
    fatal(MYNAME ": task id (C) record internal error\n%s", rec);
    break;
  }

  // Zero lat and lon indicates an unknown waypoint
  if (coords_match(wpt->latitude, wpt->longitude, 0.0, 0.0)) {
    delete wpt;
    return;
  }
  wpt->shortname = short_name;
  route_add_wpt(rte_head, wpt);
}

static void data_read()
{
  char* ibuf;
  unsigned int hours, mins, secs;
  unsigned int lat_deg, lat_min, lat_frac;
  unsigned int lon_deg, lon_min, lon_frac;
  char lat_hemi[2], lon_hemi[2];
  char validity;
  route_head* pres_head = nullptr;
  route_head* gnss_head = nullptr;
  int pres_alt, gnss_alt;
  char pres_valid = 0;
  char gnss_valid = 0;
  Waypoint* pres_wpt = nullptr;
  Waypoint* gnss_wpt = nullptr;
  time_t date = 0;
  time_t prev_tod = 0;
  time_t tod;
  struct tm tm;
  char tmp_str[20];
  char* hdr_data;
  size_t remain;
  char trk_desc[MAXDESCLEN + 1];

  strcpy(trk_desc, HDRMAGIC HDRDELIM);

  while (true) {
    igc_rec_type_t rec_type = get_record(&ibuf);
    switch (rec_type) {
    case rec_manuf_id:
      // Manufacturer/ID record already found in rd_init().
      warning(MYNAME ": duplicate manufacturer/ID record\n");
      break;

    case rec_header:
      // Get the header sub type
      if (sscanf(ibuf, "H%*1[FOPS]%3s", tmp_str) != 1) {
        fatal(MYNAME ": header (H) record parse error\n%s\n%s\n", ibuf, tmp_str);
      }
      // Optional long name of record sub type is followed by a
      // colon.  Actual header data follows that.
      if (nullptr == (hdr_data = strchr(ibuf, ':'))) {
        hdr_data = ibuf + 5;
      } else {
        hdr_data++;
      }

      // Date sub type
      if (strcmp(tmp_str, "DTE") == 0) {
        if (sscanf(hdr_data, "%2u%2u%2u", &tm.tm_mday, &tm.tm_mon, &tm.tm_year) != 3) {
          fatal(MYNAME ": date (H) record parse error\n'%s'\n", ibuf);
        }
        tm.tm_sec = tm.tm_min = tm.tm_hour = 0;
        tm.tm_mon -= 1;
        if (tm.tm_year < 70) {
          tm.tm_year += 100;
        }
        tm.tm_isdst = 0;
        date = mkgmtime(&tm);
      } else {
        // Store other header data in the track descriptions
        if (strlen(trk_desc) < MAXDESCLEN) {
          remain = MAXDESCLEN - strlen(trk_desc);
          strncat(trk_desc, ibuf, remain);
          remain = MAXDESCLEN - strlen(trk_desc);
          strncat(trk_desc, HDRDELIM, remain);
        }
      }
      break;

    case rec_fix:
      // Date must appear in file before the first fix record
      if (date < 1000000L) {
        fatal(MYNAME ": bad date %d\n", (int)date);
      }
      // Create a track for pressure altitude waypoints
      if (!pres_head) {
        pres_head = new route_head;
        pres_head->rte_name = PRESTRKNAME;
        pres_head->rte_desc = trk_desc;
        track_add_head(pres_head);
      }
      // Create a second track for GNSS altitude waypoints
      if (!gnss_head) {
        gnss_head = new route_head;
        gnss_head->rte_name = GNSSTRKNAME;
        gnss_head->rte_desc = trk_desc;
        track_add_head(gnss_head);
      }
      // Create a waypoint from the fix record data
      if (sscanf(ibuf,
                 "B%2u%2u%2u%2u%2u%3u%1[NS]%3u%2u%3u%1[WE]%c%5d%5d",
                 &hours, &mins, &secs, &lat_deg, &lat_min, &lat_frac,
                 lat_hemi, &lon_deg, &lon_min, &lon_frac, lon_hemi,
                 &validity, &pres_alt, &gnss_alt) != 14) {
        fatal(MYNAME ": fix (B) record parse error\n%s\n", ibuf);
      }
      pres_wpt = new Waypoint;

      pres_wpt->latitude = ('N' == lat_hemi[0] ? 1 : -1) *
                           (lat_deg + (lat_min * 1000 + lat_frac) / 1000.0 / 60);

      pres_wpt->longitude = ('E' == lon_hemi[0] ? 1 : -1) *
                            (lon_deg + (lon_min * 1000 + lon_frac) / 1000.0 / 60);

      // Increment date if we pass midnight UTC
      tod = (hours * 60 + mins) * 60 + secs;
      if (tod < prev_tod) {
        date += 24 * 60 * 60;
      }
      prev_tod = tod;
      pres_wpt->SetCreationTime(date + tod);

      // Add the waypoint to the pressure altitude track
      if (pres_alt) {
        pres_valid = 1;
        pres_wpt->altitude = pres_alt;
      } else {
        pres_wpt->altitude = unknown_alt;
      }
      track_add_wpt(pres_head, pres_wpt);

      // Add the same waypoint with GNSS altitude to the second
      // track
      gnss_wpt = new Waypoint(*pres_wpt);

      if (gnss_alt) {
        gnss_valid = 1;
        gnss_wpt->altitude = gnss_alt;
      } else {
        gnss_wpt->altitude = unknown_alt;
      }
      track_add_wpt(gnss_head, gnss_wpt);
      break;

    case rec_task:
      // Create a route for each pre-flight declaration
      igc_task_rec(ibuf);
      break;

    case rec_log_book:
      // Get the log book sub type
      if (sscanf(ibuf, "L%3s", tmp_str) != 1) {
        fatal(MYNAME ": log book (L) record parse error\n'%s'\n", ibuf);
      }

      if (strcmp(tmp_str, "PFC") == 0) {
        // Create a route for each post-flight declaration
        igc_task_rec(ibuf + 4);
        break;
      } else if (global_opts.debug_level) {
        if (strcmp(tmp_str, "OOI") == 0) {
          fputs(MYNAME ": Observer Input> ", stdout);
        } else if (strcmp(tmp_str, "PLT") == 0) {
          fputs(MYNAME ": Pilot Input> ", stdout);
        } else if (strcmp(tmp_str, manufacturer) == 0) {
          fputs(MYNAME ": Manufacturer Input> ", stdout);
        } else {
          fputs(MYNAME ": Anonymous Input> ", stdout);
          fputs(ibuf + 1, stdout);
          break;
        }
        fputs(ibuf + 4, stdout);
        putchar('\n');
      }
      break;

      // These record types are discarded
    case rec_diff_gps:
    case rec_event:
    case rec_constel:
    case rec_security:
    case rec_fix_defn:
    case rec_extn_defn:
    case rec_extn_data:
      break;

      // No more records
    case rec_none:

      // Include pressure altitude track only if it has useful
      // altitude data or if it is the only track available.
      if (pres_head && !pres_valid && gnss_head) {
        track_del_head(pres_head);
        pres_head = nullptr;
      }
      // Include GNSS altitude track only if it has useful altitude
      // data or if it is the only track available.
      if (gnss_head && !gnss_valid && pres_head) {
        track_del_head(gnss_head);
      }
      return;		// All done so bail

    default:
    case rec_bad:
      fatal(MYNAME ": failure reading file\n");
      break;
    }
  }
}

/*************************************************************************************************
 * Output file processing
 */

/*************************************************
 * Callbacks used to scan for specific track types
 */

static void detect_pres_track(const route_head* rh)
{
  if (rh->rte_name.startsWith(PRESTRKNAME)) {
    head = rh;
  }
}

static void detect_gnss_track(const route_head* rh)
{
  if (rh->rte_name.startsWith(GNSSTRKNAME)) {
    head = rh;
  }
}

static void detect_other_track(const route_head* rh)
{
  static int max_waypt_ct;

  if (!head) {
    max_waypt_ct = 0;
  }
  // Find other track with the most waypoints
  if (rh->rte_waypt_ct > max_waypt_ct &&
      (rh->rte_name.isEmpty() || 
       (!rh->rte_name.startsWith(PRESTRKNAME) &&
       !rh->rte_name.startsWith(GNSSTRKNAME)))) {
    head = rh;
    max_waypt_ct = rh->rte_waypt_ct;
  }
}

/*
 * Identify the pressure altitude and GNSS altitude tracks.
 * @param  pres_track  Set by the function to the pressure altitude track
 *                     head.  NULL if not found.
 * @param  gnss_track  Set by the function to the GNSS altitude track
 *                     head.  NULL if not found.
 */
static void get_tracks(const route_head** pres_track, const route_head** gnss_track)
{
  head = nullptr;
  track_disp_all(detect_pres_track, nullptr, nullptr);
  *pres_track = head;

  head = nullptr;
  track_disp_all(detect_gnss_track, nullptr, nullptr);
  *gnss_track = head;

  head = nullptr;
  track_disp_all(detect_other_track, nullptr, nullptr);

  if (!*pres_track && *gnss_track && head) {
    *pres_track = head;
  }

  if (!*gnss_track && head) {
    *gnss_track = head;
  }
}

/*************************************************
 * IGC string formatting functions
 */

static char* latlon2str(const Waypoint* wpt)
{
  static char str[18] = "";
  // We use lround here because it:
  // "Returns the integral value that is nearest to x, with halfway cases rounded away from zero."
  // The halfway rounding cases of *printf are not precisely defined, and can vary with implementation.
  // We don't really care which way the halfway cases go, but we want them to go that way consistently
  // across implementations.
  // We also try to use a minimum of floating point arithmetic to minimize accumulated fp math errors.
  long lat_milliminutes = lround(wpt->latitude * 60000.0);
  long lon_milliminutes = lround(wpt->longitude * 60000.0);
  char lat_hemi = lat_milliminutes < 0 ? 'S' : 'N';
  char lon_hemi = lon_milliminutes < 0 ? 'W' : 'E';
  ldiv_t lat_digits = ldiv(labs(lat_milliminutes), 60000L);
  ldiv_t lon_digits = ldiv(labs(lon_milliminutes), 60000L);

  if (snprintf(str, 18, "%02ld%05ld%c%03ld%05ld%c",
               lat_digits.quot, lat_digits.rem, lat_hemi, lon_digits.quot, lon_digits.rem, lon_hemi) != 17) {
    fatal(MYNAME ": Bad waypoint format '%s'\n", str);
  }
  return str;
}

static char* date2str(struct tm* dt)
{
  static char str[7] = "";

  if (snprintf(str, 7, "%02u%02u%02u", dt->tm_mday, dt->tm_mon + 1, dt->tm_year % 100) != 6) {
    fatal(MYNAME ": Bad date format '%s'\n", str);
  }
  return str;
}

static char* tod2str(struct tm* tod)
{
  static char str[7] = "";

  if (snprintf(str, 7, "%02u%02u%02u", tod->tm_hour, tod->tm_min, tod->tm_sec) != 6) {
    fatal(MYNAME ": Bad time of day format '%s'\n", str);
  }
  return str;
}

/*
 * Write header records
 */
static void wr_header()
{
  const route_head* pres_track;
  const route_head* track;
  struct tm* tm;
  time_t date;
  static const char dflt_str[] = "Unknown";
  const char* str = nullptr;
  Waypoint* wpt;

  get_tracks(&pres_track, &track);
  if (!track && pres_track) {
    track = pres_track;
  }
  // Date in header record is that of the first fix record
  date = !track ? current_time().toTime_t() :
         track->waypoint_list.front()->GetCreationTime().toTime_t();

  if (nullptr == (tm = gmtime(&date))) {
    fatal(MYNAME ": Bad track timestamp\n");
  }
  gbfprintf(file_out, "HFDTE%s\r\n", date2str(tm));

  // Other header data may have been stored in track description
  if (track && track->rte_desc.startsWith(HDRMAGIC)) {
    char *rd = xstrdup(track->rte_desc);
    for (str = strtok(rd + strlen(HDRMAGIC) + strlen(HDRDELIM), HDRDELIM);
         str; str = strtok(nullptr, HDRDELIM)) {
      gbfprintf(file_out, "%s\r\n", str);
    }
    xfree(rd);
    rd = nullptr;
  } else {
// FIXME: This almost certainly introduces a memory leak because str
// is a c string that's used for totally too many things.  Just let it
// leak for now. 2013-12-31 robertl
    if (nullptr != (wpt = find_waypt_by_name("PILOT")) && !wpt->description.isEmpty()) {
      xfree(str);
      str = xstrdup(CSTRc(wpt->description));
    } else {
      // IGC header info not found so synthesise it.
      // If a waypoint is supplied with a short name of "PILOT", use
      // its description as the pilot's name in the header.
      str = xstrdup(dflt_str);
    }
    gbfprintf(file_out, "HFPLTPILOT:%s\r\n", str);
    xfree(str);
  }
}

/*************************************************
 * Generation of IGC task declaration records
 */

static void wr_task_wpt_name(const Waypoint* wpt, const char* alt_name)
{
  gbfprintf(file_out, "C%s%s\r\n", latlon2str(wpt),
            !wpt->description.isEmpty() ? CSTR(wpt->description) : !wpt->shortname.isEmpty() ? CSTR(wpt->shortname) : alt_name);
}

static void wr_task_hdr(const route_head* rte)
{
  unsigned char have_takeoff = 0;
  char flight_date[7] = "000000";
  char task_desc[MAXRECLEN] = "";
  int num_tps = rte->rte_waypt_ct - 2;
  struct tm* tm;
  time_t rte_time;
  static unsigned int task_num = 1;

  if (num_tps < 0) {
    fatal(MYNAME ": Empty task route\n");
  }
  // See if the takeoff and landing waypoints are there or if we need to
  // generate them.
  const Waypoint* wpt = rte->waypoint_list.back();
  if (wpt->shortname.startsWith("LANDING")) {
    num_tps--;
  }
  wpt = rte->waypoint_list.front();
  if (wpt->shortname.startsWith("TAKEOFF")) {
    have_takeoff = 1;
    num_tps--;
  }
  if (num_tps < 0) {
    fatal(MYNAME ": Too few waypoints in task route\n");
  } else if (num_tps > 99) {
    fatal(MYNAME ": Too much waypoints (more than 99) in task route.\n");
  }
  // Gather data to write to the task identification (first) record
  rte_time = wpt->GetCreationTime().isValid() ? wpt->GetCreationTime().toTime_t() : current_time().toTime_t();
  if (nullptr == (tm = gmtime(&rte_time))) {
    fatal(MYNAME ": Bad task route timestamp\n");
  }

  if (!rte->rte_desc.isEmpty()) {
    // desc will be something like "IGCDATE160701: 500KTri" 
    sscanf(CSTR(rte->rte_desc), DATEMAGIC "%6[0-9]: %s", flight_date, task_desc);
  }

  gbfprintf(file_out, "C%s%s%s%04u%02u%s\r\n", date2str(tm),
            tod2str(tm), flight_date, task_num++, num_tps, task_desc);

  if (!have_takeoff) {
    // Generate the takeoff waypoint
    wr_task_wpt_name(wpt, "TAKEOFF");
  }
}

static void wr_task_wpt(const Waypoint* wpt)
{
  wr_task_wpt_name(wpt, "");
}

static void wr_task_tlr(const route_head* rte)
{
  // If the landing waypoint is not supplied we need to generate it.
  const Waypoint* wpt = rte->waypoint_list.back();
  QString sn = wpt->shortname;
//  if (!wpt->shortname || strncmp(wpt->shortname, "LANDIN", 6) != 0) {
  if (sn.isEmpty() || !sn.startsWith("LANDIN")) {
    wr_task_wpt_name(wpt, "LANDING");
  }
}

static void wr_tasks()
{
  route_disp_all(wr_task_hdr, wr_task_tlr, wr_task_wpt);
}

/*
 * Write a single fix record
 */
static void wr_fix_record(const Waypoint* wpt, int pres_alt, int gnss_alt)
{
  const time_t tt = wpt->GetCreationTime().toTime_t();
  struct tm* tm = gmtime(&tt);

  if (nullptr == tm) {
    fatal(MYNAME ": bad track timestamp\n");
  }

  if (unknown_alt == pres_alt) {
    pres_alt = 0;
  }
  if (unknown_alt == gnss_alt) {
    gnss_alt = 0;
  }
  gbfprintf(file_out, "B%02u%02u%02u%sA%05d%05d\r\n", tm->tm_hour,
            tm->tm_min, tm->tm_sec, latlon2str(wpt), pres_alt, gnss_alt);
}

/**
 * Attempt to align the pressure and GNSS tracks in time.
 * This is useful when trying to merge a track (lat/lon/time) recorded by a
 * GPS with a barograph (alt/time) recorded by a separate instrument with
 * independent clocks which are not closely synchronised.
 * @return The number of seconds to add to the GNSS track in order to align
 *         it with the pressure track.
 */
static int correlate_tracks(const route_head* pres_track, const route_head* gnss_track)
{
  double alt_diff;
  double speed;
  time_t pres_time;
  time_t gnss_time;
  int time_diff;

  // Deduce the landing time from the pressure altitude track based on
  // when we last descended to within 10m of the final track altitude.
  WaypointList::const_reverse_iterator wpt_rit = pres_track->waypoint_list.crbegin();
  double last_alt = (*wpt_rit)->altitude;
  do {
    ++wpt_rit;
    if (pres_track->waypoint_list.crend() == wpt_rit) {
      // No track left
      return 0;
    }
    alt_diff = last_alt - (*wpt_rit)->altitude;
    if (alt_diff > 10.0) {
      // Last part of track was ascending
      return 0;
    }
  } while (alt_diff > -10.0);
  pres_time = (*std::prev(wpt_rit))->GetCreationTime().toTime_t();
  if (global_opts.debug_level >= 1) {
    printf(MYNAME ": pressure landing time %s", ctime(&pres_time));
  }

  // Deduce the landing time from the GNSS altitude track based on
  // when the groundspeed last dropped below a certain level.
  wpt_rit = gnss_track->waypoint_list.crbegin();
  do {
    const Waypoint* wpt = *wpt_rit;
    ++wpt_rit;
    if (gnss_track->waypoint_list.crend() == wpt_rit) {
      // No track left
      return 0;
    }
    // Get a crude indication of groundspeed from the change in lat/lon
    time_diff = wpt->GetCreationTime().toTime_t() - (*wpt_rit)->GetCreationTime().toTime_t();
    speed = !time_diff ? 0 :
            (fabs(wpt->latitude - (*wpt_rit)->latitude) +
             fabs(wpt->longitude - (*wpt_rit)->longitude)) / time_diff;
    if (global_opts.debug_level >= 2) {
      printf(MYNAME ": speed=%f\n", speed);
    }
  } while (speed < 0.00003);
  gnss_time = (*std::prev(wpt_rit))->GetCreationTime().toTime_t();
  if (global_opts.debug_level >= 1) {
    printf(MYNAME ": gnss landing time %s", ctime(&gnss_time));
  }
  // Time adjustment is difference between the two estimated landing times
  if (15 * 60 < abs(time_diff = pres_time - gnss_time)) {
    warning(MYNAME ": excessive time adjustment %ds\n", time_diff);
  }
  return time_diff;
}

/**
 * Interpolate altitude from a track at a given time.
 * @param  track  The track containing altitude data.
 * @param  time   The time that we are interested in.
 * @return  The altitude interpolated from the track.
 */
static double interpolate_alt(const route_head* track, time_t time)
{
  static gpsbabel_optional::optional<WaypointList::const_iterator> prev_wpt;
  static gpsbabel_optional::optional<WaypointList::const_iterator> curr_wpt;
  int time_diff;

  // Start search at the beginning of the track
  if (!prev_wpt.has_value()) {
    prev_wpt = track->waypoint_list.cbegin();
    curr_wpt = track->waypoint_list.cbegin();
  }
  // Find the track points either side of the requested time
  while ((track->waypoint_list.cend() != curr_wpt.value()) &&
         ((*curr_wpt.value())->GetCreationTime().toTime_t() < time)) {
    prev_wpt = curr_wpt.value();
    curr_wpt = std::next(prev_wpt.value());
  }
  if (track->waypoint_list.cend() == curr_wpt.value()) {
    // Requested time later than all track points, we can't interpolate
    return unknown_alt;
  }

  if (track->waypoint_list.cbegin() == curr_wpt.value()) {
    if ((*curr_wpt.value())->GetCreationTime().toTime_t() == time) {
      // First point's creation time is an exact match so use it's altitude
      return (*curr_wpt.value())->altitude;
    } else {
      // Requested time is prior to any track points, we can't interpolate
      return unknown_alt;
    }
  }
  // Interpolate
  if (0 == (time_diff = (*curr_wpt.value())->GetCreationTime().toTime_t() - (*prev_wpt.value())->GetCreationTime().toTime_t())) {
    // Avoid divide by zero
    return (*curr_wpt.value())->altitude;
  }
  double alt_diff = (*curr_wpt.value())->altitude - (*prev_wpt.value())->altitude;
  return (*prev_wpt.value())->altitude + (alt_diff / time_diff) * (time - (*prev_wpt.value())->GetCreationTime().toTime_t());
}

/*
 * Pressure altitude and GNSS altitude may be provided in two separate
 * tracks.  This function attempts to merge them into one.
 */
static void wr_track()
{
  const route_head* pres_track;
  const route_head* gnss_track;
  int time_adj;

  // Find pressure altitude and GNSS altitude tracks
  get_tracks(&pres_track, &gnss_track);

  // If both found, attempt to merge them
  if (pres_track && gnss_track) {
    if (timeadj) {
      if (strcmp(timeadj, "auto") == 0) {
        time_adj = correlate_tracks(pres_track, gnss_track);
      } else if (sscanf(timeadj, "%d", &time_adj) != 1) {
        fatal(MYNAME ": bad timeadj argument '%s'\n", timeadj);
      }
    } else {
      time_adj = 0;
    }
    if (global_opts.debug_level >= 1) {
      printf(MYNAME ": adjusting time by %ds\n", time_adj);
    }
    // Iterate through waypoints in both tracks simultaneously
    foreach (const Waypoint* wpt, gnss_track->waypoint_list) {
      double pres_alt = interpolate_alt(pres_track, wpt->GetCreationTime().toTime_t() + time_adj);
      wr_fix_record(wpt, pres_alt, wpt->altitude);
    }
  } else {
    if (pres_track) {
      // Only the pressure altitude track was found so generate fix
      // records from it alone.
      foreach (const Waypoint* wpt, pres_track->waypoint_list) {
        wr_fix_record(wpt, wpt->altitude, unknown_alt);
      }
    } else if (gnss_track) {
      // Only the GNSS altitude track was found so generate fix
      // records from it alone.
      foreach (const Waypoint* wpt, gnss_track->waypoint_list) {
        wr_fix_record(wpt, unknown_alt, wpt->altitude);
      }
    } else {
      // No tracks found so nothing to do
      return;
    }
  }
}

static void wr_init(const QString& fname)
{
  file_out = gbfopen(fname, "wb", MYNAME);
}

static void wr_deinit()
{
  gbfclose(file_out);
}

static void data_write()
{
  gbfputs("AXXXZZZGPSBabel\r\n", file_out);
  wr_header();
  wr_tasks();
  wr_track();
  gbfprintf(file_out, "LXXXGenerated by GPSBabel Version %s\r\n", gpsbabel_version);
  gbfputs("GGPSBabelSecurityRecordGuaranteedToFailVALIChecks\r\n", file_out);
}


static QVector<arglist_t> igc_args = {
  {
    "timeadj", &timeadj,
    "(integer sec or 'auto') Barograph to GPS time diff",
    nullptr, ARGTYPE_STRING, ARG_NOMINMAX, nullptr
  },
};

ff_vecs_t igc_vecs = {
  ff_type_file,
  { ff_cap_none , (ff_cap)(ff_cap_read | ff_cap_write), (ff_cap)(ff_cap_read | ff_cap_write) },
  rd_init,
  wr_init,
  rd_deinit,
  wr_deinit,
  data_read,
  data_write,
  nullptr,
  &igc_args,
  CET_CHARSET_ASCII, 0	/* CET-REVIEW */
  , NULL_POS_OPS,
  nullptr
};
