/*
    Parse 'GarminDevice.xml' on a Garmin mass storage device (e.g. Zumo,
      Nuvi, Colorado, etc. and return key device info.

    Copyright (C) 2008 Robert Lipe, robertlipe@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., 59 Temple Place - Suite 330, Boston, MA 02111 USA

 */

/*
 * Describes a file on the unit.
 */
typedef struct {
  char* path;
  char* basename;
  char* extension;
  char* canon;	// full name, when applicable.
} gdx_file;

/*
 * The interesting traits of this device.
 */
typedef struct {
  const char* device_desc;
  const char* device_id;
  const char* device_mounted_path; // Not from the file; about the file.
  gdx_file from_device;
  gdx_file to_device;
//	gdx_file geocache_logs;
} gdx_info;

const gdx_info* gdx_read(const char* fname);
const gdx_info* gdx_get_info(void);
const gdx_info* gdx_find_file(char** dirlist);

// This is so gross.   By the time we know it's not a USB device
// and could be one of our devices, we're so deep into the callstack
// that can't back out tracefully without bludgeoning most of the
// (Mac|Lin|Win) x (USB|Serial) matrix.   Since we don't *really* want
// to progress any further, we just longjump back to the caller...
#include <setjmp.h>
extern jmp_buf gdx_jmp_buf;

#if 0

/*
 * The file-level information.
 */
static
struct gpx_global {
  gpx_global_entry name;
  gpx_global_entry desc;
  gpx_global_entry author;
  gpx_global_entry email;
  gpx_global_entry url;
  gpx_global_entry urlname;
  gpx_global_entry keywords;
  /* time and bounds aren't here; they're recomputed. */
}* gpx_global ;

static void
gpx_add_to_global(gpx_global_entry* ge, char* cdata)
{
  queue* elem, *tmp;
  gpx_global_entry* gep;

  QUEUE_FOR_EACH(&ge->queue, elem, tmp) {
    gep = BASE_STRUCT(elem, gpx_global_entry, queue);
    if (0 == strcmp(cdata, gep->tagdata)) {
      return;
    }
  }

  gep = xcalloc(sizeof(*gep), 1);
  QUEUE_INIT(&gep->queue);
  gep->tagdata = xstrdup(cdata);
  ENQUEUE_TAIL(&ge->queue, &gep->queue);
}

static void
gpx_rm_from_global(gpx_global_entry* ge)
{
  queue* elem, *tmp;

  QUEUE_FOR_EACH(&ge->queue, elem, tmp) {
    gpx_global_entry* g = (gpx_global_entry*) dequeue(elem);
    xfree(g->tagdata);
    xfree(g);
  }
}

static void
gpx_write_gdata(gpx_global_entry* ge, char* tag)
{
  queue* elem, *tmp;
  gpx_global_entry* gep;

  if (!gpx_global || QUEUE_EMPTY(&ge->queue)) {
    return;
  }

  gbfprintf(ofd, "<%s>", tag);
  QUEUE_FOR_EACH(&ge->queue, elem, tmp) {
    gep = BASE_STRUCT(elem, gpx_global_entry, queue);
    gbfprintf(ofd, "%s", gep->tagdata);
    /* Some tags we just output once. */
    if ((0 == strcmp(tag, "url")) ||
        (0 == strcmp(tag, "email"))) {
      break;
    }
    gbfprintf(ofd, " ");
  }
  gbfprintf(ofd, "</%s>\n", tag);
}


typedef struct tag_mapping {
  tag_type tag_type;		/* enum from above for this tag */
  int tag_passthrough;		/* true if we don't generate this */
  const char* tag_name;		/* xpath-ish tag name */
  unsigned long crc;		/* Crc32 of tag_name */
} tag_mapping;

/*
 * xpath(ish) mappings between full tag paths and internal identifers.
 * These appear in the order they appear in the GPX specification.
 * If it's not a tag we explictly handle, it doesn't go here.
 */

tag_mapping tag_path_map[] = {
  { tt_gpx, 0, "/gpx", 0UL },
  { tt_name, 0, "/gpx/name", 0UL },
  { tt_desc, 0, "/gpx/desc", 0UL },
  { tt_author, 0, "/gpx/author", 0UL },
  { tt_email, 0, "/gpx/email", 0UL },
  { tt_url, 0, "/gpx/url", 0UL },
  { tt_urlname, 0, "/gpx/urlname", 0UL },
  { tt_keywords, 0, "/gpx/keywords", 0UL },

  { tt_wpt, 0, "/gpx/wpt", 0UL },
  { tt_wpt_ele, 0, "/gpx/wpt/ele", 0UL },
  { tt_wpt_time, 0, "/gpx/wpt/time", 0UL },
  { tt_wpt_name, 0, "/gpx/wpt/name", 0UL },
  { tt_wpt_cmt, 0, "/gpx/wpt/cmt", 0UL },
  { tt_wpt_desc, 0, "/gpx/wpt/desc", 0UL },
  { tt_wpt_url, 0, "/gpx/wpt/url", 0UL },
  { tt_wpt_urlname, 0, "/gpx/wpt/urlname", 0UL },
  { tt_wpt_link, 0, "/gpx/wpt/link", 0UL },			/* GPX 1.1 */
  { tt_wpt_link_text, 0, "/gpx/wpt/link/text", 0UL },		/* GPX 1.1 */
  { tt_wpt_sym, 0, "/gpx/wpt/sym", 0UL },
  { tt_wpt_type, 1, "/gpx/wpt/type", 0UL },

  /* Double up the GPX 1.0 and GPX 1.1 styles */
#define GEOTAG(type,name) \
  {type, 1, "/gpx/wpt/groundspeak:cache/groundspeak:" name, 0UL }, \
  {type, 1, "/gpx/wpt/extensions/cache/" name, 0UL }, \
  {type, 1, "/gpx/wpt/geocache/" name, 0UL }  /* opencaching.de */

#define GARMIN_WPT_EXT "/gpx/wpt/extensions/gpxx:WaypointExtension"

//	GEOTAG( tt_cache, 		"cache"),
  { tt_cache, 1, "/gpx/wpt/groundspeak:cache" },

  GEOTAG(tt_cache_name, 		"name"),
  GEOTAG(tt_cache_container, 	"container"),
  GEOTAG(tt_cache_type, 		"type"),
  GEOTAG(tt_cache_difficulty, 	"difficulty"),
  GEOTAG(tt_cache_terrain, 	"terrain"),
  GEOTAG(tt_cache_hint, 		"encoded_hints"),
  GEOTAG(tt_cache_hint, 		"hints"),  /* opencaching.de */
  GEOTAG(tt_cache_desc_short, 	"short_description"),
  GEOTAG(tt_cache_desc_long, 	"long_description"),
  GEOTAG(tt_cache_placer, 	"owner"),
  { tt_cache_log_wpt, 1, "/gpx/wpt/groundspeak:cache/groundspeak:logs/groundspeak:log/groundspeak:log_wpt"},
  { tt_cache_log_wpt, 1, "/gpx/wpt/extensions/cache/logs/log/log_wpt"},
  { tt_cache_log_type, 1, "/gpx/wpt/groundspeak:cache/groundspeak:logs/groundspeak:log/groundspeak:type"},
  { tt_cache_log_type, 1, "/gpx/wpt/extensions/cache/logs/log/type"},
  { tt_cache_log_date, 1, "/gpx/wpt/groundspeak:cache/groundspeak:logs/groundspeak:log/groundspeak:date"},
  { tt_cache_log_date, 1, "/gpx/wpt/extensions/cache/logs/log/date"},

  { tt_wpt_extensions, 0, "/gpx/wpt/extensions", 0UL },

  { tt_garmin_wpt_extensions, 0, GARMIN_WPT_EXT, 0UL },
  { tt_garmin_wpt_proximity, 0, GARMIN_WPT_EXT "/gpxx:Proximity", 0UL },
  { tt_garmin_wpt_temperature, 0, GARMIN_WPT_EXT "/gpxx:Temperature", 0UL },
  { tt_garmin_wpt_depth, 0, GARMIN_WPT_EXT "/gpxx:Depth", 0UL },
  { tt_garmin_wpt_display_mode, 0, GARMIN_WPT_EXT "/gpxx:DisplayMode", 0UL },
  { tt_garmin_wpt_categories, 0, GARMIN_WPT_EXT "/gpxx:Categories", 0UL },
  { tt_garmin_wpt_category, 0, GARMIN_WPT_EXT "/gpxx:Categories/gpxx:Category", 0UL },
  { tt_garmin_wpt_addr, 0, GARMIN_WPT_EXT "/gpxx:Address/gpxx:StreetAddress", 0UL },
  { tt_garmin_wpt_city, 0, GARMIN_WPT_EXT "/gpxx:Address/gpxx:City", 0UL },
  { tt_garmin_wpt_state, 0, GARMIN_WPT_EXT "/gpxx:Address/gpxx:State", 0UL },
  { tt_garmin_wpt_country, 0, GARMIN_WPT_EXT "/gpxx:Address/gpxx:Country", 0UL },
  { tt_garmin_wpt_postal_code, 0, GARMIN_WPT_EXT "/gpxx:Address/gpxx:PostalCode", 0UL },
  { tt_garmin_wpt_phone_nr, 0, GARMIN_WPT_EXT "/gpxx:PhoneNumber", 0UL },

  { tt_rte, 0, "/gpx/rte", 0UL },
  { tt_rte_name, 0, "/gpx/rte/name", 0UL },
  { tt_rte_desc, 0, "/gpx/rte/desc", 0UL },
  { tt_rte_number, 0, "/gpx/rte/number", 0UL },
  { tt_rte_rtept, 0, "/gpx/rte/rtept", 0UL },
  { tt_rte_rtept_ele, 0, "/gpx/rte/rtept/ele", 0UL },
  { tt_rte_rtept_time, 0, "/gpx/rte/rtept/time", 0UL },
  { tt_rte_rtept_name, 0, "/gpx/rte/rtept/name", 0UL },
  { tt_rte_rtept_cmt, 0, "/gpx/rte/rtept/cmt", 0UL },
  { tt_rte_rtept_desc, 0, "/gpx/rte/rtept/desc", 0UL },
  { tt_rte_rtept_url, 0, "/gpx/rte/rtept/url", 0UL },
  { tt_rte_rtept_urlname, 0, "/gpx/rte/rtept/urlname", 0UL },
  { tt_rte_rtept_sym, 0, "/gpx/rte/rtept/sym", 0UL },

  { tt_trk, 0, "/gpx/trk", 0UL },
  { tt_trk_name, 0, "/gpx/trk/name", 0UL },
  { tt_trk_desc, 0, "/gpx/trk/desc", 0UL },
  { tt_trk_trkseg, 0, "/gpx/trk/trkseg", 0UL },
  { tt_trk_number, 0, "/gpx/trk/number", 0UL },
  { tt_trk_trkseg_trkpt, 0, "/gpx/trk/trkseg/trkpt", 0UL },
  { tt_trk_trkseg_trkpt_ele, 0, "/gpx/trk/trkseg/trkpt/ele", 0UL },
  { tt_trk_trkseg_trkpt_time, 0, "/gpx/trk/trkseg/trkpt/time", 0UL },
  { tt_trk_trkseg_trkpt_name, 0, "/gpx/trk/trkseg/trkpt/name", 0UL },
  { tt_trk_trkseg_trkpt_cmt, 0, "/gpx/trk/trkseg/trkpt/cmt", 0UL },
  { tt_trk_trkseg_trkpt_desc, 0, "/gpx/trk/trkseg/trkpt/desc", 0UL },
  { tt_trk_trkseg_trkpt_url, 0, "/gpx/trk/trkseg/trkpt/url", 0UL },
  { tt_trk_trkseg_trkpt_urlname, 0, "/gpx/trk/trkseg/trkpt/urlname", 0UL },
  { tt_trk_trkseg_trkpt_sym, 0, "/gpx/trk/trkseg/trkpt/sym", 0UL },
  { tt_trk_trkseg_trkpt_course, 0, "/gpx/trk/trkseg/trkpt/course", 0UL },
  { tt_trk_trkseg_trkpt_speed, 0, "/gpx/trk/trkseg/trkpt/speed", 0UL },

  /* Common to tracks, routes, and waypts */
  { tt_fix,  0, "/gpx/wpt/fix", 0UL },
  { tt_fix,  0, "/gpx/trk/trkseg/trkpt/fix", 0UL },
  { tt_fix,  0, "/gpx/rte/rtept/fix", 0UL },
  { tt_sat,  0, "/gpx/wpt/sat", 0UL },
  { tt_sat,  0, "/gpx/trk/trkseg/trkpt/sat", 0UL },
  { tt_sat,  0, "/gpx/rte/rtept/sat", 0UL },
  { tt_pdop, 0, "/gpx/wpt/pdop", 0UL },
  { tt_pdop, 0, "/gpx/trk/trkseg/trkpt/pdop", 0UL },
  { tt_pdop, 0, "/gpx/rte/rtept/pdop", 0UL },
  { tt_hdop, 0, "/gpx/wpt/hdop", 0UL },
  { tt_hdop, 0, "/gpx/trk/trkseg/trkpt/hdop", 0UL },
  { tt_hdop, 0, "/gpx/rte/rtept/hdop", 0UL },
  { tt_vdop, 0, "/gpx/wpt/vdop", 0UL },
  { tt_vdop, 0, "/gpx/trk/trkseg/trkpt/vdop", 0UL },
  { tt_vdop, 0, "/gpx/rte/rtept/hdop", 0UL },
  {0, 0, NULL, 0UL}
};

static tag_type
get_tag(const char* t, int* passthrough)
{
  tag_mapping* tm;
  unsigned long tcrc = get_crc32_s(t);

  for (tm = tag_path_map; tm->tag_type != 0; tm++) {
    if ((tcrc == tm->crc) && (0 == strcmp(tm->tag_name, t))) {
      *passthrough = tm->tag_passthrough;
      return tm->tag_type;
    }
  }
  *passthrough = 1;
  return tt_unknown;
}

static void
prescan_tags(void)
{
  tag_mapping* tm;
  for (tm = tag_path_map; tm->tag_type != 0; tm++) {
    tm->crc = get_crc32_s(tm->tag_name);
  }
}

static void
tag_gpx(const char** attrv)
{
  const char** avp;
  for (avp = &attrv[0]; *avp; avp += 2) {
    if (strcmp(avp[0], "version") == 0) {
      gpx_version = avp[1];
    } else if (strcmp(avp[0], "src") == 0) {
      gpx_creator = avp[1];
    }
    /*
     * Our handling of schemaLocation really is weird.
     * If we see we have a "normal" GPX 1.1 header, on read,
     * flip our default on write to use that and don't append
     * it to the rest...
     */
    else if (strcmp(avp[0], "xsi:schemaLocation") == 0) {
      if (0 == strcmp(avp[1], DEFAULT_XSI_SCHEMA_LOC_11)) {
        if (0 == strcmp(xsi_schema_loc, DEFAULT_XSI_SCHEMA_LOC)) {
          xfree(xsi_schema_loc);
        }
        xsi_schema_loc = xstrdup(DEFAULT_XSI_SCHEMA_LOC_11);
        continue;
      }
      if (0 == strstr(xsi_schema_loc, avp[1])) {
        xsi_schema_loc = xstrappend(xsi_schema_loc, " ");
        xsi_schema_loc = xstrappend(xsi_schema_loc, avp[1]);
      }
    }
  }
}

static void
tag_wpt(const char** attrv)
{
  const char** avp = &attrv[0];

  wpt_tmp = waypt_new();

  cur_tag = NULL;
  while (*avp) {
    if (strcmp(avp[0], "lat") == 0) {
      sscanf(avp[1], "%lf",
             &wpt_tmp->latitude);
    } else if (strcmp(avp[0], "lon") == 0) {
      sscanf(avp[1], "%lf",
             &wpt_tmp->longitude);
    }
    avp+=2;
  }
  fs_ptr = &wpt_tmp->fs;
}

static void
tag_cache_desc(const char** attrv)
{
  const char** avp;

  cache_descr_is_html = 0;
  for (avp = &attrv[0]; *avp; avp+=2) {
    if (strcmp(avp[0], "html") == 0) {
      if (strcmp(avp[1], "True") == 0) {
        cache_descr_is_html = 1;
      }
    }
  }
}

static void
tag_gs_cache(const char** attrv)
{
  const char** avp;

  for (avp = &attrv[0]; *avp; avp+=2) {
    if (strcmp(avp[0], "id") == 0) {
      wpt_tmp->gc_data.id = atoi(avp[1]);
    } else if (strcmp(avp[0], "available") == 0) {
      if (case_ignore_strcmp(avp[1], "True") == 0) {
        wpt_tmp->gc_data.is_available = status_true;
      } else if (case_ignore_strcmp(avp[1], "False") == 0) {
        wpt_tmp->gc_data.is_available = status_false;
      }
    } else if (strcmp(avp[0], "archived") == 0) {
      if (case_ignore_strcmp(avp[1], "True") == 0) {
        wpt_tmp->gc_data.is_archived = status_true;
      } else if (case_ignore_strcmp(avp[1], "False") == 0) {
        wpt_tmp->gc_data.is_archived = status_false;
      }
    }
  }
}

static void
start_something_else(const char* el, const char** attrv)
{
  const char** avp = attrv;
  char** avcp = NULL;
  int attr_count = 0;
  xml_tag* new_tag;
  fs_xml* fs_gpx;

  if (!fs_ptr) {
    return;
  }

  new_tag = (xml_tag*)xcalloc(sizeof(xml_tag),1);
  new_tag->tagname = xstrdup(el);

  /* count attributes */
  while (*avp) {
    attr_count++;
    avp++;
  }

  /* copy attributes */
  avp = attrv;
  new_tag->attributes = (char**)xcalloc(sizeof(char*),attr_count+1);
  avcp = new_tag->attributes;
  while (*avp) {
    *avcp = xstrdup(*avp);
    avcp++;
    avp++;
  }
  *avcp = NULL;

  if (cur_tag) {
    if (cur_tag->child) {
      cur_tag = cur_tag->child;
      while (cur_tag->sibling) {
        cur_tag = cur_tag->sibling;
      }
      cur_tag->sibling = new_tag;
      new_tag->parent = cur_tag->parent;
    } else {
      cur_tag->child = new_tag;
      new_tag->parent = cur_tag;
    }
  } else {
    fs_gpx = (fs_xml*)fs_chain_find(*fs_ptr, FS_GPX);

    if (fs_gpx && fs_gpx->tag) {
      cur_tag = fs_gpx->tag;
      while (cur_tag->sibling) {
        cur_tag = cur_tag->sibling;
      }
      cur_tag->sibling = new_tag;
      new_tag->parent = NULL;
    } else {
      fs_gpx = fs_xml_alloc(FS_GPX);
      fs_gpx->tag = new_tag;
      fs_chain_add(fs_ptr, (format_specific_data*)fs_gpx);
      new_tag->parent = NULL;
    }
  }
  cur_tag = new_tag;
}

static void
end_something_else()
{
  if (cur_tag) {
    cur_tag = cur_tag->parent;
  }
}

static void
tag_log_wpt(const char** attrv)
{
  waypoint* lwp_tmp;
  const char** avp = &attrv[0];

  /* create a new waypoint */
  lwp_tmp = waypt_new();

  /* extract the lat/lon attributes */
  while (*avp) {
    if (strcmp(avp[0], "lat") == 0) {
      sscanf(avp[1], "%lf",
             &lwp_tmp->latitude);
    } else if (strcmp(avp[0], "lon") == 0) {
      sscanf(avp[1], "%lf",
             &lwp_tmp->longitude);
    }
    avp+=2;
  }
  /* Make a new shortname.  Since this is a groundspeak extension,
    we assume that GCBLAH is the current shortname format and that
    wpt_tmp refers to the currently parsed waypoint. Unfortunatley,
    we need to keep track of log_wpt counts so we don't collide with
    dupe shortnames.
  */

  if ((wpt_tmp->shortname) && (strlen(wpt_tmp->shortname) > 2)) {
    /* copy of the shortname */
    lwp_tmp->shortname = xcalloc(7, 1);
    sprintf(lwp_tmp->shortname, "%-4.4s%02d",
            &wpt_tmp->shortname[2], logpoint_ct++);

    waypt_add(lwp_tmp);
  }
}

static void
gpx_start(void* data, const XML_Char* xml_el, const XML_Char** xml_attr)
{
  char* e;
  char* ep;
  int passthrough;
  const char* el = xml_convert_to_char_string(xml_el);
  const char** attr = xml_convert_attrs_to_char_string(xml_attr);

  vmem_realloc(&current_tag, strlen(current_tag.mem) + 2 + strlen(el));
  e = current_tag.mem;
  ep = e + strlen(e);
  *ep++ = '/';
  strcpy(ep, el);


  /*
   * FIXME: Find out why a cdatastr[0] doesn't adequately reset the
   * cdata handler.
   */
  memset(cdatastr.mem, 0, cdatastr.size);

  switch (get_tag(current_tag.mem, &passthrough)) {
  case tt_gpx:
    tag_gpx(attr);
    break;
  case tt_wpt:
    tag_wpt(attr);
    break;
  case tt_wpt_link:
    if (0 == strcmp(attr[0], "href")) {
      link_url = attr[1];
    }
    break;
  case tt_wpt_link_text:
    link_text = cdatastr.mem;
    break;
  case tt_rte:
    rte_head = route_head_alloc();
    route_add_head(rte_head);
    fs_ptr = &rte_head->fs;
    break;
  case tt_rte_rtept:
    tag_wpt(attr);
    break;
  case tt_trk:
    trk_head = route_head_alloc();
    track_add_head(trk_head);
    fs_ptr = &trk_head->fs;
    break;
  case tt_trk_trkseg_trkpt:
    tag_wpt(attr);
    break;
  case tt_unknown:
    start_something_else(el, attr);
    return;
  case tt_cache:
    tag_gs_cache(attr);
    break;
  case tt_cache_log_wpt:
    if (opt_logpoint) {
      tag_log_wpt(attr);
    }
    break;
  case tt_cache_desc_long:
  case tt_cache_desc_short:
    tag_cache_desc(attr);
    break;
  case tt_cache_placer:
    if (*attr && (0 == strcmp(attr[0], "id"))) {
      wpt_tmp->gc_data.placer_id = atoi(attr[1]);
    }
  default:
    break;
  }
  if (passthrough) {
    start_something_else(el, attr);
  }
  xml_free_converted_string(el);
  xml_free_converted_attrs(attr);
}

struct
    gs_type_mapping {
  geocache_type type;
  const char* name;
} gs_type_map[] = {
  { gt_traditional, "Traditional Cache" },
  { gt_traditional, "Traditional" }, /* opencaching.de */
  { gt_multi, "Multi-cache" },
  { gt_multi, "Multi" }, /* opencaching.de */
  { gt_virtual, "Virtual Cache" },
  { gt_virtual, "Virtual" }, /* opencaching.de */
  { gt_event, "Event Cache" },
  { gt_event, "Event" }, /* opencaching.de */
  { gt_webcam, "Webcam Cache" },
  { gt_webcam, "Webcam" }, /* opencaching.de */
  { gt_suprise, "Unknown Cache" },
  { gt_earth, "Earthcache" },
  { gt_earth, "Earth" }, /* opencaching.de */
  { gt_cito, "Cache In Trash Out Event" },
  { gt_letterbox, "Letterbox Hybrid" },
  { gt_locationless, "Locationless (Reverse) Cache" },
  { gt_ape, "Project APE Cache" },
  { gt_mega, "Mega-Event Cache" },

  { gt_benchmark, "Benchmark" }, /* Not Groundspeak; for GSAK  */
};

struct
    gs_container_mapping {
  geocache_container type;
  const char* name;
} gs_container_map[] = {
  { gc_other, "Unknown" },
  { gc_other, "Other" }, /* Synonym on read. */
  { gc_micro, "Micro" },
  { gc_regular, "Regular" },
  { gc_large, "Large" },
  { gc_small, "Small" },
  { gc_virtual, "Virtual" }
};

geocache_type
gs_mktype(const char* t)
{
  int i;
  int sz = sizeof(gs_type_map) / sizeof(gs_type_map[0]);

  for (i = 0; i < sz; i++) {
    if (0 == case_ignore_strcmp(t, gs_type_map[i].name)) {
      return gs_type_map[i].type;
    }
  }
  return gt_unknown;
}

const char*
gs_get_cachetype(geocache_type t)
{
  int i;
  int sz = sizeof(gs_type_map) / sizeof(gs_type_map[0]);

  for (i = 0; i < sz; i++) {
    if (t == gs_type_map[i].type) {
      return gs_type_map[i].name;
    }
  }
  return "Unknown";
}

geocache_container
gs_mkcont(const char* t)
{
  int i;
  int sz = sizeof(gs_container_map) / sizeof(gs_container_map[0]);

  for (i = 0; i < sz; i++) {
    if (0 == case_ignore_strcmp(t, gs_container_map[i].name)) {
      return gs_container_map[i].type;
    }
  }
  return gc_unknown;
}

const char*
gs_get_container(geocache_container t)
{
  int i;
  int sz = sizeof(gs_container_map) / sizeof(gs_container_map[0]);

  for (i = 0; i < sz; i++) {
    if (t == gs_container_map[i].type) {
      return gs_container_map[i].name;
    }
  }
  return "Unknown";
}

time_t
xml_parse_time(const char* cdatastr, int* microsecs)
{
  int off_hr = 0;
  int off_min = 0;
  int off_sign = 1;
  char* offsetstr = NULL;
  char* pointstr = NULL;
  struct tm tm;
  time_t rv = 0;
  char* timestr = xstrdup(cdatastr);

  memset(&tm, 0, sizeof(tm));

  offsetstr = strchr(timestr, 'Z');
  if (offsetstr) {
    /* zulu time; offsets stay at defaults */
    *offsetstr = '\0';
  } else {
    offsetstr = strchr(timestr, '+');
    if (offsetstr) {
      /* positive offset; parse it */
      *offsetstr = '\0';
      sscanf(offsetstr+1, "%d:%d", &off_hr, &off_min);
    } else {
      offsetstr = strchr(timestr, 'T');
      if (offsetstr) {
        offsetstr = strchr(offsetstr, '-');
        if (offsetstr) {
          /* negative offset; parse it */
          *offsetstr = '\0';
          sscanf(offsetstr+1, "%d:%d",
                 &off_hr, &off_min);
          off_sign = -1;
        }
      }
    }
  }

  pointstr = strchr(timestr, '.');
  if (pointstr) {
    if (microsecs) {
      double fsec;
      sscanf(pointstr, "%le", &fsec);
      /* Round to avoid FP jitter */
      *microsecs = .5 + (fsec * 1000000.0) ;
    }
    *pointstr = '\0';
  }

  sscanf(timestr, "%d-%d-%dT%d:%d:%d",
         &tm.tm_year,
         &tm.tm_mon,
         &tm.tm_mday,
         &tm.tm_hour,
         &tm.tm_min,
         &tm.tm_sec);
  tm.tm_mon -= 1;
  tm.tm_year -= 1900;
  tm.tm_isdst = 0;

  rv = mkgmtime(&tm) - off_sign*off_hr*3600 - off_sign*off_min*60;

  xfree(timestr);

  return rv;
}

static void
gpx_end(void* data, const XML_Char* xml_el)
{
  const char* el = xml_convert_to_char_string(xml_el);
  char* s = strrchr(current_tag.mem, '/');
  float x;
  char* cdatastrp = cdatastr.mem;
  int passthrough;
  static time_t gc_log_date;
  tag_type tag;

  if (strcmp(s + 1, el)) {
    fprintf(stderr, "Mismatched tag %s\n", el);
  }

  tag = get_tag(current_tag.mem, &passthrough);
  switch (tag) {
    /*
     * First, the tags that are file-global.
     */
  case tt_name:
    gpx_add_to_global(&gpx_global->name, cdatastrp);
    break;
  case tt_desc:
    gpx_add_to_global(&gpx_global->desc, cdatastrp);
    break;
  case tt_author:
    gpx_add_to_global(&gpx_global->author, cdatastrp);
    break;
  case tt_email:
    gpx_add_to_global(&gpx_global->email, cdatastrp);
    if (gpx_email == NULL) {
      gpx_email = xstrdup(cdatastrp);
    }
    break;
  case tt_url:
    gpx_add_to_global(&gpx_global->url, cdatastrp);
    break;
  case tt_urlname:
    gpx_add_to_global(&gpx_global->urlname, cdatastrp);
    break;
  case tt_keywords:
    gpx_add_to_global(&gpx_global->keywords, cdatastrp);
    break;

    /*
     * Waypoint-specific tags.
     */
  case tt_wpt:
    waypt_add(wpt_tmp);
    logpoint_ct = 0;
    cur_tag = NULL;
    wpt_tmp = NULL;
    break;
  case tt_cache_name:
    if (wpt_tmp->notes != NULL) {
      xfree(wpt_tmp->notes);
    }
    wpt_tmp->notes = xstrdup(cdatastrp);
    break;
  case tt_cache_container:
    wpt_tmp->gc_data.container = gs_mkcont(cdatastrp);
    break;
  case tt_cache_type:
    wpt_tmp->gc_data.type = gs_mktype(cdatastrp);
    break;
  case tt_cache_difficulty:
    sscanf(cdatastrp, "%f", &x);
    wpt_tmp->gc_data.diff = x * 10;
    break;
  case tt_cache_hint:
    rtrim(cdatastrp);
    if (cdatastrp[0]) {
      wpt_tmp->gc_data.hint = xstrdup(cdatastrp);
    }
    break;
  case tt_cache_desc_long:
    rtrim(cdatastrp);
    if (cdatastrp[0]) {
      wpt_tmp->gc_data.desc_long.is_html = cache_descr_is_html;
      wpt_tmp->gc_data.desc_long.utfstring = xstrdup(cdatastrp);
    }
    break;
  case tt_cache_desc_short:
    rtrim(cdatastrp);
    if (cdatastrp[0]) {
      wpt_tmp->gc_data.desc_short.is_html = cache_descr_is_html;
      wpt_tmp->gc_data.desc_short.utfstring = xstrdup(cdatastrp);
    }
    break;
  case tt_cache_terrain:
    sscanf(cdatastrp, "%f", &x);
    wpt_tmp->gc_data.terr = x * 10;
    break;
  case tt_cache_placer:
    wpt_tmp->gc_data.placer = xstrdup(cdatastrp);
    break;
  case tt_cache_log_date:
    gc_log_date = xml_parse_time(cdatastrp, NULL);
    break;
    /*
     * "Found it" logs follow the date according to the schema,
     * if this is the first "found it" for this waypt, just use the
     * last date we saw in this log.
     */
  case tt_cache_log_type:
    if ((0 == strcmp(cdatastrp, "Found it")) &&
        (0 == wpt_tmp->gc_data.last_found)) {
      wpt_tmp->gc_data.last_found  = gc_log_date;
    }
    gc_log_date = 0;
    break;

    /*
     * Garmin-waypoint-specific tags.
     */
  case tt_garmin_wpt_proximity:
  case tt_garmin_wpt_temperature:
  case tt_garmin_wpt_depth:
  case tt_garmin_wpt_display_mode:
  case tt_garmin_wpt_category:
  case tt_garmin_wpt_addr:
  case tt_garmin_wpt_city:
  case tt_garmin_wpt_state:
  case tt_garmin_wpt_country:
  case tt_garmin_wpt_postal_code:
  case tt_garmin_wpt_phone_nr:
    garmin_fs_xml_convert(tt_garmin_wpt_extensions, tag, cdatastrp, wpt_tmp);
    break;

    /*
     * Route-specific tags.
     */
  case tt_rte_name:
    rte_head->rte_name = xstrdup(cdatastrp);
    break;
  case tt_rte:
    break;
  case tt_rte_rtept:
    route_add_wpt(rte_head, wpt_tmp);
    wpt_tmp = NULL;
    break;
  case tt_rte_desc:
    rte_head->rte_desc = xstrdup(cdatastrp);
    break;
  case tt_rte_number:
    rte_head->rte_num = atoi(cdatastrp);
    break;
    /*
     * Track-specific tags.
     */
  case tt_trk_name:
    trk_head->rte_name = xstrdup(cdatastrp);
    break;
  case tt_trk:
    break;
  case tt_trk_trkseg_trkpt:
    track_add_wpt(trk_head, wpt_tmp);
    wpt_tmp = NULL;
    break;
  case tt_trk_desc:
    trk_head->rte_desc = xstrdup(cdatastrp);
    break;
  case tt_trk_number:
    trk_head->rte_num = atoi(cdatastrp);
    break;
  case tt_trk_trkseg_trkpt_course:
    WAYPT_SET(wpt_tmp, course, atof(cdatastrp));
    break;
  case tt_trk_trkseg_trkpt_speed:
    WAYPT_SET(wpt_tmp, speed, atof(cdatastrp));
    break;

    /*
     * Items that are actually in multiple categories.
     */
  case tt_wpt_ele:
  case tt_rte_rtept_ele:
  case tt_trk_trkseg_trkpt_ele:
    sscanf(cdatastrp, "%lf", &wpt_tmp->altitude);
    break;
  case tt_wpt_name:
  case tt_rte_rtept_name:
  case tt_trk_trkseg_trkpt_name:
    wpt_tmp->shortname = xstrdup(cdatastrp);
    break;
  case tt_wpt_sym:
  case tt_rte_rtept_sym:
  case tt_trk_trkseg_trkpt_sym:
    wpt_tmp->icon_descr = xstrdup(cdatastrp);
    wpt_tmp->wpt_flags.icon_descr_is_dynamic = 1;
    break;
  case tt_wpt_time:
  case tt_trk_trkseg_trkpt_time:
  case tt_rte_rtept_time:
    wpt_tmp->creation_time = xml_parse_time(cdatastrp, &wpt_tmp->microseconds);
    break;
  case tt_wpt_cmt:
  case tt_rte_rtept_cmt:
  case tt_trk_trkseg_trkpt_cmt:
    wpt_tmp->description = xstrdup(cdatastrp);
    break;
  case tt_wpt_desc:
  case tt_trk_trkseg_trkpt_desc:
  case tt_rte_rtept_desc:
    if (wpt_tmp->notes != NULL) {
      xfree(wpt_tmp->notes);
    }
    wpt_tmp->notes = xstrdup(cdatastrp);
    break;
  case tt_pdop:
    wpt_tmp->pdop = atof(cdatastrp);
    break;
  case tt_hdop:
    wpt_tmp->hdop = atof(cdatastrp);
    break;
  case tt_vdop:
    wpt_tmp->vdop = atof(cdatastrp);
    break;
  case tt_sat:
    wpt_tmp->sat = atof(cdatastrp);
    break;
  case tt_fix:
    wpt_tmp->fix = atoi(cdatastrp)-1;
    if (wpt_tmp->fix < fix_2d) {
      if (!case_ignore_strcmp(cdatastrp, "none")) {
        wpt_tmp->fix = fix_none;
      } else if (!case_ignore_strcmp(cdatastrp, "dgps")) {
        wpt_tmp->fix = fix_dgps;
      } else if (!case_ignore_strcmp(cdatastrp, "pps")) {
        wpt_tmp->fix = fix_pps;
      } else {
        wpt_tmp->fix = fix_unknown;
      }
    }
    break;
  case tt_wpt_url:
  case tt_trk_trkseg_trkpt_url:
  case tt_rte_rtept_url:
    wpt_tmp->url = xstrdup(cdatastrp);
    break;
  case tt_wpt_urlname:
  case tt_trk_trkseg_trkpt_urlname:
  case tt_rte_rtept_urlname:
    wpt_tmp->url_link_text = xstrdup(cdatastrp);
    break;
  case tt_wpt_link:
//TODO: implement GPX 1.1 	case tt_trk_trkseg_trkpt_link:
//TODO: implement GPX 1.1 	case tt_rte_rtept_link:
  {
    char* lt = link_text;
    if (lt) {
      lt = xstrdup(lrtrim(link_text));
    }

    waypt_add_url(wpt_tmp, xstrdup(link_url), lt);
    link_text = NULL;
  }
  break;
  case tt_unknown:
    end_something_else();
    *s = 0;
    return;
  default:
    break;
  }

  if (passthrough) {
    end_something_else();
  }

  *s = 0;
  xml_free_converted_string(el);
}

#if ! HAVE_LIBEXPAT
static void
gpx_rd_init(const char* fname)
{
  fatal(MYNAME ": This build excluded GPX support because expat was not installed.\n");
}

static void
gpx_rd_deinit(void)
{
}

#else /* NO_EXPAT */

static void
gpx_cdata(void* dta, const XML_Char* xml_el, int len)
{
  char* estr;
  int* cdatalen;
  char** cdata;
  xml_tag* tmp_tag;
  size_t slen = strlen(cdatastr.mem);
  const char* s = xml_convert_to_char_string_n(xml_el, &len);

  vmem_realloc(&cdatastr,  1 + len + slen);
  estr = ((char*) cdatastr.mem) + slen;
  memcpy(estr, s, len);
  estr[len]  = 0;

  if (!cur_tag) {
    return;
  }

  if (cur_tag->child) {
    tmp_tag = cur_tag->child;
    while (tmp_tag->sibling) {
      tmp_tag = tmp_tag->sibling;
    }
    cdata = &(tmp_tag->parentcdata);
    cdatalen = &(tmp_tag->parentcdatalen);
  } else {
    cdata = &(cur_tag->cdata);
    cdatalen = &(cur_tag->cdatalen);
  }
  estr = *cdata;
  *cdata = xcalloc(*cdatalen + len + 1, 1);
  if (estr) {
    memcpy(*cdata, estr, *cdatalen);
    xfree(estr);
  }
  estr = *cdata + *cdatalen;
  memcpy(estr, s, len);
  *(estr+len) = '\0';
  *cdatalen += len;

  xml_free_converted_string(s);
}

static void
gpx_rd_init(const char* fname)
{
  if (fname[0]) {
    fd = gbfopen(fname, "r", MYNAME);
    input_fname = fname;
  } else {
    fd = NULL;
    input_string = fname+1;
    input_string_len = strlen(input_string);
    input_fname = NULL;
  }


  file_time = 0;
  current_tag = vmem_alloc(1, 0);
  *((char*)current_tag.mem) = '\0';

  prescan_tags();

  psr = XML_ParserCreate(NULL);
  if (!psr) {
    fatal(MYNAME ": Cannot create XML Parser\n");
  }
  XML_SetUnknownEncodingHandler(psr, cet_lib_expat_UnknownEncodingHandler, NULL);

  cdatastr = vmem_alloc(1, 0);
  *((char*)cdatastr.mem) = '\0';

  if (!xsi_schema_loc) {
    xsi_schema_loc = xstrdup(DEFAULT_XSI_SCHEMA_LOC);
  }
  if (!xsi_schema_loc) {
    fatal("gpx: Unable to allocate %ld bytes of memory.\n",
          (unsigned long) strlen(DEFAULT_XSI_SCHEMA_LOC) + 1);
  }

  if (NULL == gpx_global) {
    gpx_global = xcalloc(sizeof(*gpx_global), 1);
    QUEUE_INIT(&gpx_global->name.queue);
    QUEUE_INIT(&gpx_global->desc.queue);
    QUEUE_INIT(&gpx_global->author.queue);
    QUEUE_INIT(&gpx_global->email.queue);
    QUEUE_INIT(&gpx_global->url.queue);
    QUEUE_INIT(&gpx_global->urlname.queue);
    QUEUE_INIT(&gpx_global->keywords.queue);
  }

  XML_SetElementHandler(psr, gpx_start, gpx_end);
  XML_SetCharacterDataHandler(psr, gpx_cdata);
  fs_ptr = NULL;
}

static
void
gpx_rd_deinit(void)
{
  vmem_free(&current_tag);
  vmem_free(&cdatastr);
  /*
   * Don't free schema_loc.  It really is important that we preserve
   * this across reads or else merges/copies of files with different
   * schemas won't retain the headers.
   *
   *  moved to gpx_exit

  if ( xsi_schema_loc ) {
  	xfree(xsi_schema_loc);
  	xsi_schema_loc = NULL;
  }
  */

  if (gpx_email) {
    xfree(gpx_email);
    gpx_email = NULL;
  }
  if (gpx_author) {
    xfree(gpx_author);
    gpx_author = NULL;
  }
  if (fd) {
    gbfclose(fd);
  }
  XML_ParserFree(psr);
  psr = NULL;
  wpt_tmp = NULL;
  cur_tag = NULL;
  input_fname = NULL;
}
#endif

static void
gpx_wr_init(const char* fname)
{
  mkshort_handle = mkshort_new_handle();

  ofd = gbfopen(fname, "w", MYNAME);
}

static void
gpx_wr_deinit(void)
{
  gbfclose(ofd);
  mkshort_del_handle(&mkshort_handle);
}

void
gpx_read(void)
{
#if HAVE_LIBEXPAT
  int len;
  int done = 0;
  char* buf = xmalloc(MY_CBUF_SZ);
  int result = 0;
  int extra;

  while (!done) {
    if (fd) {
      /*
       * The majority of this block (in fact, all but the
       * call to XML_Parse) are a disgusting hack to
       * correct defective GPX files that Geocaching.com
       * issues as pocket queries.   They contain escape
       * characters as entities (&#x00-&#x1f) which makes
       * them not validate which croaks expat and torments
       * users.
       *
       * Look for '&' in the last maxentlength chars.   If
       * we find it, strip it, then read byte-at-a-time
       * until we find a non-entity.
       */
      char* badchar;
      char* semi;
      int maxentlength = 8;
      len = gbfread(buf, 1, MY_CBUF_SZ - maxentlength, fd);
      done = gbfeof(fd) || !len;
      buf[len] = '\0';
      if (len < maxentlength) {
        maxentlength = len;
      }
      badchar = buf+len-maxentlength;
      badchar = strchr(badchar, '&');
      extra = maxentlength - 1; /* for terminator */
      while (badchar && len < MY_CBUF_SZ-1) {
        semi = strchr(badchar, ';');
        while (extra && !semi) {
          len += gbfread(buf+len, 1, 1, fd);
          buf[len]='\0';
          extra--;
          if (buf[len-1] == ';') {
            semi= buf+len-1;
          }
        }
        badchar = strchr(badchar+1, '&');
      }
      {
        char* hex="0123456789abcdef";
        badchar = strstr(buf, "&#x");
        while (badchar) {
          int val = 0;
          char* hexit = badchar+3;
          semi = strchr(badchar, ';');
          if (semi) {
            while (*hexit && *hexit != ';') {
              char hc = isalpha(*hexit) ? tolower(*hexit) : *hexit;
              val *= 16;
              val += strchr(hex, hc)-hex;
              hexit++;
            }

            if (val < 32) {
              warning(MYNAME ": Ignoring illegal character %s;\n\tConsider emailing %s at <%s>\n\tabout illegal characters in their GPX files.\n", badchar, gpx_author?gpx_author:"(unknown author)", gpx_email?gpx_email:"(unknown email address)");
              memmove(badchar, semi+1, strlen(semi+1)+1);
              len -= (semi-badchar)+1;
              badchar--;
            }
          }
          badchar = strstr(badchar+1, "&#x");
        }
      }
      result = XML_Parse(psr, buf, len, done);
    } else if (input_string) {
      done = 0;
      result = XML_Parse(psr, input_string,
                         input_string_len, done);
      done = 1;
    } else {
      done = 1;
      result = -1;
    }
    if (!result) {
      fatal(MYNAME ": XML parse error at line %d of '%s' : %s\n",
            (int) XML_GetCurrentLineNumber(psr),
            input_fname ? input_fname : "unknown file",
            XML_ErrorString(XML_GetErrorCode(psr)));
    }
  }
  xfree(buf);
#endif /* HAVE_LIBEXPAT */
}

static void
fprint_tag_and_attrs(char* prefix, char* suffix, xml_tag* tag)
{
  char** pa;
  gbfprintf(ofd, "%s%s", prefix, tag->tagname);
  pa = tag->attributes;
  if (pa) {
    while (*pa) {
      gbfprintf(ofd, " %s=\"%s\"", pa[0], pa[1]);
      pa += 2;
    }
  }
  gbfprintf(ofd, "%s", suffix);
}

static void
fprint_xml_chain(xml_tag* tag, const waypoint* wpt)
{
  char* tmp_ent;
  while (tag) {
    if (!tag->cdata && !tag->child) {
      fprint_tag_and_attrs("<", " />", tag);
    } else {
      fprint_tag_and_attrs("<", ">", tag);

      if (tag->cdata) {
        tmp_ent = xml_entitize(tag->cdata);
        gbfprintf(ofd, "%s", tmp_ent);
        xfree(tmp_ent);
      }
      if (tag->child) {
        fprint_xml_chain(tag->child, wpt);
      }
      if (wpt && wpt->gc_data.exported &&
          strcmp(tag->tagname, "groundspeak:cache") == 0) {
        xml_write_time(ofd, wpt->gc_data.exported, 0,
                       "groundspeak:exported");
      }
      gbfprintf(ofd, "</%s>\n", tag->tagname);
    }
    if (tag->parentcdata) {
      tmp_ent = xml_entitize(tag->parentcdata);
      gbfprintf(ofd, "%s", tmp_ent);
      xfree(tmp_ent);
    }
    tag = tag->sibling;
  }
}

void free_gpx_extras(xml_tag* tag)
{
  xml_tag* next = NULL;
  char** ap;

  while (tag) {
    if (tag->cdata) {
      xfree(tag->cdata);
    }
    if (tag->child) {
      free_gpx_extras(tag->child);
    }
    if (tag->parentcdata) {
      xfree(tag->parentcdata);
    }
    if (tag->tagname) {
      xfree(tag->tagname);
    }
    if (tag->attributes) {
      ap = tag->attributes;

      while (*ap) {
        xfree(*ap++);
      }

      xfree(tag->attributes);
    }

    next = tag->sibling;
    xfree(tag);
    tag = next;
  }
}

/*
 * Handle the grossness of GPX 1.0 vs. 1.1 handling of linky links.
 */
static void
write_gpx_url(const waypoint* waypointp)
{
  char* tmp_ent;

  if (waypointp->url == NULL) {
    return;
  }

  if (gpx_wversion_num > 10) {
    url_link* tail;
    for (tail = (url_link*)&waypointp->url_next; tail; tail = tail->url_next) {
      tmp_ent = xml_entitize(tail->url);
      gbfprintf(ofd, "  <link href=\"%s%s\">\n",
                urlbase ? urlbase : "", tmp_ent);
      write_optional_xml_entity(ofd, "  ", "text",
                                tail->url_link_text);
      gbfprintf(ofd, "  </link>\n");
      xfree(tmp_ent);
    }
  } else {
    tmp_ent = xml_entitize(waypointp->url);
    gbfprintf(ofd, "  <url>%s%s</url>\n",
              urlbase ? urlbase : "", tmp_ent);
    write_optional_xml_entity(ofd, "  ", "urlname",
                              waypointp->url_link_text);
    xfree(tmp_ent);
  }
}

/*
 * Write optional accuracy information for a given (way|track|route)point
 * to the output stream.  Done in one place since it's common for all three.
 * Order counts.
 */
static void
gpx_write_common_acc(const waypoint* waypointp, const char* indent)
{
  char* fix = NULL;

  switch (waypointp->fix) {
  case fix_2d:
    fix = "2d";
    break;
  case fix_3d:
    fix = "3d";
    break;
  case fix_dgps:
    fix = "dgps";
    break;
  case fix_pps:
    fix = "pps";
    break;
  case fix_none:
    fix = "none";
    break;
    /* GPX spec says omit if we don't know. */
  case fix_unknown:
  default:
    break;
  }
  if (fix) {
    gbfprintf(ofd, "%s<fix>%s</fix>\n", indent, fix);
  }
  if (waypointp->sat > 0) {
    gbfprintf(ofd, "%s<sat>%d</sat>\n", indent, waypointp->sat);
  }
  if (waypointp->hdop) {
    gbfprintf(ofd, "%s<hdop>%f</hdop>\n", indent, waypointp->hdop);
  }
  if (waypointp->vdop) {
    gbfprintf(ofd, "%s<vdop>%f</vdop>\n", indent, waypointp->vdop);
  }
  if (waypointp->pdop) {
    gbfprintf(ofd, "%s<pdop>%f</pdop>\n", indent, waypointp->pdop);
  }
}

static void
gpx_write_common_position(const waypoint* waypointp, const char* indent)
{
  if (waypointp->altitude != unknown_alt) {
    gbfprintf(ofd, "%s<ele>%f</ele>\n",
              indent, waypointp->altitude);
  }
  if (waypointp->creation_time) {
    xml_write_time(ofd, waypointp->creation_time, waypointp->microseconds, "time");
  }
}

static void
gpx_write_common_description(const waypoint* waypointp, const char* indent,
                             const char* oname)
{
  write_optional_xml_entity(ofd, indent, "name", oname);
  write_optional_xml_entity(ofd, indent, "cmt", waypointp->description);
  if (waypointp->notes && waypointp->notes[0]) {
    write_xml_entity(ofd, indent, "desc", waypointp->notes);
  } else {
    write_optional_xml_entity(ofd, indent, "desc", waypointp->description);
  }
  write_gpx_url(waypointp);
  write_optional_xml_entity(ofd, indent , "sym", waypointp->icon_descr);
}

static void
gpx_waypt_pr(const waypoint* waypointp)
{
  const char* oname;
  char* odesc;
  fs_xml* fs_gpx;
  garmin_fs_t* gmsd;	/* gARmIN sPECIAL dATA */

  /*
   * Desparation time, try very hard to get a good shortname
   */
  odesc = waypointp->notes;
  if (!odesc) {
    odesc = waypointp->description;
  }
  if (!odesc) {
    odesc = waypointp->shortname;
  }

  oname = global_opts.synthesize_shortnames ?
          mkshort(mkshort_handle, odesc) :
          waypointp->shortname;

  gbfprintf(ofd, "<wpt lat=\"" FLT_FMT "\" lon=\"" FLT_FMT "\">\n",
            waypointp->latitude,
            waypointp->longitude);

  gpx_write_common_position(waypointp, "  ");
  gpx_write_common_description(waypointp, "  ", oname);
  gpx_write_common_acc(waypointp, "  ");

  fs_gpx = (fs_xml*)fs_chain_find(waypointp->fs, FS_GPX);
  gmsd = GMSD_FIND(waypointp);
  if (fs_gpx) {
    if (! gmsd) {
      fprint_xml_chain(fs_gpx->tag, waypointp);
    }
  }
  if (gmsd && (gpx_wversion_num > 10)) {
    /* MapSource doesn't accepts extensions from 1.0 */
    garmin_fs_xml_fprint(ofd, waypointp);
  }
  gbfprintf(ofd, "</wpt>\n");
}

static void
gpx_track_hdr(const route_head* rte)
{
  fs_xml* fs_gpx;

  gbfprintf(ofd, "<trk>\n");
  write_optional_xml_entity(ofd, "  ", "name", rte->rte_name);
  write_optional_xml_entity(ofd, "  ", "desc", rte->rte_desc);
  if (rte->rte_num) {
    gbfprintf(ofd, "<number>%d</number>\n", rte->rte_num);
  }
  gbfprintf(ofd, "<trkseg>\n");

  fs_gpx = (fs_xml*)fs_chain_find(rte->fs, FS_GPX);
  if (fs_gpx) {
    fprint_xml_chain(fs_gpx->tag, NULL);
  }
}

static void
gpx_track_disp(const waypoint* waypointp)
{
  fs_xml* fs_gpx;

  gbfprintf(ofd, "<trkpt lat=\"" FLT_FMT_T "\" lon=\"" FLT_FMT_T "\">\n",
            waypointp->latitude,
            waypointp->longitude);

  gpx_write_common_position(waypointp, "  ");

  /* These were accidentally removed from 1.1 */
  if (gpx_wversion_num == 10) {
    if WAYPT_HAS(waypointp, course) {
      gbfprintf(ofd, "  <course>%f</course>\n",
                waypointp->course);
    }
    if WAYPT_HAS(waypointp, speed) {
      gbfprintf(ofd, "  <speed>%f</speed>\n",
                waypointp->speed);
    }
  }

  /* GPX doesn't require a name on output, so if we made one up
   * on input, we might as well say nothing.
   */
  gpx_write_common_description(waypointp, "  ",
                               waypointp->wpt_flags.shortname_is_synthetic ?
                               NULL : waypointp->shortname);
  gpx_write_common_acc(waypointp, "  ");

  fs_gpx = (fs_xml*)fs_chain_find(waypointp->fs, FS_GPX);
  if (fs_gpx) {
    fprint_xml_chain(fs_gpx->tag, waypointp);
  }

  gbfprintf(ofd, "</trkpt>\n");
}

static void
gpx_track_tlr(const route_head* rte)
{
  gbfprintf(ofd, "</trkseg>\n");
  gbfprintf(ofd, "</trk>\n");
}

static
void gpx_track_pr()
{
  track_disp_all(gpx_track_hdr, gpx_track_tlr, gpx_track_disp);
}

static void
gpx_route_hdr(const route_head* rte)
{
  fs_xml* fs_gpx;

  gbfprintf(ofd, "<rte>\n");
  write_optional_xml_entity(ofd, "  ", "name", rte->rte_name);
  write_optional_xml_entity(ofd, "  ", "desc", rte->rte_desc);
  if (rte->rte_num) {
    gbfprintf(ofd, "  <number>%d</number>\n", rte->rte_num);
  }

  fs_gpx = (fs_xml*)fs_chain_find(rte->fs, FS_GPX);
  if (fs_gpx) {
    fprint_xml_chain(fs_gpx->tag, NULL);
  }
}

static void
gpx_route_disp(const waypoint* waypointp)
{
  fs_xml* fs_gpx;

  gbfprintf(ofd, "  <rtept lat=\"" FLT_FMT_R "\" lon=\"" FLT_FMT_R "\">\n",
            waypointp->latitude,
            waypointp->longitude);

  gpx_write_common_position(waypointp, "    ");
  gpx_write_common_description(waypointp, "    ", waypointp->shortname);
  gpx_write_common_acc(waypointp, "    ");

  fs_gpx = (fs_xml*)fs_chain_find(waypointp->fs, FS_GPX);
  if (fs_gpx) {
    fprint_xml_chain(fs_gpx->tag, waypointp);
  }

  gbfprintf(ofd, "  </rtept>\n");
}

static void
gpx_route_tlr(const route_head* rte)
{
  gbfprintf(ofd, "</rte>\n");
}

static
void gpx_route_pr()
{
  /* output routes */
  route_disp_all(gpx_route_hdr, gpx_route_tlr, gpx_route_disp);
}

static void
gpx_waypt_bound_calc(const waypoint* waypointp)
{
  waypt_add_to_bounds(&all_bounds, waypointp);
}

static void
gpx_write_bounds(void)
{
  waypt_init_bounds(&all_bounds);

  waypt_disp_all(gpx_waypt_bound_calc);
  route_disp_all(NULL, NULL, gpx_waypt_bound_calc);
  track_disp_all(NULL, NULL, gpx_waypt_bound_calc);

  if (waypt_bounds_valid(&all_bounds)) {
    gbfprintf(ofd, "<bounds minlat=\"%0.9f\" minlon=\"%0.9f\" "
              "maxlat=\"%0.9f\" maxlon=\"%0.9f\"/>\n",
              all_bounds.min_lat, all_bounds.min_lon,
              all_bounds.max_lat, all_bounds.max_lon);
  }
}

static void
gpx_write(void)
{
  time_t now = 0;
  int short_length;

  gpx_wversion_num = strtod(gpx_wversion, NULL) * 10;

  if (gpx_wversion_num <= 0) {
    fatal(MYNAME ": gpx version number of '%s' not valid.\n", gpx_wversion);
  }

  now = current_time();

  short_length = atoi(snlen);

  if (suppresswhite) {
    setshort_whitespace_ok(mkshort_handle, 0);
  }

  setshort_length(mkshort_handle, short_length);

  gbfprintf(ofd, "<?xml version=\"1.0\" encoding=\"%s\"?>\n", global_opts.charset_name);
  gbfprintf(ofd, "<gpx\n version=\"%s\"\n", gpx_wversion);
  gbfprintf(ofd, "creator=\"" CREATOR_NAME_URL "\"\n");
  gbfprintf(ofd, "xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n");
  gbfprintf(ofd, "xmlns=\"http://www.topografix.com/GPX/%c/%c\"\n", gpx_wversion[0], gpx_wversion[2]);
  if (xsi_schema_loc) {
    gbfprintf(ofd, "xsi:schemaLocation=\"%s\">\n", xsi_schema_loc);
  } else {
    gbfprintf(ofd,
              "xsi:schemaLocation=" DEFAULT_XSI_SCHEMA_LOC_FMT">\n",
              gpx_wversion[0], gpx_wversion[2],
              gpx_wversion[0], gpx_wversion[2]);
  }

  if (gpx_wversion_num > 10) {
    gbfprintf(ofd, "<metadata>\n");
  }
  gpx_write_gdata(&gpx_global->name, "name");
  gpx_write_gdata(&gpx_global->desc, "desc");
  /* In GPX 1.1, author changed from a string to a PersonType.
   * since it's optional, we just drop it instead of rewriting it.
   */
  if (gpx_wversion_num < 11) {
    gpx_write_gdata(&gpx_global->author, "author");
  }
  gpx_write_gdata(&gpx_global->email, "email");
  gpx_write_gdata(&gpx_global->url, "url");
  gpx_write_gdata(&gpx_global->urlname, "urlname");
  xml_write_time(ofd, now, 0, "time");
  gpx_write_gdata(&gpx_global->keywords, "keywords");

  gpx_write_bounds();

  if (gpx_wversion_num > 10) {
    gbfprintf(ofd, "</metadata>\n");
  }

  waypt_disp_all(gpx_waypt_pr);
  gpx_route_pr();
  gpx_track_pr();

  gbfprintf(ofd, "</gpx>\n");
}


static void
gpx_free_gpx_global(void)
{
  gpx_rm_from_global(&gpx_global->name);
  gpx_rm_from_global(&gpx_global->desc);
  gpx_rm_from_global(&gpx_global->author);
  gpx_rm_from_global(&gpx_global->email);
  gpx_rm_from_global(&gpx_global->url);
  gpx_rm_from_global(&gpx_global->urlname);
  gpx_rm_from_global(&gpx_global->keywords);
  xfree(gpx_global);
}

static void
gpx_exit(void)
{
  if (xsi_schema_loc) {
    xfree(xsi_schema_loc);
    xsi_schema_loc = NULL;
  }

  if (gpx_global) {
    gpx_free_gpx_global();
    gpx_global = NULL;
  }
}

static
arglist_t gpx_args[] = {
  {
    "snlen", &snlen, "Length of generated shortnames",
    "32", ARGTYPE_INT, "1", NULL
  },
  {
    "suppresswhite", &suppresswhite,
    "No whitespace in generated shortnames",
    NULL, ARGTYPE_BOOL, ARG_NOMINMAX
  },
  {
    "logpoint", &opt_logpoint,
    "Create waypoints from geocache log entries",
    NULL, ARGTYPE_BOOL, ARG_NOMINMAX
  },
  {
    "urlbase", &urlbase, "Base URL for link tag in output",
    NULL, ARGTYPE_STRING, ARG_NOMINMAX
  },
  {
    "gpxver", &gpx_wversion, "Target GPX version for output",
    "1.0", ARGTYPE_STRING, ARG_NOMINMAX
  },
  ARG_TERMINATOR
};

ff_vecs_t gpx_vecs = {
  ff_type_file,
  FF_CAP_RW_ALL,
  gpx_rd_init,
  gpx_wr_init,
  gpx_rd_deinit,
  gpx_wr_deinit,
  gpx_read,
  gpx_write,
  gpx_exit,
  gpx_args,
  CET_CHARSET_UTF8, 0	/* non-fixed to create non UTF-8 XML's for testing | CET-REVIEW */
};
#endif
