/***************************************************************************
**
**  This file is part of GeopsySLink.
**
**  GeopsySLink 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 3 of the License, or
**  (at your option) any later version.
**
**  GeopsySLink 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 Foobar.  If not, see <http://www.gnu.org/licenses/>
**
**  See http://www.geopsy.org for more information.
**
**  Created: 2007-04-23
**  Copyright: 2007-2019
**    Marc Wathelet
**    Marc Wathelet (LGIT, Grenoble, France)
**
***************************************************************************/

#include <QtNetwork>

#include <GeopsyCore.h>
#include <QGpCoreTools.h>
#include <slink.h>

#include "SeedLink.h"
#include "SeedLinkStation.h"

namespace GeopsySLink {

class SeedLinkPrivate
{
public:
  static QByteArray streamTag(LibSLink::MSrecord * msr);
  LibSLink::SLCD * slconn;
  LibSLink::SLpacket * slpack;
};

QByteArray SeedLinkPrivate::streamTag(LibSLink::MSrecord * msr)
{
  QByteArray tag(msr->fsdh.network,2);
  tag+="_";
  tag+=QByteArray(msr->fsdh.station,5);
  tag+=QByteArray(msr->fsdh.location,2);
  tag+=QByteArray(msr->fsdh.channel,3);
  return tag;
}

/*!
  \class SeedLink SeedLink.h
  \brief Brief description of class still missing

  Full description of class still missing
*/

SeedLink * SeedLink::_instance=nullptr;

/*!
  Description of constructor still missing
*/
SeedLink::SeedLink(QObject * parent)
  : Thread(parent)
{
  TRACE;
  _serverPort=18000;
  _internals=new SeedLinkPrivate;
  _internals->slconn=0;
  _bufferType=SeedLinkStream::Unlimited;
  _maximumDuration=0.0;
  if(_instance) {
    qWarning("Only one instance of Seedlink allowed!");
  } else {
    _instance=this;
  }
  _error=NoError;
  qRegisterMetaType<GeopsySLink::SeedLink::Error>();
}

/*!
  Description of destructor still missing
*/
SeedLink::~SeedLink()
{
  TRACE;
  if(_internals->slconn) sl_freeslcd (_internals->slconn);
  delete _internals;
  _instance=0;
}

/*!

*/
void SeedLink::setServer(QByteArray address, qint16 port)
{
  TRACE;
  stop();
  _serverName=address.trimmed();
  _serverPort=port;
  _request=Dns;
  start();
}

void SeedLink::stop()
{
  TRACE;
  if(_internals->slconn) sl_terminate(_internals->slconn);
  wait();
  ASSERT(_internals->slconn==0);
}

void SeedLink::streams()
{
  TRACE;
  stop();
  _request=Streams;
  _serverInfo.clear();
  _listeningStreams.clear();
  start();
}

void SeedLink::stations()
{
  TRACE;
  stop();
  _request=Stations;
  _serverInfo.clear();
  _listeningStreams.clear();
  start();
}

bool SeedLink::addStream(SeedLinkStream * str)
{
  TRACE;
  QMutexLocker m(&_mutex);
  _request=Data;
  if(!_listeningStreams.contains(str->tag())) {
    _listeningStreams.insert(str->tag(),str);
    str->setListening(true);
    App::log(tr("Listening to stream %1\n").arg(str->tag().data()) );
    return true;
  } else {
    return false;
  }
}

bool SeedLink::removeStream(SeedLinkStream * str)
{
  TRACE;
  // This function may be called while the main loop is still running.
  // Streams with detached signal cannot be used anymore (see condition below).
  QMutexLocker m(&_mutex);
  _request=Data;
  if(_listeningStreams.contains(str->tag())) {
    _listeningStreams.remove(str->tag());
    str->detachSignal();
    str->setListening(false);
    App::log(tr("Stop listening to stream %1\n").arg(str->tag().data()) );
    return true;
  } else {
    return false;
  }
}

SubSignalPool SeedLink::subPool() const
{
  TRACE;
  SubSignalPool s;
  for(QMap<QByteArray,SeedLinkStream *>::const_iterator it=_listeningStreams.begin(); it!=_listeningStreams.end();it++) {
    if(it.value()->signal()) {
      s.addSignal(it.value()->signal());
    }
  }
  s.setName(tr("Active SeedLink signals"));
  return s;
}

/*!
  Forward seedlink errors to application log system
*/
void SeedLink::log(const char * msg)
{
  TRACE;
  QByteArray str(msg);
  App::log(str+"\n");
  if(str.contains("DATA/FETCH/TIME command is not accepted")) {
    _instance->setError(FromTimeNotAvailable);
  }
}

void SeedLink::setError(Error e)
{
  TRACE;
  _error=e;
  for(QMap<QByteArray,SeedLinkStream *>::iterator it=_listeningStreams.begin(); it!=_listeningStreams.end();it++) {
    SeedLinkStream& str=*it.value();
    str.detachSignal();
    str.setListening(false);
  }
  _listeningStreams.clear();
  sl_terminate (_internals->slconn);
  emit error(_error);
}

void SeedLink::start()
{
  TRACE;
  if(isRunning()) stop();
  LibSLink::sl_loginit(1, log, NULL, log, NULL);
  QThread::start();
}

void SeedLink::run()
{
  TRACE;
  _error=NoError;
  switch (_request) {
  case Streams:
    _xmlInfos.clear();
    _internals->slconn=LibSLink::sl_newslcd();
    sl_request_info(_internals->slconn, "STREAMS" );
    sl_setuniparams (_internals->slconn, 0, -1, 0);
    App::log(tr("Requesting list of streams...\n") );
    break;
  case Stations:
    _xmlInfos.clear();
    _internals->slconn=LibSLink::sl_newslcd();
    LibSLink::sl_request_info(_internals->slconn, "STATIONS" );
    LibSLink::sl_setuniparams (_internals->slconn, 0, -1, 0);
    App::log(tr("Requesting list of stations...\n") );
    break;
  case Data: {
      _internals->slconn=LibSLink::sl_newslcd();
      _mutex.lock();
      // Set fromTime to _fromTime if not all active signals are already initialized
      DateTime fromTime;
      for(QMap<QByteArray,SeedLinkStream *>::iterator it=_listeningStreams.begin(); it!=_listeningStreams.end();it++) {
        SeedLinkStream& str=*it.value();
        if( !str.signal()) {
          fromTime=_fromTime;
          break;
        }
      }
      QMap<SeedLinkStation *,QList<SeedLinkStream *> > newStreams;
      for(QMap<QByteArray,SeedLinkStream *>::iterator it=_listeningStreams.begin(); it!=_listeningStreams.end();it++) {
        SeedLinkStream * str=it.value();
        SeedLinkStation * sta=str->station();
        QMap<SeedLinkStation *,QList<SeedLinkStream *> >::iterator its;
        its=newStreams.find(sta);
        if(its!=newStreams.end()) {
          its.value().append(str);
        } else {
          QList<SeedLinkStream *> list;
          list.append(str);
          newStreams.insert(sta, list);
        }
      }
      for(QMap<SeedLinkStation *,QList<SeedLinkStream *> >::iterator it=newStreams.begin(); it!=newStreams.end();it++) {
        SeedLinkStation * sta=it.key();
        QList<SeedLinkStream *>& list=it.value();
        QString selectors;
        for(QList<SeedLinkStream *>::iterator its=list.begin();its!=list.end();its++) {
          SeedLinkStream * str=*its;
          if(str->signal()) {
            DateTime endTime=str->signal()->endTime();
            if(!fromTime.isValid() || endTime<fromTime) fromTime=endTime;
          }
          selectors+=" "+str->location()+str->seedName()+"."+str->type();
        }
        App::log(tr("Requesting streams %1_%2:%3\n").arg(sta->network()).arg(sta->name()).arg(selectors) );
        LibSLink::sl_addstream(_internals->slconn, sta->network().toLatin1().data(),
                               sta->name().toLatin1().data(),
                               selectors.toLatin1().data(), -1, 0);
      }
      if(fromTime.isValid()) {
        _internals->slconn->begin_time=strdup(fromTime.toString("yyyy,MM,dd,hh,mm,ss").toLatin1().data());
      }
      _mutex.unlock();
    }
    break;
  case Dns: {
      // Check if serverName is a numerical address or not
      QRegExp ipExp("[0-9]*\\.[0-9]*\\.[0-9]*\\.[0-9]*");
      if(ipExp.exactMatch(_serverName) ) {
        _serverAddress=_serverName+":"+QString::number(_serverPort).toLatin1();
      } else {
        QHostInfo info=QHostInfo::fromName(_serverName);
        if(info.addresses().isEmpty()) {
          _serverAddress.clear();
          return;
        }
        _serverAddress=info.addresses().first().toString().toLatin1()+":"+QString::number(_serverPort).toLatin1();
        App::log(tr("Resolved %1:%2 into %3\n").arg(_serverName.data()).arg(_serverPort).arg(_serverAddress.data()) );
      }
    }
    return;
  }

  _internals->slconn->sladdr=(char *) malloc(_serverAddress.length()+1);
  strcpy(_internals->slconn->sladdr, _serverAddress.data());
  _internals->slconn->resume=0;

  while(sl_collect(_internals->slconn, &_internals->slpack)!=0) {
    //App::log(tr("collect packet\n") );
    packetReceived();
  }
  /* Make sure everything is shut down*/
  if(_internals->slconn->link!=-1) sl_disconnect (_internals->slconn);
  sl_freeslcd (_internals->slconn);
  _internals->slconn=0;
}

void SeedLink::packetReceived()
{
  TRACE;
  int ptype =sl_packettype (_internals->slpack);
  static LibSLink::MSrecord * msr=NULL;
  switch(ptype) {
  case SLINF:
  case SLINFT:
    LibSLink::sl_msr_parse (_internals->slconn->log, _internals->slpack->msrecord, &msr, 0, 0);
    if(strncmp(msr->fsdh.channel, "ERR", 3)==0) {
      switch (_request) {
      case Streams:
        setError(StreamsNotAvailable);
        break;
      case Stations:
        setError(StationsNotAvailable);
      default:
        break;
      }
    }
    _xmlInfos+=QByteArray((char *) msr->msrecord + msr->fsdh.begin_data, msr->fsdh.num_samples);
    if(ptype==SLINFT) {
      XMLHeader hdr(&_serverInfo);
      //App::log(_xmlInfos << endl;
      if(hdr.xml_restoreString(_xmlInfos)==XMLClass::NoError) {
        App::log(tr("List of streams downloaded successfully: %1 stations\n").arg(_serverInfo.count()) );
        emit infoAvailable();
      } else {
        App::log(tr("Error parsing xml info received from %1:\n").arg(_serverAddress.data()));
        App::log(_xmlInfos+"\n");
      }
    }
    break;
  case SLDATA: {
      if(sl_msr_parse(_internals->slconn->log, _internals->slpack->msrecord, &msr, 1, 1)) {
        QByteArray tag=SeedLinkPrivate::streamTag(msr); // msr structure is populated by sl_msr_parse
        _mutex.lock();
        QMap<QByteArray,SeedLinkStream *>::iterator it;
        if(_listeningStreams.count()==1) { // Be less strict in identifying streams if only one listening stream
          it=_listeningStreams.begin();
        } else {
          it=_listeningStreams.find(tag);
        }
        if(it!=_listeningStreams.end()) {
          SeedLinkStream * str=it.value();
          if(str && str->listening()) {
            DateTime t;
            t.fromTime_t(LibSLink::sl_msr_depochstime(msr));
            if(AbstractStream::applicationVerbosity()>=2) {
              DateTime tend(t);
              tend.addSeconds((double)msr->numsamples/(double)sl_msr_dnomsamprate(msr));
              App::log(tr("Received packet %1 from %2 to %3 for stream %4\n")
                           .arg(sl_sequence(_internals->slpack))
                           .arg(t.toString(DateTime::defaultUserFormat))
                           .arg(tend.toString(DateTime::defaultUserFormat))
                           .arg(tag.data()));
            }
            if(!str->signal()) {
              str->initSignal(t, LibSLink::sl_msr_dnomsamprate(msr));
              t=str->signal()->startTime();
            }
            //App::log(tr("  Time of packet=%1\n").arg(t0Packet) );
            TimeRange r(t, msr->numsamples*str->signal()->samplingPeriod());
            emit beginSignalChange(str->signal(), r);
            str->set(t, msr->datasamples, msr->numsamples, _bufferType, _maximumDuration);
            emit endSignalChange(str->signal(), r);
          }
        } else {
          App::log(tr("Warning: received data for stream '%1', not listening to that stream\n").arg(tag.data()) );
        }
        _mutex.unlock();
      } else {
        // msr structure might be partially initialized in case of error, we display what we can.
        App::log(tr("Error parsing packet from stream %1, no data downloaded.\n")
                  .arg(SeedLinkPrivate::streamTag(msr).data()));
      }
    }
    break;
  case SLKEEP:
    App::log(tr("Keep alive packet received\n"));
    break;
  default:
    App::log(tr("Received unknown packet\n"));
    break;
  }
}

} // namespace GeopsySLink
