/*
    Jeeps wrapper for Garmin serial protocol.

    Copyright (C) 2002, 2003, 2004, 2005, 2006  Robert Lipe, robertlipe+source@gpsbabel.org

    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 <cctype>               // for isalpha, toupper
#include <climits>              // for INT_MAX
#include <cmath>                // for atan2, floor, sqrt
#include <csetjmp>              // for setjmp
#include <cstdio>               // for fprintf, fflush, snprintf, sprintf
#include <cstdlib>              // for atoi, strtol
#include <cstring>              // for memcpy, strlen, strncpy, strchr
#include <ctime>                // for time_t

#include <QtCore/QByteArray>    // for QByteArray
#include <QtCore/QChar>         // for QChar
#include <QtCore/QString>       // for QString
#include <QtCore/Qt>            // for CaseInsensitive
#include <QtCore/QtGlobal>      // for qPrintable, foreach

#include "defs.h"
#include "cet_util.h"           // for cet_convert_init, cet_cs_vec_utf8
#include "format.h"             // for Format
#include "garmin_device_xml.h"  // for gdx_get_info, gdx_info, gdx_file, gdx_jmp_buf
#include "garmin_fs.h"          // for garmin_fs_garmin_after_read, garmin_fs_garmin_before_write
#include "garmin_tables.h"      // for gt_find_icon_number_from_desc, PCX, gt_find_desc_from_icon_number
#include "grtcirc.h"            // for DEG
#include "jeeps/gps.h"
#include "jeeps/gpsserial.h"
#include "src/core/datetime.h"  // for DateTime
#include "vecs.h"               // for Vecs

#define MYNAME "GARMIN"
static const char* portname;
static short_handle mkshort_handle;
static GPS_PWay* tx_waylist;
static GPS_PWay* tx_routelist;
static GPS_PWay* cur_tx_routelist_entry;
static GPS_PTrack* tx_tracklist;
static GPS_PTrack* cur_tx_tracklist_entry;
static int my_track_count = 0;
static char* getposn = nullptr;
static char* poweroff = nullptr;
static char* eraset = nullptr;
static char* resettime = nullptr;
static char* snlen = nullptr;
static char* snwhiteopt = nullptr;
static char* deficon = nullptr;
static char* category = nullptr;
static char* categorybitsopt = nullptr;
static char* baudopt = nullptr;
static int baud = 0;
static int categorybits;
static int receiver_must_upper = 1;

static Format* gpx_vec;

#define MILITANT_VALID_WAYPT_CHARS "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"

/* Technically, even this is a little loose as spaces arent allowed */
static const char* valid_waypt_chars = MILITANT_VALID_WAYPT_CHARS " ";

static
QVector<arglist_t> garmin_args = {
  {
    "snlen", &snlen, "Length of generated shortnames", nullptr,
    ARGTYPE_INT, "1", nullptr, nullptr
  },
  {
    "snwhite", &snwhiteopt, "Allow whitespace synth. shortnames",
    nullptr, ARGTYPE_BOOL, ARG_NOMINMAX, nullptr
  },
  { "deficon", &deficon, "Default icon name", nullptr, ARGTYPE_STRING, ARG_NOMINMAX, nullptr },
  {
    "get_posn", &getposn, "Return current position as a waypoint",
    nullptr, ARGTYPE_BOOL, ARG_NOMINMAX, nullptr
  },
  {
    "power_off", &poweroff, "Command unit to power itself down",
    nullptr, ARGTYPE_BOOL, ARG_NOMINMAX, nullptr
  },
  {
    "erase_t", &eraset, "Erase existing courses when writing new ones",
    nullptr, ARGTYPE_BOOL, ARG_NOMINMAX, nullptr
  },
  {
    "resettime", &resettime, "Sync GPS time to computer time",
    nullptr, ARGTYPE_BOOL, ARG_NOMINMAX, nullptr
  },
  {
    "category", &category, "Category number to use for written waypoints",
    nullptr, ARGTYPE_INT, "1", "16", nullptr
  },
  {
    "bitscategory", &categorybitsopt, "Bitmap of categories",
    nullptr, ARGTYPE_INT, "1", "65535", nullptr
  },
  {
    "baud", &baudopt, "Speed in bits per second of serial port (baud=9600)",
    nullptr, ARGTYPE_INT, ARG_NOMINMAX, nullptr },

};

static const char* d103_symbol_from_icon_number(unsigned int n);
static int d103_icon_number_from_symbol(const QString& s);


static void
rw_init(const QString& fname)
{
  receiver_must_upper = 1;
  const char* receiver_charset = nullptr;

  if (!mkshort_handle) {
    mkshort_handle = mkshort_new_handle();
  }

  if (global_opts.debug_level > 0)  {
    GPS_Enable_Warning();
    GPS_Enable_User();
  }
  if (global_opts.debug_level > 1)  {
    GPS_Enable_Diagnose();
  }
  GPS_Enable_Error();

  if (poweroff) {
    GPS_Command_Off(qPrintable(fname));
    return;
  }

  if (categorybitsopt) {
    categorybits = strtol(categorybitsopt, nullptr, 0);
  }

  if (baudopt) {
    baud = strtol(baudopt, nullptr, 0);
    switch (baud) {
      case 9600:
      case 19200:
      case 38400:
      case 57600:
      case 115200:
        break;
      default:
        fatal("Baud rate %d is not supported\n", baud);
    }
  }

  if (GPS_Init(qPrintable(fname)) < 0) {
    fatal(MYNAME ":Can't init %s\n", qPrintable(fname));
  }

  /*
   * THis is Gross. The B&W Vista sometimes sets its time decades into
   * the future with no way to reset it.  This apparently can "cure"
   * an affected unit.
   */
  if (resettime) {
    GPS_User("Issuing Time Reset...\n");
    GPS_Command_Send_Time(qPrintable(fname), current_time().toTime_t());
    GPS_User("done.\n");
  }

  portname = xstrdup(qPrintable(fname));

  if (baud && baud != DEFAULT_BAUD) {
    if (0 == GPS_Set_Baud_Rate(portname, baud)) {
      gps_baud_rate = baud;
    }
  }

  /*
   * Grope the unit we're talking to to set setshort_length to
   * 	20 for  the V,
   * 	10 for Street Pilot, (old) Rhino, 76
   * 	6 for the III, 12, emap, and etrex
   * Fortunately, getting this "wrong" only results in ugly names
   * when we're using the synthesize_shortname path.
   */
  int receiver_short_length = 10;

  switch (gps_waypt_type) {	/* waypoint type as defined by jeeps */
  case 0:
    fatal("Garmin unit %d does not support waypoint xfer.",
          gps_save_id);

    break;
  case 100:	/* The GARMIN GPS Interface Specification, */
  case 101:	/* says these waypoint types use an ident */
  case 102:	/* length of 6.  Waypoint types 106, 108 */
  case 103:	/* and 109 are all variable  length    */
  case 104:
  case 105:
  case 107:
  case 150:
  case 151:
  case 152:
  case 154:
  case 155:
    receiver_short_length = 6;
    break;
  case 106:	/* Waypoint types with variable ident length */
  case 108: 	/* Need GPSr id to know the actual length */
  case 109:
  case 110:
    switch (gps_save_id) {
    case 130:	/* Garmin Etrex (yellow) */
      receiver_short_length = 6;
      break;
    case 295: 	/* eTrex (yellow, fw v. 3.30) */
    case 696: 	/* eTrex HC */
    case 574: 	/* Geko 201 */
      receiver_short_length = 6;
      valid_waypt_chars =
        MILITANT_VALID_WAYPT_CHARS " +-";
      setshort_badchars(mkshort_handle, "\"$.,'!");
      break;

    case 155:	/* Garmin V */
    case 404:	/* SP2720 */
    case 520:	/* SP2820 */
      receiver_short_length = 20;
      break;
    case 382: 	/* C320 */
      receiver_short_length = 30;
      receiver_must_upper = 0;
      break;
    case 292: /* (60|76)C[S]x series */
    case 421: /* Vista|Legend Cx */
    case 694: /* Legend HCx */
    case 695: /* Vista HC */
    case 786: /* HC model */
    case 957: /* Legend HC */
      receiver_short_length = 14;
      snwhiteopt = xstrdup("1");
      receiver_must_upper = 0;
      /* This might be 8859-1 */
      receiver_charset = CET_CHARSET_MS_ANSI;
      break;
    case 291: /* GPSMAP 60CS, probably others */
    case 1095: /* GPS 72H */
      receiver_short_length = 10;
      valid_waypt_chars = MILITANT_VALID_WAYPT_CHARS " +-";
      setshort_badchars(mkshort_handle, "\"$.,'!");
      break;
    case 231: /* Quest */
    case 463: /* Quest 2 */
      receiver_must_upper = 0;
      receiver_short_length = 30;
      receiver_charset = CET_CHARSET_MS_ANSI;
      break;
    case 577: // Rino 530HCx Version 2.50
      receiver_must_upper = 0;
      receiver_short_length = 14;
      break;
    case 429: // Streetpilot i3
      receiver_must_upper = 0;
      receiver_charset = CET_CHARSET_MS_ANSI;
      receiver_short_length = 30;
      break;
    case 484: // Forerunner 305
      receiver_short_length = 8;
      break;
    case 260: /* GPSMap 296 */
    default:
      break;
    }
    break;
  default:
    break;

  }

  if (global_opts.debug_level > 0)  {
    fprintf(stderr, "Waypoint type: %d\n"
            "Chosen waypoint length %d\n",
            gps_waypt_type, receiver_short_length);
    if (gps_category_type) {
      fprintf(stderr, "Waypoint category type: %d\n",
              gps_category_type);
    }
  }

  /*
   * If the user provided a short_length, override the calculated value.
   */
  if (snlen) {
    setshort_length(mkshort_handle, atoi(snlen));
  } else {
    setshort_length(mkshort_handle, receiver_short_length);
  }

  if (snwhiteopt) {
    setshort_whitespace_ok(mkshort_handle, atoi(snwhiteopt));
  }

  /*
   * Until Garmins documents how to determine valid character space
   * for the new models, we just release this safety check manually.
   */
  if (receiver_must_upper) {
    setshort_goodchars(mkshort_handle, valid_waypt_chars);
  } else {
    setshort_badchars(mkshort_handle, "");
  }

  setshort_mustupper(mkshort_handle, receiver_must_upper);

  /*
   * This used to mean something when we used cet, but these days this
   * format either use implicit QString conversions (utf8) which is
   * likely a bug, or we have hard coded QString::fromLatin1 or CSTRc.
   * So all the above detection of receiver_charset is for naught.
   * But perhaps we will use an appropriate codec based on receiver_charset
   * someday.
   */
  if (receiver_charset) {
    cet_convert_init(receiver_charset, 1);
  }
}

static void
rd_init(const QString& fname)
{
  if (setjmp(gdx_jmp_buf)) {
    const gdx_info* gi = gdx_get_info();
    gpx_vec = Vecs::Instance().find_vec("gpx");
    gpx_vec->rd_init(gi->from_device.canon);
  } else {
    gpx_vec = nullptr;
    rw_init(fname);
  }
}

static void
rw_deinit()
{
  if (gps_baud_rate != DEFAULT_BAUD) {
    if (0 == GPS_Set_Baud_Rate(portname, DEFAULT_BAUD)) {
      gps_baud_rate = baud;
    }
  }
  
  if (mkshort_handle) {
    mkshort_del_handle(&mkshort_handle);
  }

  xfree(portname);
  portname = nullptr;
}

static int
waypt_read_cb(int total_ct, GPS_PWay* )
{
  static int i;

  if (global_opts.verbose_status) {
    i++;
    waypt_status_disp(total_ct, i);
  }
  return 0;
}

static void
waypt_read()
{
  int n;
  GPS_PWay* way = nullptr;

  if (getposn) {
    auto* wpt = new Waypoint;
    wpt->latitude = gps_save_lat;
    wpt->longitude = gps_save_lon;
    wpt->shortname = "Position";
    if (gps_save_time) {
      wpt->SetCreationTime(gps_save_time);
    }
    waypt_add(wpt);
    return;
  }

  if ((n = GPS_Command_Get_Waypoint(portname, &way, waypt_read_cb)) < 0) {
    fatal(MYNAME  ":Can't get waypoint from %s\n", portname);
  }

  for (int i = 0; i < n; i++) {
    auto* wpt_tmp = new Waypoint;

    wpt_tmp->shortname = QString::fromLatin1(way[i]->ident);
    wpt_tmp->description = QString::fromLatin1(way[i]->cmnt);
    wpt_tmp->shortname = wpt_tmp->shortname.simplified();
    wpt_tmp->description = wpt_tmp->description.simplified();
    wpt_tmp->longitude = way[i]->lon;
    wpt_tmp->latitude = way[i]->lat;
    if (gps_waypt_type == 103) {
      wpt_tmp->icon_descr = d103_symbol_from_icon_number(
                              way[i]->smbl);
    } else {
      wpt_tmp->icon_descr = gt_find_desc_from_icon_number(
                              way[i]->smbl, PCX);
    }
    /*
     * If a unit doesn't store altitude info (i.e. a D103)
     * gpsmem will default the alt to INT_MAX.   Other units
     * (I can't recall if it was the V (D109) hor the Vista (D108)
     * return INT_MAX+1, contrary to the Garmin protocol doc which
     * says they should report 1.0e25.   So we'll try to trap
     * all the cases here.     Yes, libjeeps should probably
     * do this and not us...
     */
    if ((way[i]->alt == (float)(1U<<31)) ||
        (way[i]->alt == INT_MAX) ||
        (way[i]->alt >= (float) 1.0e20)
       ) {
      wpt_tmp->altitude = unknown_alt;
    } else {
      wpt_tmp->altitude = way[i]->alt;
    }
    if (way[i]->time_populated) {
      wpt_tmp->SetCreationTime(way[i]->time);
    }
    garmin_fs_garmin_after_read(way[i], wpt_tmp, gps_waypt_type);
    waypt_add(wpt_tmp);
    GPS_Way_Del(&way[i]);
  }
  if (way) {
    xfree(way);
  }
}

static int lap_read_nop_cb(int, struct GPS_SWay**)
{
  return 0;
}

// returns 1 if the waypoint's start_time can be found
// in the laps array, 0 otherwise
static unsigned int checkWayPointIsAtSplit(Waypoint* wpt, GPS_PLap* laps, int nlaps)
{
  int result = 0;

  if ((laps != nullptr) && (nlaps > 0)) {
    for (int i = (nlaps-1); i >= 0; i--) {
      GPS_PLap lap = laps[i];
      time_t delta = lap->start_time - wpt->GetCreationTime().toTime_t();
      if ((delta >= -1) && (delta <= 1)) {
        result = 1;
        break;

        // as an optimization this will stop going through
        // the lap array when the negative delta gets too
        // big. It assumes that laps is sorted by time in
        // ascending order (which appears to be the case for
        // Forerunners. Don't know about other devices.
      } else if (delta < -1) {
        break;
      }
    }
  }

  return result;
}

static
void
track_read()
{
  GPS_PTrack* array;
  route_head* trk_head = nullptr;
  int trk_num = 0;
  const char* trk_name = "";
  GPS_PLap* laps = nullptr;
  int nlaps = 0;
  int next_is_new_trkseg = 0;

  if (gps_lap_type != -1) {
    nlaps = GPS_Command_Get_Lap(portname, &laps, &lap_read_nop_cb);
  }


  int32 ntracks = GPS_Command_Get_Track(portname, &array, waypt_read_cb);

  if (ntracks <= 0) {
    return;
  }

  for (int i = 0; i < ntracks; i++) {
    /*
     * This is probably always in slot zero, but the Garmin
     * serial spec says these can appear anywhere.  Toss them
     * out so we don't treat it as an extraneous trackpoint.
     */
    if (array[i]->ishdr) {
      trk_name = array[i]->trk_ident;
      if (!trk_name) {
        trk_name = "";
      }
    }

    if (trk_head == nullptr || array[i]->ishdr) {
      trk_head = new route_head;
      trk_head->rte_num = trk_num;
      trk_head->rte_name = QString::fromLatin1(trk_name);
      trk_num++;
      track_add_head(trk_head);
    }

    /* Need to do this here because fitness devices set tnew
     * on a trackpoint without lat/lon.
     */
    if (array[i]->tnew) {
      next_is_new_trkseg = 1;
    }

    if (array[i]->no_latlon || array[i]->ishdr) {
      continue;
    }
    auto* wpt = new Waypoint;

    wpt->longitude = array[i]->lon;
    wpt->latitude = array[i]->lat;
    wpt->altitude = array[i]->alt;
    wpt->heartrate = array[i]->heartrate;
    wpt->cadence = array[i]->cadence;
    wpt->shortname = array[i]->trk_ident;
    wpt->SetCreationTime(array[i]->Time);
    wpt->wpt_flags.is_split = checkWayPointIsAtSplit(wpt, laps,
                              nlaps);
    wpt->wpt_flags.new_trkseg = next_is_new_trkseg;
    next_is_new_trkseg = 0;

    if (array[i]->dpth < 1.0e25f) {
      WAYPT_SET(wpt, depth, array[i]->dpth);
    }
    if (array[i]->temperature_populated) {
      WAYPT_SET(wpt, temperature, array[i]->temperature);
    }

    track_add_wpt(trk_head, wpt);
  }

  while (ntracks) {
    GPS_Track_Del(&array[--ntracks]);
  }
  xfree(array);
}

static
void
route_read()
{
  GPS_PWay* array;
  /* TODO: Fixes warning but is it right?
   * RJL:  No, the warning isn't right; GCC's flow analysis is broken.
   * still, it's good taste...
   */
  route_head* rte_head = nullptr;

  int32 nroutepts = GPS_Command_Get_Route(portname, &array);

//	fprintf(stderr, "Routes %d\n", (int) nroutepts);
#if 1
  for (int i = 0; i < nroutepts; i++) {
    if (array[i]->isrte) {
      char* csrc = nullptr;
      /* What a horrible API has libjeeps for making this
       * my problem.
       */
      switch (array[i]->rte_prot) {
      case 201:
        csrc = array[i]->rte_cmnt;
        break;
      case 202:
        csrc = array[i]->rte_ident;
        break;
      default:
        break;
      }
      rte_head = new route_head;
      route_add_head(rte_head);
      if (csrc) {
        rte_head->rte_name = QString::fromLatin1(csrc);
      }
    } else {
      if (array[i]->islink)  {
        continue;
      } else {
        auto* wpt_tmp = new Waypoint;
        wpt_tmp->latitude = array[i]->lat;
        wpt_tmp->longitude = array[i]->lon;
        wpt_tmp->shortname = array[i]->ident;
        route_add_wpt(rte_head, wpt_tmp);
      }
    }
  }
#else
  GPS_Fmt_Print_Route(array, nroutepts, stderr);
#endif

}

#if 0
static
void
lap_read_as_track(void)
{
  int32 ntracks;
  GPS_PLap* array;
  route_head* trk_head = NULL;
  int trk_num = 0;
  int index;
  int i;
  char tbuf[128];

  ntracks = GPS_Command_Get_Lap(portname, &array, waypt_read_cb);
  if (ntracks <= 0) {
    return;
  }
  for (i = 0; i < ntracks; i++) {
    Waypoint* wpt;
    if (array[i]->index == -1) {
      index=i;
    } else {
      index=array[i]->index;
      index=i;
    }

    if ((trk_head == NULL) || (i == 0) ||
        /* D906 - last track:index is the track index */
        (array[i]->index == -1 && array[i]->track_index != 255) ||
        /* D10xx - no real separator, use begin/end time to guess */
        (abs(array[i-1]->start_time + array[i]->total_time/100-array[i]->start_time) > 2)
       ) {
      static struct tm* stmp;
      stmp = gmtime(&array[i]->start_time);
      trk_head = new route_head;
      /*For D906, we would like to use the track_index in the last packet instead...*/
      trk_head->rte_num = ++trk_num;
      strftime(tbuf, 32, "%Y-%m-%dT%H:%M:%SZ", stmp);
      trk_head->rte_name = tbuf;
      track_add_head(trk_head);

      wpt = new Waypoint;

      wpt->longitude = array[i]->begin_lon;
      wpt->latitude = array[i]->begin_lat;
      wpt->heartrate = array[i]->avg_heart_rate;
      wpt->cadence = array[i]->avg_cadence;
      wpt->speed = array[i]->max_speed;
      wpt->creation_time = array[i]->start_time;
      wpt->microseconds = 0;

      sprintf(tbuf, "#%d-0", index);
      wpt->shortname = tbuf;
      sprintf(tbuf, "D:%f Cal:%d MS:%f AH:%d MH:%d AC:%d I:%d T:%d",
              array[i]->total_distance, array[i]->calories, array[i]->max_speed, array[i]->avg_heart_rate,
              array[i]->max_heart_rate, array[i]->avg_cadence, array[i]->intensity, array[i]->trigger_method);
      wpt->description = tbuf;
      track_add_wpt(trk_head, wpt);
    }
    /*Allow even if no correct location, no skip if invalid */
    /*		if (array[i]->no_latlon) {
    *			continue;
    *		}
    */
    wpt = new Waypoint;

    wpt->longitude = array[i]->end_lon;
    wpt->latitude = array[i]->end_lat;
    wpt->heartrate = array[i]->avg_heart_rate;
    wpt->cadence = array[i]->avg_cadence;
    wpt->speed = array[i]->max_speed;
    wpt->creation_time = array[i]->start_time + array[i]->total_time/100;
    wpt->microseconds = 10000*(array[i]->total_time % 100);
    /*Add fields with no mapping in the description */
    sprintf(tbuf, "#%d", index);
    wpt->shortname = tbuf;
    sprintf(tbuf, "D:%f Cal:%d MS:%f AH:%d MH:%d AC:%d I:%d T:%d (%f,%f)",
            array[i]->total_distance, array[i]->calories, array[i]->max_speed, array[i]->avg_heart_rate,
            array[i]->max_heart_rate, array[i]->avg_cadence, array[i]->intensity, array[i]->trigger_method,
            array[i]->begin_lon, array[i]->begin_lat);
    wpt->description = tbuf;

    track_add_wpt(trk_head, wpt);
  }
  while (ntracks) {
    GPS_Lap_Del(&array[--ntracks]);
  }
  xfree(array);
}
#endif

/*
 * Rather than propagate Garmin-specific data types outside of the Garmin
 * code, we convert the PVT (position/velocity/time) data from the receiver
 * to the data type we use throughout.   Yes, we do lose some data that way.
 */
static void
pvt2wpt(GPS_PPvt_Data pvt, Waypoint* wpt)
{
  wpt->altitude = pvt->alt;
  wpt->latitude = pvt->lat;
  wpt->longitude = pvt->lon;
  WAYPT_SET(wpt,course,1);
  WAYPT_SET(wpt,speed,1);

  wpt->course = 180 + DEG(atan2(-pvt->east, -pvt->north));

  /* velocity in m/s */
  WAYPT_SET(wpt,speed, sqrt(pvt->north*pvt->north + pvt->east*pvt->east));
  // wpt->vs = pvt->up;

  /*
   * The unit reports time in three fields:
   * 1) The # of days to most recent Sun. since  1989-12-31 midnight UTC.
   * 2) The number of seconds (fractions allowed) since that Sunday.
   * 3) The number of leap seconds that offset the current UTC and GPS
   *    reference clocks.
   */
  double wptime = 631065600.0 + pvt->wn_days * 86400.0  +
    pvt->tow
    - pvt->leap_scnds;
  double wptimes = floor(wptime);
  wpt->SetCreationTime(wptimes, 1000000.0 * (wptime - wptimes));

  /*
   * The Garmin spec fifteen different models that use a different
   * table for 'fix' without a really good way to tell if the model
   * we're talking to happens to be one of those...By inspection,
   * it looks like even though the models (Summit, Legend, etc.) may
   * be popular, it's older (2001 and earlier or so) versions that
   * are affected and I think there are relatively few readers of
   * the fix field anyway.   Time will tell if this is a good plan.
   */
  switch (pvt->fix) {
  case 0:
    wpt->fix = fix_unknown;
    break;
  case 1:
    wpt->fix = fix_none;
    break;
  case 2:
    wpt->fix = fix_2d;
    break;
  case 3:
    wpt->fix = fix_3d;
    break;
  case 4:
    wpt->fix = fix_dgps;
    break; /* 2D_diff */
  case 5:
    wpt->fix = fix_dgps;
    break; /* 3D_diff */
  default:
    /* undocumented type. */
    break;
  }
}

static gpsdevh* pvt_fd;

static void
pvt_init(const QString& fname)
{
  rw_init(fname);
  GPS_Command_Pvt_On(qPrintable(fname), &pvt_fd);
}

static Waypoint*
pvt_read(posn_status* posn_status)
{
  auto* wpt = new Waypoint;
  GPS_PPvt_Data pvt = GPS_Pvt_New();

  if (GPS_Command_Pvt_Get(&pvt_fd, &pvt)) {
    pvt2wpt(pvt, wpt);
    GPS_Pvt_Del(&pvt);

    wpt->shortname = "Position";

    if (gps_errno && posn_status) {
      posn_status->request_terminate = 1;
    }

    return wpt;
  }

  /*
   * If the caller has not given us a better way to return the
   * error, do it now.
   */
  if (gps_errno) {
    fatal(MYNAME ": Fatal error reading position.\n");
  }

  delete wpt;
  GPS_Pvt_Del(&pvt);

  return nullptr;
}

static void
data_read()
{
  if (gpx_vec) {
    gpx_vec->read();
    return;
  }

  if (poweroff) {
    return;
  }

  if (global_opts.masked_objective & WPTDATAMASK) {
    waypt_read();
  }
  if (global_opts.masked_objective & TRKDATAMASK) {
    track_read();
  }
  if (global_opts.masked_objective & RTEDATAMASK) {
    route_read();
  }
  if (!(global_opts.masked_objective &
        (WPTDATAMASK | TRKDATAMASK | RTEDATAMASK | POSNDATAMASK))) {
    fatal(MYNAME ": Nothing to do.\n");
  }
}

static GPS_PWay
sane_GPS_Way_New()
{
  GPS_PWay way = GPS_Way_New();
  if (!way) {
    fatal(MYNAME ":not enough memory\n");
  }

  /*
   *  Undo less than helpful defaults from Way_New.
   */
  way->rte_ident[0] = 0;
  way->rte_cmnt[0] = 0;
  way->rte_link_subclass[0] = 0;
  way->rte_link_ident[0] = 0;
  way->city[0] = 0;
  way->state[0] = 0;
  way->facility[0] = 0;
  way->addr[0] = 0;
  way->cross_road[0] = 0;
  way->dpth = 1.0e25f;
  way->wpt_class = 0;  // user waypoint by default.

  return way;
}

static int
waypt_write_cb(GPS_PWay*)
{
  static int i;
  int n = waypt_count();

  if (global_opts.verbose_status) {
    i++;
    waypt_status_disp(n, i);
  }
  return 0;
}

/*
 * If we're using smart names, try to put the cache info in the
 * description.
 */
static const char*
get_gc_info(const Waypoint* wpt)
{
  if (global_opts.smart_names) {
    if (wpt->gc_data->type == gt_virtual) {
      return  "V ";
    }
    if (wpt->gc_data->type == gt_unknown) {
      return  "? ";
    }
    if (wpt->gc_data->type == gt_multi) {
      return  "Mlt ";
    }
    if (wpt->gc_data->type == gt_earth) {
      return  "EC ";
    }
    if (wpt->gc_data->type == gt_event) {
      return  "Ev ";
    }
    if (wpt->gc_data->container == gc_micro) {
      return  "M ";
    }
    if (wpt->gc_data->container == gc_small) {
      return  "S ";
    }
  }
  return "";
}

static int
waypoint_prepare()
{
  int i;
  int n = waypt_count();
  extern WaypointList* global_waypoint_list;
  int icon;

  tx_waylist = (struct GPS_SWay**) xcalloc(n,sizeof(*tx_waylist));

  for (i = 0; i < n; i++) {
    tx_waylist[i] = sane_GPS_Way_New();
  }

  i = 0;

  // Iterate with waypt_disp_all?
  for (const Waypoint* wpt : qAsConst(*global_waypoint_list)) {
    char obuf[256];

    QString src;
    if (!wpt->description.isEmpty()) {
      src = wpt->description;
    }
    if (!wpt->notes.isEmpty()) {
      src = wpt->notes;
    }

    /*
     * mkshort will do collision detection and namespace
     * cleaning
     */
    char* ident = mkshort(mkshort_handle,
                           global_opts.synthesize_shortnames ? CSTRc(src) :
                             CSTRc(wpt->shortname), false);
    /* Should not be a strcpy as 'ident' isn't really a C string,
     * but rather a garmin "fixed length" buffer that's padded
     * to the end with spaces.  So this is NOT (strlen+1).
     */
    memcpy(tx_waylist[i]->ident, ident, strlen(ident));

    if (global_opts.synthesize_shortnames) {
      xfree(ident);
    }
    tx_waylist[i]->ident[sizeof(tx_waylist[i]->ident)-1] = 0;

    // If we were explicitly given a comment from GPX, use that.
    //  This logic really is horrible and needs to be untangled.
    if (!wpt->description.isEmpty() &&
        global_opts.smart_names && !wpt->gc_data->diff) {
      memcpy(tx_waylist[i]->cmnt, CSTRc(wpt->description), strlen(CSTRc(wpt->description)));
    } else {
      if (global_opts.smart_names &&
          wpt->gc_data->diff && wpt->gc_data->terr) {
        snprintf(obuf, sizeof(obuf), "%s%u/%u %s",
                 get_gc_info(wpt),
                 wpt->gc_data->diff, wpt->gc_data->terr,
                 CSTRc(src));
        memcpy(tx_waylist[i]->cmnt, obuf, strlen(obuf));
      } else  {
        memcpy(tx_waylist[i]->cmnt, CSTRc(src), strlen(CSTRc(src)));
      }
    }



    tx_waylist[i]->lon = wpt->longitude;
    tx_waylist[i]->lat = wpt->latitude;

    if (deficon) {
      icon = gt_find_icon_number_from_desc(deficon, PCX);
    } else {
      if (get_cache_icon(wpt)) {
        icon = gt_find_icon_number_from_desc(get_cache_icon(wpt), PCX);
      } else {
        icon = gt_find_icon_number_from_desc(wpt->icon_descr, PCX);
      }
    }

    /* For units that support tiny numbers of waypoints, just
     * overwrite that and go very literal.
     */
    if (gps_waypt_type == 103) {
      icon = d103_icon_number_from_symbol(wpt->icon_descr);
    }
    tx_waylist[i]->smbl = icon;
    if (wpt->altitude == unknown_alt) {
      tx_waylist[i]->alt_is_unknown = 1;
      tx_waylist[i]->alt = 0;
    } else {
      tx_waylist[i]->alt = wpt->altitude;
    }
    gpsbabel::DateTime t = wpt->GetCreationTime();
    if (t.isValid()) {
      tx_waylist[i]->time = t.toTime_t();
      tx_waylist[i]->time_populated = 1;
    }
    if (category) {
      tx_waylist[i]->category = 1 << (atoi(category) - 1);
    }
    if (categorybits) {
      tx_waylist[i]->category = categorybits;
    }
    garmin_fs_garmin_before_write(wpt, tx_waylist[i], gps_waypt_type);
    i++;
  }

  return n;
}

static void
waypoint_write()
{
  int32 ret;

  int n = waypoint_prepare();

  if ((ret = GPS_Command_Send_Waypoint(portname, tx_waylist, n, waypt_write_cb)) < 0) {
    fatal(MYNAME ":communication error sending waypoints..\n");
  }

  for (int i = 0; i < n; ++i) {
    GPS_Way_Del(&tx_waylist[i]);
  }
  if (global_opts.verbose_status) {
    fprintf(stdout, "\r\n");
    fflush(stdout);
  }
  xfree(tx_waylist);
}

static void
route_hdr_pr(const route_head* rte)
{
  (*cur_tx_routelist_entry)->rte_num = rte->rte_num;
  (*cur_tx_routelist_entry)->isrte = 1;
  if (!rte->rte_name.isEmpty()) {
    strncpy((*cur_tx_routelist_entry)->rte_ident, CSTRc(rte->rte_name),
            sizeof((*cur_tx_routelist_entry)->rte_ident) - 1);
    (*cur_tx_routelist_entry)->rte_ident[sizeof((*cur_tx_routelist_entry)->rte_ident) - 1] = 0;
  }
}

static void
route_waypt_pr(const Waypoint* wpt)
{
  GPS_PWay rte = *cur_tx_routelist_entry;

  /*
   * As stupid as this is, libjeeps seems to want an empty
   * waypoint between every link in a route that has nothing
   * but the 'islink' member set.   Rather than "fixing" libjeeps,
   * we just double them up (sigh) and do that here.
   */
  rte->islink = 1;
  rte->lon = wpt->longitude;
  rte->lat = wpt->latitude;
  cur_tx_routelist_entry++;
  rte = *cur_tx_routelist_entry;

  rte->lon = wpt->longitude;
  rte->lat = wpt->latitude;
  rte->smbl = gt_find_icon_number_from_desc(wpt->icon_descr, PCX);

  // map class so unit doesn't duplicate routepoints as a waypoint.
  rte->wpt_class = 0x80;

  if (wpt->altitude != unknown_alt) {
    rte->alt = wpt->altitude;
  } else {
    rte->alt_is_unknown = 1;
    rte->alt = 0;
  }

  char* d = rte->ident;
  for (auto idx : wpt->shortname) {
    int c = idx.toLatin1();
    if (receiver_must_upper && isalpha(c)) {
      c = toupper(c);
    }
    if (strchr(valid_waypt_chars, c)) {
      *d++ = c;
    }
  }

  rte->ident[sizeof(rte->ident)-1] = 0;
  if (wpt->description.isEmpty()) {
    rte->cmnt[0] = 0;
  } else {
    strncpy(rte->cmnt, CSTR(wpt->description), sizeof(rte->cmnt) - 1);
    rte->cmnt[sizeof(rte->cmnt)-1] = 0;
  }
  cur_tx_routelist_entry++;
}

static void
route_write()
{
  int n = 2 * route_waypt_count(); /* Doubled for the islink crap. */

  tx_routelist = (struct GPS_SWay**) xcalloc(n,sizeof(GPS_PWay));
  cur_tx_routelist_entry = tx_routelist;

  for (int i = 0; i < n; i++) {
    tx_routelist[i] = sane_GPS_Way_New();
  }

  route_disp_all(route_hdr_pr, nullptr, route_waypt_pr);
  GPS_Command_Send_Route(portname, tx_routelist, n);
}

static void
track_hdr_pr(const route_head* trk_head)
{
  (*cur_tx_tracklist_entry)->ishdr = true;
  if (!trk_head->rte_name.isEmpty()) {
    strncpy((*cur_tx_tracklist_entry)->trk_ident, CSTRc(trk_head->rte_name), sizeof((*cur_tx_tracklist_entry)->trk_ident) - 1);
    (*cur_tx_tracklist_entry)->trk_ident[sizeof((*cur_tx_tracklist_entry)->trk_ident)-1] = 0;
  } else {
    sprintf((*cur_tx_tracklist_entry)->trk_ident, "TRACK%02d", my_track_count);
  }
  cur_tx_tracklist_entry++;
  my_track_count++;
}

static void
track_waypt_pr(const Waypoint* wpt)
{
  (*cur_tx_tracklist_entry)->lat = wpt->latitude;
  (*cur_tx_tracklist_entry)->lon = wpt->longitude;
  (*cur_tx_tracklist_entry)->alt = (wpt->altitude != unknown_alt) ? wpt->altitude : 1e25;
  (*cur_tx_tracklist_entry)->Time = wpt->GetCreationTime().toTime_t();
  if (!wpt->shortname.isEmpty()) {
    strncpy((*cur_tx_tracklist_entry)->trk_ident, CSTRc(wpt->shortname), sizeof((*cur_tx_tracklist_entry)->trk_ident) - 1);
    (*cur_tx_tracklist_entry)->trk_ident[sizeof((*cur_tx_tracklist_entry)->trk_ident)-1] = 0;
  }
  (*cur_tx_tracklist_entry)->tnew = wpt->wpt_flags.new_trkseg;
  cur_tx_tracklist_entry++;
}

static int
track_prepare()
{
  int32 n = track_waypt_count() + track_count();

  tx_tracklist = (struct GPS_STrack**) xcalloc(n, sizeof(GPS_PTrack));
  cur_tx_tracklist_entry = tx_tracklist;
  for (int i = 0; i < n; i++) {
    tx_tracklist[i] = GPS_Track_New();
  }
  my_track_count = 0;
  track_disp_all(track_hdr_pr, nullptr, track_waypt_pr);

  GPS_Prepare_Track_For_Device(&tx_tracklist, &n);

  return n;
}

static void
track_write()
{
  int n = track_prepare();
  GPS_Command_Send_Track(portname, tx_tracklist, n, (eraset)? 1 : 0);

  for (int i = 0; i < n; i++) {
    GPS_Track_Del(&tx_tracklist[i]);
  }
  xfree(tx_tracklist);
}

static void
course_write()
{
  int i;

  int n_wpt = waypoint_prepare();
  int n_trk = track_prepare();

  GPS_Command_Send_Track_As_Course(portname, tx_tracklist, n_trk,
                                   tx_waylist, n_wpt, (eraset)? 1 : 0);

  for (i = 0; i < n_wpt; ++i) {
    GPS_Way_Del(&tx_waylist[i]);
  }
  xfree(tx_waylist);

  for (i = 0; i < n_trk; i++) {
    GPS_Track_Del(&tx_tracklist[i]);
  }
  xfree(tx_tracklist);
}

static void
data_write()
{
  if (poweroff) {
    return;
  }

  /* If we have both trackpoints and waypoints and the device
   * supports courses, combine them to a course. Otherwise,
   * send tracks & waypoints separately.
   */
  if ((global_opts.masked_objective & WPTDATAMASK) &&
      (global_opts.masked_objective & TRKDATAMASK) &&
      gps_course_transfer != -1) {
    course_write();
  } else {
    if (global_opts.masked_objective & WPTDATAMASK) {
      waypoint_write();
    }
    if (global_opts.masked_objective & TRKDATAMASK) {
      track_write();
    }
  }
  if (global_opts.masked_objective & RTEDATAMASK) {
    route_write();
  }
}


ff_vecs_t garmin_vecs = {
  ff_type_serial,
  FF_CAP_RW_ALL,
  rd_init,
  rw_init,
  rw_deinit,
  rw_deinit,
  data_read,
  data_write,
  nullptr,
  &garmin_args,
  CET_CHARSET_ASCII, 0,
  { pvt_init, pvt_read, rw_deinit, nullptr, nullptr, nullptr },
  nullptr
};

static const char* d103_icons[16] = {
  "dot",
  "house",
  "gas",
  "car",
  "fish",
  "boat",
  "anchor",
  "wreck",
  "exit",
  "skull",
  "flag",
  "camp",
  "circle_x",
  "deer",
  "1st_aid",
  "back-track"
};

static const char*
d103_symbol_from_icon_number(unsigned int n)
{
  if (n  <= 15) {
    return d103_icons[n];
  } else {
    return "unknown";
  }
}

static int
d103_icon_number_from_symbol(const QString& s)
{
  if (s.isNull()) {
    return 0;
  }

  for (unsigned int i = 0; i < sizeof(d103_icons) / sizeof(d103_icons[0]); i++) {
    if (0 == (s.compare(d103_icons[i], Qt::CaseInsensitive))) {
      return i;
    }
  }
  return 0;
}
