/*
    Garmin USB layer - OS independent component.

    Copyright (C) 2006 Robert Lipe, robertlipe@usa.net

    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 "jeeps/gps.h"
#include "jeeps/garminusb.h"
#include "jeeps/gpsusbcommon.h"

/*
 * This receive logic is a little convoluted as we go to some efforts here
 * to hide most of the differences between the bulk only and bulk-interrupt
 * protocols as exhibited in the handhelds and dashtops.
 */

static enum {
  rs_fromintr,
  rs_frombulk
} receive_state;

static gusb_llops_t* gusb_llops;

/* Decide when to truncate packets for debug output */
#define DEBUG_THRESH  ((global_opts.debug_level < 5) && (i > 10))

/* Called from OS layer to register its low-level entry points. */
void
gusb_register_ll(gusb_llops_t* p)
{
  gusb_llops = p;
}

int
gusb_close(gpsdevh* dh, bool exit_lib)
{
  garmin_usb_packet scratch;

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

  switch (receive_state) {
  case rs_frombulk:
    gusb_cmd_get(&scratch, sizeof(scratch));
    break;
  default:
    break;
  }

  gusb_llops->llop_close(dh, exit_lib);
  return 1;

#if BOOGER
  garmin_usb_packet scratch = {0};
  abort();
  switch (receive_state) {
  case rs_frombulk:
    gusb_cmd_get(dh, &scratch, sizeof(scratch));
    break;
  }

  return 1;
#endif
}


int
gusb_cmd_get(garmin_usb_packet* ibuf, size_t sz)
{
  int rv;
  auto* buf = (unsigned char*) &ibuf->dbuf;
  unsigned short pkt_id;
top:
  switch (receive_state) {
  case rs_fromintr:
    rv = gusb_llops->llop_get_intr(ibuf, sz);
    break;
  case rs_frombulk:
    rv = gusb_llops->llop_get_bulk(ibuf, sz);
    break;
  default:
    fatal("Unknown receiver state %d\n", receive_state);
  }

  pkt_id = le_read16(&ibuf->gusb_pkt.pkt_id);
  if (gps_show_bytes) {
    int i;
    const char* m1, *m2;
    unsigned short pkttype = le_read16(&ibuf->gusb_pkt.databuf[0]);

    GPS_Diag("RX (%s) [%d]:",
             receive_state == rs_fromintr ? "intr" : "bulk", rv);

    for (i=0; i<rv; i++) {
      if (DEBUG_THRESH) {
        GPS_Diag("[...]");
        break;
      }
      GPS_Diag("%02x ", buf[i]);
    }

    for (i=0; i<rv; i++) {
      if (DEBUG_THRESH) {
        GPS_Diag("[...]");
        break;
      }
      int c = buf[i];
      GPS_Diag("%c", isascii(c) && isalnum(c) ? c : '.');
    }

    m1 = Get_Pkt_Type(pkt_id, pkttype, &m2);
    if ((rv == 0)  && (receive_state == rs_frombulk)) {
      m1= "RET2INTR";
      m2=nullptr;
    }
    GPS_Diag("(%-8s%s)\n", m1, m2 ? m2 : "");
  }

  /* Adjust internal state and retry the read */
  if ((rv > 0) && (pkt_id == GUSB_REQUEST_BULK)) {
    receive_state = rs_frombulk;
    goto top;
  }
  /*
   * If we were reading from the bulk pipe and we just got
   * a zero request, adjust our internal state.
   * It's tempting to retry the read here to hide this "stray"
   * packet from our callers, but that only works when you know
   * there's another packet coming.   That works in every case
   * except the A000 discovery sequence.
  */
  if ((receive_state == rs_frombulk) && (rv <= 0)) {
    receive_state = rs_fromintr;
  }

  return rv;
}

int
gusb_cmd_send(const garmin_usb_packet* opkt, size_t sz)
{
  const auto* obuf = opkt->dbuf;
  const char* m2;

  unsigned int rv = gusb_llops->llop_send(opkt, sz);

  if (gps_show_bytes) {
    const unsigned short pkttype = le_read16(&opkt->gusb_pkt.databuf[0]);
    const unsigned short pkt_id = le_read16(&opkt->gusb_pkt.pkt_id);
    GPS_Diag("TX [%zu]:", sz);

    for (unsigned int i=0; i<sz; ++i) {
      GPS_Diag("%02x ", obuf[i]);
    }

    for (unsigned int i=0; i<sz; ++i) {
      int c = obuf[i];
      GPS_Diag("%c", isascii(c) && isalnum(c) ? c : '.');
    }

    const char* m1 = Get_Pkt_Type(pkt_id, pkttype, &m2);

    GPS_Diag("(%-8s%s)\n", m1, m2 ? m2 : "");
  }
  /*
   * Recursion, when used in a disciplined way, can be our friend.
   *
   * The Garmin protocol requires that packets that are exactly
   * a multiple of the max tx size be followed by a zero length
   * packet.  Do that here so we can see it in debugging traces.
   */

  if (sz && !(sz % gusb_llops->max_tx_size)) {
    gusb_cmd_send(opkt, 0);
  }

  return (rv);
}

void
gusb_list_units()
{
  int i;

  for (i = 0; i < GUSB_MAX_UNITS; i++) {
    if (garmin_unit_info[i].serial_number) {
      printf("%d %lu %lu %s\n", i,
             garmin_unit_info[i].serial_number,
             garmin_unit_info[i].unit_id,
             garmin_unit_info[i].product_identifier
            );
    }
  }
}

void
gusb_id_unit(garmin_unit_info_t* gu)
{
  static const unsigned char  oid[12] =
  {20, 0, 0, 0, 0xfe, 0, 0, 0, 0, 0, 0, 0};
  garmin_usb_packet iresp;
  int i;

  gusb_cmd_send(reinterpret_cast<const garmin_usb_packet*>(oid), sizeof(oid));

  for (i = 0; i < 25; i++) {
    iresp.gusb_pkt.type = 0;
    if (gusb_cmd_get(&iresp, sizeof(iresp)) < 0) {
      return;
    }
    if (le_read16(iresp.gusb_pkt.pkt_id) == 0xff) {
      gu->product_identifier = xstrdup((char*) iresp.gusb_pkt.databuf+4);
      gu->unit_id = le_read16(iresp.gusb_pkt.databuf+0);
      gu->unit_version = le_read16(iresp.gusb_pkt.databuf+2);
    }
    /*
     * My goodness, this is fragile.  During command syncup,
     * we need to know if we're at the end.  The 0xfd packet
     * is promised by Garmin engineering to be the last.
     */
    if (le_read16(iresp.gusb_pkt.pkt_id) == 0xfd) {
      return;
    }
  }
  fatal("Unable to sync with Garmin USB device in %d attempts.", i);
}

void
gusb_syncup()
{
  static int unit_number;
  static const char  oinit[12] =
  {0, 0, 0, 0, GUSB_SESSION_START, 0, 0, 0, 0, 0, 0, 0};
  garmin_usb_packet iresp;
  int i;

  /*
   * This is our first communication with the unit.
   */
  receive_state = rs_fromintr;

  for (i = 0; i < 25; i++) {
    le_write16(&iresp.gusb_pkt.pkt_id, 0);
    le_write32(&iresp.gusb_pkt.datasz, 0);
    le_write32(&iresp.gusb_pkt.databuf, 0);

    gusb_cmd_send((const garmin_usb_packet*) oinit, sizeof(oinit));
    gusb_cmd_get(&iresp, sizeof(iresp));

    if ((le_read16(iresp.gusb_pkt.pkt_id) == GUSB_SESSION_ACK) &&
        (le_read32(iresp.gusb_pkt.datasz) == 4)) {
      unsigned serial_number = le_read32(iresp.gusb_pkt.databuf);
      garmin_unit_info[unit_number].serial_number = serial_number;
      gusb_id_unit(&garmin_unit_info[unit_number]);

      unit_number++;

      return;
    }
  }
  fatal("Unable to establish USB syncup\n");
}
