/*

    Handle Geogrid-Viewer binary overlay file format (.ovl)

    Copyright (C) 2016-2020 Ralf Horstmann <ralf@ackstorm.de>
    Copyright (C) 2016-2020 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 <QByteArray>
#include <QDataStream>
#include <QDebug>
#include <QFile>
#include <QtEndian>

#include "ggv_bin.h"

#define MYNAME "ggv_bin"

/***************************************************************************
 *           local helper functions                                        *
 ***************************************************************************/

void
GgvBinFormat::ggv_bin_read_bytes(QDataStream& stream, QByteArray& buf, int len, const char* descr)
{
  if (len < 0) {
    fatal(MYNAME ": Read error, negative len (%s)\n", descr ? descr : "");
  }
  buf.resize(len);
  if (stream.readRawData(buf.data(), len) != len || stream.status() != QDataStream::Ok) {
    fatal(MYNAME ": Read error (%s)\n", descr ? descr : "");
  }
}

quint16
GgvBinFormat::ggv_bin_read16(QDataStream& stream, const char* descr)
{
  quint16 res;
  stream >> res;
  if (stream.status() != QDataStream::Ok) {
    fatal(MYNAME ": Read error (%s)\n", descr ? descr : "");
  }
  if (global_opts.debug_level > 1) {
    qDebug("ovl: %-15s %5u (0x%04x)", descr, res, res);
  }
  return res;
}

quint32
GgvBinFormat::ggv_bin_read32(QDataStream& stream, const char* descr)
{
  quint32 res;
  stream >> res;
  if (stream.status() != QDataStream::Ok) {
    fatal(MYNAME ": Read error (%s)\n", descr ? descr : "");
  }
  if (global_opts.debug_level > 1) {
    if ((res & 0xFFFF0000) == 0) {
      qDebug("ovl: %-15s %5u (0x%08x)", descr, res, res);
    } else {
      qDebug("ovl: %-15s       (0x%08x)", descr, res);
    }
  }
  return res;
}

void
GgvBinFormat::ggv_bin_read_text16(QDataStream& stream, QByteArray& buf, const char* descr)
{
  quint16 len = ggv_bin_read16(stream, descr);
  ggv_bin_read_bytes(stream, buf, len, descr);
  buf.append('\0');
  if (global_opts.debug_level > 1) {
    qDebug() << "ovl: text =" << QString::fromLatin1(buf.constData()).simplified();
  }
}

void
GgvBinFormat::ggv_bin_read_text32(QDataStream& stream, QByteArray& buf, const char* descr)
{
  quint32 len = ggv_bin_read32(stream, descr);
  // The following check prevents passing an unsigned int with a
  // value greater than INT32_MAX to a signed int parameter in
  // ggv_bin_read_bytes later on. If this happens, the file is
  // almost certainly corrupted.
  if (len > INT32_MAX) {
    fatal(MYNAME ": Read error, max len exceeded (%s)\n", descr ? descr : "");
  }
  ggv_bin_read_bytes(stream, buf, len, descr);
  buf.append('\0');
  if (global_opts.debug_level > 1) {
    qDebug() << "ovl: text =" << QString::fromLatin1(buf.constData()).simplified();
  }
}

double
GgvBinFormat::ggv_bin_read_double(QDataStream& stream, const char* descr)
{
  double res;
  stream >> res;
  if (stream.status() != QDataStream::Ok) {
    fatal(MYNAME ": Read error (%s)\n", descr ? descr : "");
  }
  return res;
}

/***************************************************************************
 *            OVL Version 2.0                                              *
 ***************************************************************************/

void
GgvBinFormat::ggv_bin_read_v2(QDataStream& stream)
{
  QByteArray buf;
  QString track_name;
  QString waypt_name;
  route_head* ggv_bin_track;
  Waypoint* wpt;
  double lon, lat;
  quint16 line_points;

  // header length is usually either 0x90 or 0x00
  quint16 header_len = ggv_bin_read16(stream, "map name len");
  if (header_len > 0) {
    ggv_bin_read_bytes(stream, buf, header_len, "map name");
    buf.remove(0,4);
    buf.append('\0');
    if (global_opts.debug_level > 1) {
      qDebug() << "ovl: name =" << buf.constData();
    }
  }

  while (!stream.atEnd()) {
    track_name.clear();

    if (global_opts.debug_level > 1) {
      qDebug("------------------------------------ 0x%llx", stream.device()->pos());
    }

    auto entry_pos = stream.device()->pos();
    quint16 entry_type = ggv_bin_read16(stream, "entry type");
    ggv_bin_read16(stream, "entry group");
    ggv_bin_read16(stream, "entry zoom");
    quint16 entry_subtype = ggv_bin_read16(stream, "entry subtype");

    if (entry_subtype != 1) {
      ggv_bin_read_text32(stream, buf, "text len");
      track_name = QString::fromLatin1(buf.constData()).simplified();
    }

    switch (entry_type) {
    case 0x02:
      // text
      ggv_bin_read16(stream, "text color");
      ggv_bin_read16(stream, "text size");
      ggv_bin_read16(stream, "text trans");
      ggv_bin_read16(stream, "text font");
      ggv_bin_read16(stream, "text angle");
      lon = ggv_bin_read_double(stream, "text lon");
      lat = ggv_bin_read_double(stream, "text lat");
      ggv_bin_read_text16(stream, buf, "text label");
      waypt_name = QString::fromLatin1(buf.constData()).simplified();
      wpt = new Waypoint;
      wpt->longitude = lon;
      wpt->latitude = lat;
      wpt->description = waypt_name;
      waypt_add(wpt);
      break;
    case 0x03:
    // line
    case 0x04:
      // area
      ggv_bin_read16(stream, "line color");
      ggv_bin_read16(stream, "line width");
      ggv_bin_read16(stream, "line type");
      line_points = ggv_bin_read16(stream, "line points");
      ggv_bin_track = new route_head;
      track_add_head(ggv_bin_track);
      if (! track_name.isEmpty()) {
        ggv_bin_track->rte_name = track_name;
      }

      for (int i = 1; i <= line_points; i++) {
        lon = ggv_bin_read_double(stream, "line lon");
        lat = ggv_bin_read_double(stream, "line lat");
        wpt = new Waypoint;
        wpt->longitude = lon;
        wpt->latitude = lat;
        track_add_wpt(ggv_bin_track, wpt);
      }
      break;
    case 0x05:
    // rectangle
    case 0x06:
    // circle
    case 0x07:
      // triangle
      ggv_bin_read16(stream, "geom color");
      ggv_bin_read16(stream, "geom prop1");
      ggv_bin_read16(stream, "geom prop2");
      ggv_bin_read16(stream, "geom angle");
      ggv_bin_read16(stream, "geom stroke");
      ggv_bin_read16(stream, "geom area");
      ggv_bin_read_double(stream, "geom lon");
      ggv_bin_read_double(stream, "geom lat");
      break;
    case 0x09:
      ggv_bin_read16(stream, "bmp color");
      ggv_bin_read16(stream, "bmp prop1");
      ggv_bin_read16(stream, "bmp prop2");
      ggv_bin_read16(stream, "bmp prop3");
      ggv_bin_read_double(stream, "bmp lon");
      ggv_bin_read_double(stream, "bmp lat");
      ggv_bin_read_text32(stream, buf, "bmp data");
      break;
    default:
      fatal(MYNAME ": Unknown entry type (0x%hx, pos=0x%llx) \n", entry_type, entry_pos);
    }
  }
}

/***************************************************************************
 *           OVL Version 3.0 and 4.0                                       *
 ***************************************************************************/

void
GgvBinFormat::ggv_bin_read_v34_header(QDataStream& stream, quint32& number_labels, quint32& number_records)
{
  QByteArray buf;

  ggv_bin_read_bytes(stream, buf, 8, "unknown");
  number_labels = ggv_bin_read32(stream, "num labels");
  number_records = ggv_bin_read32(stream, "num records");
  ggv_bin_read_text16(stream, buf, "text label");
  ggv_bin_read16(stream, "unknown");
  ggv_bin_read16(stream, "unknown");
  // 8 bytes ending with 1E 00, contains len of header block
  ggv_bin_read16(stream, "unknown");
  quint16 header_len = ggv_bin_read16(stream, "header len");
  ggv_bin_read16(stream, "unknown");
  ggv_bin_read16(stream, "unknown");
  if (header_len > 0) {
    ggv_bin_read_bytes(stream, buf, header_len, "map name");
    buf.remove(0,4);
    buf.append('\0');
    if (global_opts.debug_level > 1) {
      qDebug() << "ovl: name =" << buf.constData();
    }
  }
}

void
GgvBinFormat::ggv_bin_read_v34_label(QDataStream& stream)
{
  QByteArray buf;

  if (global_opts.debug_level > 1) {
    qDebug("------------------------------------ 0x%llx", stream.device()->pos());
  }
  ggv_bin_read_bytes(stream, buf, 0x08, "label header");
  ggv_bin_read_bytes(stream, buf, 0x14, "label number");
  ggv_bin_read_text16(stream, buf, "label text");
  ggv_bin_read16(stream, "label flag1");
  ggv_bin_read16(stream, "label flag2");
}

QString
GgvBinFormat::ggv_bin_read_v34_common(QDataStream& stream)
{
  QByteArray buf;

  ggv_bin_read16(stream, "entry group");
  ggv_bin_read16(stream, "entry prop2");
  ggv_bin_read16(stream, "entry prop3");
  ggv_bin_read16(stream, "entry prop4");
  ggv_bin_read16(stream, "entry prop5");
  ggv_bin_read16(stream, "entry prop6");
  ggv_bin_read16(stream, "entry prop7");
  ggv_bin_read16(stream, "entry prop8");
  ggv_bin_read16(stream, "entry zoom");
  ggv_bin_read16(stream, "entry prop10");
  ggv_bin_read_text16(stream, buf, "entry txt");
  QString res = QString::fromLatin1(buf.constData()).simplified();
  quint16 type1 = ggv_bin_read16(stream, "entry type1");
  if (type1 != 1) {
    ggv_bin_read_text32(stream, buf, "entry object");
  }
  quint16 type2 = ggv_bin_read16(stream, "entry type2");
  if (type2 != 1) {
    ggv_bin_read_text32(stream, buf, "entry object");
  }
  return res;
}

void
GgvBinFormat::ggv_bin_read_v34_record(QDataStream& stream)
{
  QByteArray buf;
  Waypoint* wpt;
  route_head* ggv_bin_track;
  quint32 bmp_len;
  quint16 line_points;
  double lon, lat;

  if (global_opts.debug_level > 1) {
    qDebug("------------------------------------ 0x%llx", stream.device()->pos());
  }

  quint16 entry_type = ggv_bin_read16(stream, "entry type");
  QString label = ggv_bin_read_v34_common(stream);

  switch (entry_type) {
  case 0x02:
    // text
    ggv_bin_read16(stream, "text prop1");
    ggv_bin_read32(stream, "text prop2");
    ggv_bin_read16(stream, "text prop3");
    ggv_bin_read32(stream, "text prop4");
    ggv_bin_read16(stream, "text ltype");
    ggv_bin_read16(stream, "text angle");
    ggv_bin_read16(stream, "text size");
    ggv_bin_read16(stream, "text area");
    lon = ggv_bin_read_double(stream, "text lon");
    lat = ggv_bin_read_double(stream, "text lat");
    ggv_bin_read_double(stream, "text unk");
    ggv_bin_read_text16(stream, buf, "text label");
    wpt = new Waypoint;
    wpt->longitude = lon;
    wpt->latitude = lat;
    wpt->description = QString::fromLatin1(buf.constData()).simplified();
    waypt_add(wpt);
    break;
  case 0x03:
  case 0x04:
  // area
  case 0x17:
    // line
    ggv_bin_track = new route_head;
    track_add_head(ggv_bin_track);

    if (! label.isEmpty()) {
      ggv_bin_track->rte_name = label;
    }

    ggv_bin_read16(stream, "line prop1");
    ggv_bin_read32(stream, "line prop2");
    ggv_bin_read16(stream, "line prop3");
    ggv_bin_read32(stream, "line color");
    ggv_bin_read16(stream, "line size");
    ggv_bin_read16(stream, "line stroke");
    line_points = ggv_bin_read16(stream, "line points");
    if (entry_type == 0x04) {
      // found in example.ovl generated by Geogrid-Viewer 1.0
      ggv_bin_read16(stream, "line pad");
    }

    for (int i=1; i <= line_points; i++) {
      lon = ggv_bin_read_double(stream, "line lon");
      lat = ggv_bin_read_double(stream, "line lat");
      ggv_bin_read_double(stream, "line unk");
      wpt = new Waypoint;
      wpt->longitude = lon;
      wpt->latitude = lat;
      track_add_wpt(ggv_bin_track, wpt);
    }
    break;
  case 0x05:
  case 0x06:
  case 0x07:
    // circle
    ggv_bin_read16(stream, "circle prop1");
    ggv_bin_read32(stream, "circle prop2");
    ggv_bin_read16(stream, "circle prop3");
    ggv_bin_read32(stream, "circle color");
    ggv_bin_read32(stream, "circle prop5");
    ggv_bin_read32(stream, "circle prop6");
    ggv_bin_read16(stream, "circle ltype");
    ggv_bin_read16(stream, "circle angle");
    ggv_bin_read16(stream, "circle size");
    ggv_bin_read16(stream, "circle area");
    ggv_bin_read_double(stream, "circle lon");
    ggv_bin_read_double(stream, "circle lat");
    ggv_bin_read_double(stream, "circle unk");
    break;
  case 0x09:
    // bmp
    ggv_bin_read16(stream, "bmp prop1");
    ggv_bin_read32(stream, "bmp prop2");
    ggv_bin_read16(stream, "bmp prop3");
    ggv_bin_read32(stream, "bmp prop4");
    ggv_bin_read32(stream, "bmp prop5");
    ggv_bin_read32(stream, "bmp prop6");
    ggv_bin_read_double(stream, "bmp lon");
    ggv_bin_read_double(stream, "bmp lat");
    ggv_bin_read_double(stream, "bmp unk");
    bmp_len = ggv_bin_read32(stream, "bmp len");
    // The following check prevents passing an unsigned int with a
    // value greater than INT32_MAX to a signed int parameter in
    // ggv_bin_read_bytes later on. If this happens, the file is
    // almost certainly corrupted.
    if (bmp_len > INT32_MAX) {
      fatal(MYNAME ": Read error, max bmp_len exceeded\n");
    }
    ggv_bin_read16(stream, "bmp prop");
    ggv_bin_read_bytes(stream, buf, bmp_len, "bmp data");
    break;
  default:
    fatal(MYNAME ": Unsupported type: %x\n", entry_type);
  }
}

void
GgvBinFormat::ggv_bin_read_v34(QDataStream& stream)
{
  QByteArray buf;
  quint32 label_count;
  quint32 record_count;

  while (!stream.atEnd()) {
    ggv_bin_read_v34_header(stream, label_count, record_count);

    if (label_count && !stream.atEnd()) {
      if (global_opts.debug_level > 1) {
        qDebug("-----labels------------------------- 0x%llx", stream.device()->pos());
      }
      for (unsigned int i = 0; i < label_count; i++) {
        ggv_bin_read_v34_label(stream);
      }
    }

    if (record_count && !stream.atEnd()) {
      if (global_opts.debug_level > 1) {
        qDebug("-----records------------------------ 0x%llx", stream.device()->pos());
      }
      for (unsigned int i = 0; i < record_count; i++) {
        ggv_bin_read_v34_record(stream);
      }
    }

    if (!stream.atEnd()) {
      if (global_opts.debug_level > 1) {
        qDebug("------------------------------------ 0x%llx", stream.device()->pos());
      }
      // we just skip over the next magic bytes without checking they
      // contain the correct string. This is consistent with what I
      // believe GGV does
      ggv_bin_read_bytes(stream, buf, 23, "magicbytes");
      if (global_opts.debug_level > 1) {
        qDebug() << "ovl: header = " << buf.constData();
      }
    }
  }

  if (global_opts.debug_level > 1) {
    qDebug("fpos: 0x%llx", stream.device()->pos());
    qDebug("size: 0x%llx", stream.device()->size());
  }
}

/***************************************************************************
 *              entry points called by gpsbabel main process               *
 ***************************************************************************/

void
GgvBinFormat::read()
{
  QFile file(read_fname);
  if (!file.open(QIODevice::ReadOnly)) {
    fatal(MYNAME ": Error opening file %s\n", qPrintable(read_fname));
  }

  QDataStream stream(&file);
  stream.setFloatingPointPrecision(QDataStream::DoublePrecision);
  stream.setByteOrder(QDataStream::LittleEndian);

  QByteArray buf;
  ggv_bin_read_bytes(stream, buf, 0x17, "magic");
  buf.append('\0');
  if (global_opts.debug_level > 1) {
    qDebug() << "ovl: header =" << buf.constData();
  }

  if (buf.startsWith("DOMGVCRD Ovlfile V2.0")) {
    ggv_bin_read_v2(stream);
  } else if (buf.startsWith("DOMGVCRD Ovlfile V3.0")) {
    ggv_bin_read_v34(stream);
  } else if (buf.startsWith("DOMGVCRD Ovlfile V4.0")) {
    ggv_bin_read_v34(stream);
  } else {
    fatal(MYNAME ": Unsupported file format\n");
  }

  file.close();
}

void
GgvBinFormat::rd_init(const QString& fname)
{
  read_fname = fname;
}

void
GgvBinFormat::rd_deinit()
{
  read_fname.clear();
}

