/***************************************************************************
**
**  This file is part of campbelltob3.
**
**  campbelltob3 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.
**
**  campbelltob3 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: 2016-06-10
**  Copyright: 2016-2019
**    Marc Wathelet (ISTerre, Grenoble, France)
**
***************************************************************************/

#include <limits>

#include "Tob3Scanner.h"

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

  Full description of class still missing
*/

/*!
  Description of constructor still missing
*/
Tob3Scanner::Tob3Scanner(QObject * parent)
  : DeviceCardScanner(parent)
{
  TRACE;
  _dataFrameSize=0;
  _channelCount=0;
  _frameTimeResolution=0;
  _samplingPeriod=0;
  _dataTypes=nullptr;
  _recordSize=0;
  _recordPerFrame=1;
  _dataSize=0;
  _validationStamp=0;
  _countPerVolt=nullptr;
  _voltPerUnit=nullptr;
  _buffer=nullptr;
}

Tob3Scanner::~Tob3Scanner()
{
  TRACE;
  _offsets.clear(true);
  delete [] _dataTypes;
  delete [] _countPerVolt;
  delete [] _voltPerUnit;
  deleteBuffer();
}

bool Tob3Scanner::setFileName(const QString& fileName)
{
  TRACE;
  clear();
  _fileName=fileName;
  return start();
}

QFile * Tob3Scanner::stream(QIODevice::OpenModeFlag mode)
{
  TRACE;
  QFile * f=new QFile(_fileName);
  if(f->open(mode)) {
    App::log(this, tr("Opening file %1.\n").arg(_fileName));
    return f;
  } else {
    App::log(this, tr(" # Error # Cannot open file %1.\n")
                           .arg(_fileName));
    delete f;
    return nullptr;
  }
}

inline bool Tob3Scanner::readFrameFast(QDataStream& s, Record *rec)
{
  s >> rec->seconds >> rec->subSeconds >> rec->recordNumber;
  s.skipRawData(_dataSize);
  s >> rec->validationStamp;
  return (((rec->validationStamp & 0xFFFF0000) >> 16)==_validationStamp);
}

void Tob3Scanner::deleteBuffer()
{
  if(_buffer) {
    for(int ic=0; ic<_channelCount; ic++) {
      delete [] _buffer[ic];
    }
    delete [] _buffer;
  }
  _buffer=nullptr;
}

int Tob3Scanner::allocateBuffer(double duration)
{
  TRACE;
  deleteBuffer();
  double n=static_cast<double>(_recordPerFrame*_samplingPeriod);
  int nSamples=(qFloor(duration/n)+2)*static_cast<int>(_recordPerFrame);
  _buffer=new double*[static_cast<size_t>(_channelCount)];
  for(int ic=0; ic<_channelCount; ic++) {
    _buffer[ic]=new double[static_cast<size_t>(nSamples)];
  }
  return nSamples;
}

inline void Tob3Scanner::readFrame(QDataStream& s, int& index)
{
  s.skipRawData(12);
  int lastIndex=index+static_cast<int>(_recordPerFrame);
  for(int ir=index; ir<lastIndex; ir++) {
    for(int ic=0; ic<_channelCount; ic++) {
      switch(_dataTypes[ic]) {
      case IEEE4L: {
          float val;
          s >> val;
          _buffer[ic][ir]=Number::toDouble(val);
        }
        break;
      case IEEE4B: {
          s.setByteOrder(QDataStream::LittleEndian);
          float val;
          s >> val;
          _buffer[ic][ir]=Number::toDouble(val);
          s.setByteOrder(QDataStream::BigEndian);
        }
        break;
      case FP2: {
          qint16 val;
          s >> val;
          _buffer[ic][ir]=Number::toDouble(csiFs2ToFloat(val));
        }
        break;
      }
    }
  }
  s.skipRawData(4);
  index=lastIndex;
}

void Tob3Scanner::run()
{
  TRACE;
  if(!_stream) return;

  if(!readAsciiHeader()) {
    stop();
    return;
  }

  QDataStream s(_stream);
  s.setByteOrder(QDataStream::LittleEndian);

  Record rec[2];
  Record *current=rec, *last=rec+1;

  // Read first two frames to get first starting time and sampling period
  emit beginClearRange();
  _offsets.clear(true);
  _range.clear();
  emit endClearRange();
  qint64 pos=_stream->pos();
  TimeRange r;
  double referenceSeconds;
  double referenceSubSeconds;

  if(readFrameFast(s, last) && readFrameFast(s, current)) {
    referenceSeconds=last->seconds;
    referenceSubSeconds=_frameTimeResolution*last->subSeconds;
    _offsets.append(new offset(0, pos));
    _referenceTime.setDate(QDate(1990, 1, 1));
    _referenceTime.addSeconds(referenceSeconds);
    _referenceTime.addSeconds(referenceSubSeconds);
    _samplingPeriod=_frameTimeResolution*(static_cast<double>(current->subSeconds)-static_cast<double>(last->subSeconds));
    _samplingPeriod+=static_cast<double>(current->seconds)-static_cast<double>(last->seconds);
    if(current->recordNumber-last->recordNumber!=_recordPerFrame) {
      App::log(tr(" # Error # Bad number of records in first frame (%1, expected %2)\n")
                       .arg(current->recordNumber-last->recordNumber)
                       .arg(_recordPerFrame));
      stop();
      return;
    }
    _samplingPeriod/=_recordPerFrame;
    if(_frameInterval>0 && _frameInterval!=_samplingPeriod) {
      App::log(tr(" # Error # Bad sampling period (computed=%1 s, header=%2 s)\n").arg(_samplingPeriod).arg(_frameInterval));
      stop();
      return;
    }
  } else {
    App::log(tr(" # Error # Empty file\n"));
    stop();
    return;
  }
  emit headerParsed();
  r.setStart(_referenceTime);

  // Read all frames until this end
  qSwap(current, last);
  while(!s.atEnd()) {
    if(isStopRequested()) {
      return;
    }
    pos=_stream->pos();
    if(!readFrameFast(s, current)) {
      break;
    }
    double gap=static_cast<double>(current->seconds)-static_cast<double>(last->seconds)+
        _frameTimeResolution*(static_cast<double>(current->subSeconds)-static_cast<double>(last->subSeconds));
    if(fabs(gap-_samplingPeriod*_recordPerFrame)>1e-12) {

      r.setEnd(_referenceTime.shifted(last->seconds-referenceSeconds+
                                      _frameTimeResolution*last->subSeconds-referenceSubSeconds+
                                      (current->recordNumber-last->recordNumber-1)*_samplingPeriod));
      emit beginAddRange(_range.ranges().count());
      _rangeMutex.lock();
      _range.add(r);
      _rangeMutex.unlock();
      emit endAddRange();
      r.setStart(_referenceTime.shifted(current->seconds-referenceSeconds+
                                        _frameTimeResolution*current->subSeconds-referenceSubSeconds));
      _offsets.append(new offset(_referenceTime.secondsTo(r.start()), pos));
      App::log(tr("Gap of %1 s between %2 and %3\n")
                       .arg(Number::secondsToDuration(gap))
                       .arg(r.end().toString())
                       .arg(r.start().toString()));
    }
    qSwap(current, last);
  }
  // For last frame, we do not know how many records are really written, we cut at the beginning of the last frame.
  r.setEnd(_referenceTime.shifted(last->seconds-referenceSeconds+
                                  _frameTimeResolution*last->subSeconds-referenceSubSeconds));
  emit beginAddRange(_range.ranges().count());
  _rangeMutex.lock();
  _range.add(r);
  _rangeMutex.unlock();
  emit endAddRange();
  stop();
}

/*!
  Read first 6 ASCII lines
*/
bool Tob3Scanner::readAsciiHeader()
{
  TRACE;
  LineParser p;
  bool ok=true;
  p.setDelimiters(", ");
  p.setQuotes(true);

  // Experiment environment
  p.setString(_stream->readLine());
  if(p.toString(0, ok)!="TOB3") {
     App::log(tr(" # Error # Not a TOB3 file.\n") );
     return false;
  }
  _stationName=p.toString(1, ok);
  _modelName=p.toString(2, ok);
  _serialNumber=p.toString(3, ok);

  // Additional Decode Information
  QRegExp exp;
  p.setString(_stream->readLine());
  exp.setPattern("([0-9]+) ([a-zA-Z]+)");
  if(exp.exactMatch(p.toString(1, ok))) {
    QString unit=exp.cap(2).toLower();
    switch(unit[0].unicode()) {
    case 'n':
      _frameInterval=1e-9;
      break;
    case 'u':
      _frameInterval=1e-6;
      break;
    case 'm':
      if(unit=="msec") {
        _frameInterval=1e-3;
      } else {
        _frameInterval=60;
      }
      break;
    case 's':
      _frameInterval=1;
      break;
    case 'h':
      _frameInterval=3600;
      break;
    case 'd':
      _frameInterval=86400;
      break;
    }
  } else {
    exp.setPattern("([0-9]+)");
    if(exp.exactMatch(p.toString(1, ok))) {
      _frameInterval=1e-9;
    } else {
      App::log(tr(" # Error # bad Non-Timestamped Record Interval (%1).\n").arg(p.toString(1, ok)) );
      return false;
    }
  }
  _frameInterval*=exp.cap(1).toDouble();
  _dataFrameSize=p.toInt(2, ok);
  _intendedTablesize=p.toUInt(3, ok);
  _validationStamp=p.toUInt(4, ok);
  if(p.toString(5, ok).toLower()=="secusec") {
    _frameTimeResolution=10*1e-6;
  } else {
    exp.setPattern("Sec([0-9]+)([UMN])[Ss]ec"); // Capital letter in documentation, but encountered small
    if(exp.exactMatch(p.toString(5, ok))) {
      switch(exp.cap(2).toLower()[0].unicode()) {
      case 'n':
        _frameTimeResolution=1e-9;
        break;
      case 'u':
        _frameTimeResolution=1e-6;
        break;
      case 'm':
        _frameTimeResolution=1e-3;
        break;
      }
      _frameTimeResolution*=exp.cap(1).toInt();
    } else {
      App::log(tr(" # Error # bad Frame Time Resolution (%1).\n").arg(p.toString(5, ok)) );
      return false;
    }
  }

  // Field Names
  p.setString(_stream->readLine());
  _channelCount=p.count();

  // Field Units
  p.setString(_stream->readLine());
  if(_channelCount!=p.count()) {
    App::log(tr(" # Error # bad number of field units.\n") );
    return false;
  }
  delete [] _countPerVolt;
  _countPerVolt=new double[static_cast<size_t>(_channelCount)];
  delete [] _voltPerUnit;
  _voltPerUnit=new double[static_cast<size_t>(_channelCount)];
  for(int i=0; i<_channelCount; i++) {
    _countPerVolt[i]=1;
    _voltPerUnit[i]=1;
    QString u=p.toString(i, ok).toLower();
    if(!u.isEmpty()) {
      switch(u[0].unicode()) {
      case 'm':
        if(u=="mv") {
          _countPerVolt[i]=1000;
        }
        break;
      }
    }
  }

  // Field Processing
  p.setString(_stream->readLine());
  if(_channelCount!=p.count()) {
    App::log(tr(" # Error # bad number of field processing.\n") );
    return false;
  }
  for(int i=0; i<_channelCount; i++) {
    if(p.toString(i, ok)!="Smp") {
      App::log(tr(" # Error # Cannot read something else than 'Smp' processing.\n") );
      return false;
    }
  }

  // Field Data Types
  p.setString(_stream->readLine());
  if(_channelCount!=p.count()) {
    App::log(tr(" # Error # bad number of field data types.\n") );
    return false;
  }
  delete [] _dataTypes;
  _dataTypes=new DataType[static_cast<size_t>(_channelCount)];
  _recordSize=0;
  for(int i=0; i<_channelCount; i++) {
    QString dataTypeString=p.toString(i, ok);
    if(dataTypeString=="FP2") {
      _dataTypes[i]=FP2;
      _recordSize+=2;
    } else if(dataTypeString.startsWith("IEEE4")) {
      if(dataTypeString=="IEEE4B") {
        _dataTypes[i]=IEEE4B;
      } else {
        _dataTypes[i]=IEEE4L;
      }
      _recordSize+=4;
    } else {
      App::log(tr(" # Error # Data type not supported ('%1').\n").arg(dataTypeString) );
      return false;
    }
  }

  static const int headerSize=3*4;
  static const int footerSize=4;
  int n=headerSize+_recordSize+footerSize;
  if(_frameInterval==0.0 || n>1024) {
    _recordPerFrame=1;
    if(n!=_dataFrameSize) {
      App::log(tr(" # Error # Data frame size does not match (%1 instead of %2).\n")
                       .arg(_dataFrameSize).arg(n));
      return false;
    }
  } else {
    _recordPerFrame=(_dataFrameSize-(headerSize+footerSize))/_recordSize;
    n=headerSize+_recordPerFrame*_recordSize+footerSize;
    if(n!=_dataFrameSize) {
      App::log(tr(" # Error # Data frame size does not match (%1 instead of %2).\n")
                       .arg(_dataFrameSize).arg(n));
      return false;
    }
  }
  _dataSize=_recordPerFrame*_recordSize;

  return true;
}


/*!
  Converts a value represented in two byte floating point format
  into a single precision float. The size of the buffer provided
  is assumed to be at least two bytes in length.

  Adapted from code provided in section 15.2.1
  from "Campbell Scientific Data File Formats" version 1.31 (6 August 2012)
*/
float Tob3Scanner::csiFs2ToFloat(qint16 value)
{
  // we are going to cast the binary value into a two byte
  // integer so that it is convenient to pick
  // out patterns and pick off parts of the structure.
  const uchar *buf=reinterpret_cast<const uchar*>(&value);
  quint16 fs_word=static_cast<quint16>(quint16(buf[0]) << 8)+buf[1];
  // we can now pick off the components of the FS2 structure
  bool is_negative=((fs_word & 0x8000)!=0);
  quint16 mantissa=fs_word & 0x1FFF;
  quint16 exponent=(fs_word & 0x6000) >> 13;
  float rtn;
  static quint16 const pos_infinity=0x1fff;
  static quint16 const neg_infinity=0x9fff;
  static quint16 const not_a_number=0x9ffe;
  switch(fs_word) {
  case pos_infinity:
    rtn=std::numeric_limits<float>::infinity();
    break;
  case neg_infinity:
    rtn=-1.0f*std::numeric_limits<float>::infinity();
    break;
  case not_a_number:
    rtn=std::numeric_limits<float>::quiet_NaN();
    break;
  default:
    rtn = static_cast<float>(mantissa);
    for(quint16 i=0; i<exponent; ++i) {
      rtn*=0.1f;
    }
    if(is_negative) {
      rtn*=-1.0f;
    }
    break;
  }
  return rtn;
}

SubSignalPool Tob3Scanner::load(SignalDatabase * db,
                                const TimeRange& userRange,
                                double minLength, double maxLength)
{
  TRACE;
  SubSignalPool pool;
  if(isRunning()) {
    cleanStop();
  }
  QFile * f=stream(QIODevice::ReadOnly);
  if(!f) return pool;
  _stream=f;

  QVector<TimeRange> rList=_range.intersection(userRange).split(minLength, maxLength);  

  // Create a new temporary file
  SignalFile * newFile=new SignalFile(db, _fileName, SignalFileFormat::Temporary);
  int iSig=0;

  for(QVector<TimeRange>::const_iterator it=rList.begin(); it!=rList.end(); it++) {
    const TimeRange& r=*it;
    int dt=qRound((_referenceTime.secondsTo(r.start())-readFrame(db, r))/_samplingPeriod);
    for(int ic=0; ic<_channelCount; ic++) {
      Signal * newSignal=new Signal(db);
      newSignal->setSamplingPeriod(_samplingPeriod);
      newSignal->setFile(newFile);
      newSignal->setNumberInFile(iSig++);
      newSignal->setType(Signal::Waveform);
      newSignal->setStartTime(r.start());
      newSignal->setCountPerVolt(_countPerVolt[ic]);
      newSignal->setVoltPerUnit(_voltPerUnit[ic]);
      newSignal->setNSamples(r.lengthSamples(1.0/_samplingPeriod));
      LOCK_SAMPLES(double, newSamples, newSignal)
        memcpy(newSamples, _buffer[ic]+dt, newSignal->nSamples()*sizeof(double));
      UNLOCK_SAMPLES(newSignal)

      pool.addSignal(newSignal);
    }
  }
  stop();
  return pool;
}

double Tob3Scanner::readFrame(SignalDatabase * db, const TimeRange &r)
{
  TRACE;
  double t=_referenceTime.secondsTo(r.start());
  int frameIndex=_offsets.firstIndexAfter(t);
  // Get the range index
  if(frameIndex>0) {
    if(frameIndex==_offsets.count() || _offsets.at(frameIndex)->_time>t) {
      frameIndex--;
    }
  }
  const offset * o=_offsets.at(frameIndex);
  _stream->seek(o->_value);

  QDataStream s(_stream);
  s.setByteOrder(QDataStream::LittleEndian);

  int nSamples=allocateBuffer(r.lengthSeconds());
  int index=0;
  int block20Size=nSamples/20;
  int block20Index=block20Size;
  GeopsyCoreEngine::instance()->setProgressMaximum(db, 20);
  for(int i=0; i<20; i++) {
    GeopsyCoreEngine::instance()->setProgressValue(db, i);
    while(index<block20Index) {
      readFrame(s, index);
    }
    block20Index+=block20Size;
  }
  while(index<nSamples) {
    readFrame(s, index);
  }
  GeopsyCoreEngine::instance()->setProgressValue(db, 20);

  return o->_time;
}
