// Interface to WSPRnet website
//
// by Edson Pereira - PY2SDR

#include "wsprnet.h"

#include <cmath>

#include <QTimer>
#include <QFile>
#include <QRegExp>
#include <QRegularExpression>
#include <QNetworkAccessManager>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QUrl>
#include <QDebug>

#include "moc_wsprnet.cpp"

namespace
{
  char const * const wsprNetUrl = "http://wsprnet.org/post/";
  //char const * const wsprNetUrl = "http://127.0.0.1:5000/post/";

  //
  // tested with this python REST mock of WSPRNet.org
  //
  /*
# Mock WSPRNet.org RESTful API
from flask import Flask, request, url_for
from flask_restful import Resource, Api

app = Flask(__name__)

@app.route ('/post/', methods=['GET', 'POST'])
def spot ():
    if request.method == 'POST':
        print (request.form)
    return "1 spot(s) added"

with app.test_request_context ():
    print (url_for ('spot'))
  */

  // regexp to parse FST4W decodes
  QRegularExpression fst4_re {R"(
  (?<time>\d{4})
  \s+(?<db>[-+]?\d+)
  \s+(?<dt>[-+]?\d+\.\d+)
  \s+(?<freq>\d+)
  \s+`
  \s+<?(?<call>[A-Z0-9/]+)>?(?:\s(?<grid>[A-R]{2}[0-9]{2}(?:[A-X]{2})?))?(?:\s+(?<dBm>\d+))?
)", QRegularExpression::ExtendedPatternSyntaxOption};

  // regexp to parse wspr_spots.txt from wsprd
  //
  // 130223 2256 7    -21 -0.3  14.097090  DU1MGA PK04 37          0    40    0
  // Date   Time Sync dBm  DT   Freq       Msg
  // 1      2    3     4   5     6         -------7------          8     9    10
  QRegularExpression wspr_re(R"(^(\d+)\s+(\d+)\s+(\d+)\s+([+-]?\d+)\s+([+-]?\d+\.\d+)\s+(\d+\.\d+)\s+([^ ].*[^ ])\s+([+-]?\d+)\s+([+-]?\d+)\s+([+-]?\d+))");
};

WSPRNet::WSPRNet (QNetworkAccessManager * manager, QObject *parent)
  : QObject {parent}
  , network_manager_ {manager}
  , spots_to_send_ {0}
{
  connect (network_manager_, &QNetworkAccessManager::finished, this, &WSPRNet::networkReply);
  connect (&upload_timer_, &QTimer::timeout, this, &WSPRNet::work);
}

void WSPRNet::upload (QString const& call, QString const& grid, QString const& rfreq, QString const& tfreq,
                      QString const& mode, float TR_period, QString const& tpct, QString const& dbm,
                      QString const& version, QString const& fileName)
{
  m_call = call;
  m_grid = grid;
  m_rfreq = rfreq;
  m_tfreq = tfreq;
  m_mode = mode;
  TR_period_ = TR_period;
  m_tpct = tpct;
  m_dbm = dbm;
  m_vers = version;
  m_file = fileName;

  // Open the wsprd.out file
  if (m_uploadType != 3)
    {
      QFile wsprdOutFile (fileName);
      if (!wsprdOutFile.open (QIODevice::ReadOnly | QIODevice::Text) || !wsprdOutFile.size ())
        {
          spot_queue_.enqueue (urlEncodeNoSpot ());
          m_uploadType = 1;
        }
      else
        {
          // Read the contents
          while (!wsprdOutFile.atEnd())
            {
              SpotQueue::value_type query;
              if (decodeLine (wsprdOutFile.readLine(), query))
                {
                  // Prevent reporting data ouside of the current frequency band
                  float f = fabs (m_rfreq.toFloat() - query.queryItemValue ("tqrg", QUrl::FullyDecoded).toFloat());
                  if (f < 0.01)     // MHz
                    {
                      spot_queue_.enqueue(urlEncodeSpot (query));
                      m_uploadType = 2;
                    }
                }
            }
        }
    }
  spots_to_send_ = spot_queue_.size ();
  upload_timer_.start (200);
}

void WSPRNet::post (QString const& call, QString const& grid, QString const& rfreq, QString const& tfreq,
                    QString const& mode, float TR_period, QString const& tpct, QString const& dbm,
                    QString const& version, QString const& decode_text)
{
  m_call = call;
  m_grid = grid;
  m_rfreq = rfreq;
  m_tfreq = tfreq;
  m_mode = mode;
  TR_period_ = TR_period;
  m_tpct = tpct;
  m_dbm = dbm;
  m_vers = version;

  if (!decode_text.size ())
    {
      if (!spot_queue_.size ())
        {
          spot_queue_.enqueue (urlEncodeNoSpot ());
          m_uploadType = 3;
        }
    }
  else
    {
      auto const& match = fst4_re.match (decode_text);
      if (match.hasMatch ())
        {
          SpotQueue::value_type query;
          // Prevent reporting data ouside of the current frequency
          // band - removed by G4WJS to accommodate FST4W spots
          // outside of WSPR segments
          auto tqrg = match.captured ("freq").toInt ();
          // if (tqrg >= 1400 && tqrg <= 1600)
            {
              query.addQueryItem ("function", "wspr");
              // use time as at 3/4 of T/R period before current to
              // ensure date is in Rx period
              auto const& date = QDateTime::currentDateTimeUtc ().addSecs (-TR_period * 3. / 4.).date ();
              query.addQueryItem ("date", date.toString ("yyMMdd"));
              query.addQueryItem ("time", match.captured ("time"));
              query.addQueryItem ("sig", match.captured ("db"));
              query.addQueryItem ("dt", match.captured ("dt"));
              query.addQueryItem ("tqrg", QString::number (rfreq.toDouble () + (tqrg - 1500) / 1e6, 'f', 6));
              query.addQueryItem ("tcall", match.captured ("call"));
              query.addQueryItem ("drift", "0");
              query.addQueryItem ("tgrid", match.captured ("grid"));
              query.addQueryItem ("dbm", match.captured ("dBm"));
              spot_queue_.enqueue (urlEncodeSpot (query));
              m_uploadType = 2;
            }
        }
    }
}

void WSPRNet::networkReply (QNetworkReply * reply)
{
  // check if request was ours
  if (m_outstandingRequests.removeOne (reply))
    {
      if (QNetworkReply::NoError != reply->error ())
        {
          Q_EMIT uploadStatus (QString {"Error: %1"}.arg (reply->error ()));
          // not clearing queue or halting queuing as it may be a
          // transient one off request error
        }
      else
        {
          QString serverResponse = reply->readAll ();
          if (m_uploadType == 2)
            {
              if (!serverResponse.contains(QRegExp("spot\\(s\\) added")))
                {
                  Q_EMIT uploadStatus (QString {"Upload Failed: %1"}.arg (serverResponse));
                  spot_queue_.clear ();
                  upload_timer_.stop ();
                }
            }

          if (!spot_queue_.size ())
            {
              Q_EMIT uploadStatus("done");
              QFile f {m_file};
              if (f.exists ()) f.remove ();
              upload_timer_.stop ();
            }
        }

      qDebug () << QString {"WSPRnet.org %1 outstanding requests"}.arg (m_outstandingRequests.size ());

      // delete request object instance on return to the event loop otherwise it is leaked
      reply->deleteLater ();
    }
}

bool WSPRNet::decodeLine (QString const& line, SpotQueue::value_type& query) const
{
  auto const& rx_match = wspr_re.match (line);
  if (rx_match.hasMatch ()) {
    int msgType = 0;
    QString msg = rx_match.captured (7);
    QString call, grid, dbm;
    QRegularExpression msgRx;

    // Check for Message Type 1
    msgRx.setPattern(R"(^([A-Z0-9]{3,6})\s+([A-R]{2}\d{2})\s+(\d+))");
    auto match = msgRx.match (msg);
    if (match.hasMatch ()) {
      msgType = 1;
      call = match.captured (1);
      grid = match.captured (2);
      dbm = match.captured (3);
    }

    // Check for Message Type 2
    msgRx.setPattern(R"(^([A-Z0-9/]+)\s+(\d+))");
    match = msgRx.match (msg);
    if (match.hasMatch ()) {
      msgType = 2;
      call = match.captured (1);
      grid = "";
      dbm = match.captured (2);
    }

    // Check for Message Type 3
    msgRx.setPattern(R"(^<([A-Z0-9/]+)>\s+([A-R]{2}\d{2}[A-X]{2})\s+(\d+))");
    match = msgRx.match (msg);
    if (match.hasMatch ()) {
      msgType = 3;
      call = match.captured (1);
      grid = match.captured (2);
      dbm = match.captured (3);
    }

    // Unknown message format
    if (!msgType) {
      return false;
    }

    query.addQueryItem ("function", "wspr");
    query.addQueryItem ("date", rx_match.captured (1));
    query.addQueryItem ("time", rx_match.captured (2));
    query.addQueryItem ("sig", rx_match.captured (4));
    query.addQueryItem ("dt", rx_match.captured(5));
    query.addQueryItem ("drift", rx_match.captured(8));
    query.addQueryItem ("tqrg", rx_match.captured(6));
    query.addQueryItem ("tcall", call);
    query.addQueryItem ("tgrid", grid);
    query.addQueryItem ("dbm", dbm);
  } else {
    return false;
  }
  return true;
}

QString WSPRNet::encode_mode () const
{
  if (m_mode == "WSPR") return "2";
  if (m_mode == "WSPR-15") return "15";
  if (m_mode == "FST4W")
    {
      auto tr = static_cast<int> ((TR_period_ / 60.)+.5);
      if (2 == tr || 15 == tr)
        {
          tr += 1;              // distinguish from WSPR-2 and WSPR-15
        }
      return QString::number (tr);
    }
  return "";
}

auto WSPRNet::urlEncodeNoSpot () const -> SpotQueue::value_type
{
  SpotQueue::value_type query;
  query.addQueryItem ("function", "wsprstat");
  query.addQueryItem ("rcall", m_call);
  query.addQueryItem ("rgrid", m_grid);
  query.addQueryItem ("rqrg", m_rfreq);
  query.addQueryItem ("tpct", m_tpct);
  query.addQueryItem ("tqrg", m_tfreq);
  query.addQueryItem ("dbm", m_dbm);
  query.addQueryItem ("version", m_vers);
  query.addQueryItem ("mode", encode_mode ());
  return query;;
}

auto WSPRNet::urlEncodeSpot (SpotQueue::value_type& query) const -> SpotQueue::value_type
{
  query.addQueryItem ("version", m_vers);
  query.addQueryItem ("rcall", m_call);
  query.addQueryItem ("rgrid", m_grid);
  query.addQueryItem ("rqrg", m_rfreq);
  query.addQueryItem ("mode", encode_mode ());
  return query;
}

void WSPRNet::work()
{
  if (spots_to_send_ && spot_queue_.size ())
    {
#if QT_VERSION < QT_VERSION_CHECK (5, 15, 0)
      if (QNetworkAccessManager::Accessible != network_manager_->networkAccessible ()) {
        // try and recover network access for QNAM
        network_manager_->setNetworkAccessible (QNetworkAccessManager::Accessible);
      }
#endif
      QNetworkRequest request (QUrl {wsprNetUrl});
      request.setHeader (QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
      auto const& spot = spot_queue_.dequeue ();
      m_outstandingRequests << network_manager_->post (request, spot.query (QUrl::FullyEncoded).toUtf8 ());
      Q_EMIT uploadStatus(QString {"Uploading Spot %1/%2"}.arg (spots_to_send_ - spot_queue_.size()).arg (spots_to_send_));
    }
  else
    {
      upload_timer_.stop ();
    }
}

void WSPRNet::abortOutstandingRequests () {
  spot_queue_.clear ();
  for (auto& request : m_outstandingRequests) {
    request->abort ();
  }
}