/***************************************************************************
**
**  This file is part of GeopsyCore.
**
**  GeopsyCore 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.
**
**  GeopsyCore 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-10-12
**  Copyright: 2007-2019
**    Marc Wathelet
**    Marc Wathelet (LGIT, Grenoble, France)
**
***************************************************************************/

#include <math.h>

#include <QGpCoreTools.h>
#include "GuralpCompressedBlock.h"

namespace GeopsyCore {

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

    Full description of class still missing
  */

  /*!
      \a str must point to a char[7]
  */
  inline void GuralpCompressedBlock::base36ToAscii(qint32 b, char * str)
  {
    static const double base36[]={60466176.0, 1679616.0, 46656.0, 1296.0, 36.0, 1.0};
    static const double invbase36[]={1.0/60466176.0, 1.0/1679616.0, 1.0/46656.0, 1.0/1296.0, 1.0/36.0, 1.0};
    static const char charMap[]={ '0','1','2','3','4','5','6','7','8','9','A','B','C','D',
                                    'E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T',
                                    'U','V','W','X','Y','Z' };

    double bdt;
    if(b>=0) {
      double bd=static_cast<double>(b);
      for(int i=0; i<6; i++) {
        bdt=floor(bd * invbase36[i] );
        str[i]=charMap[static_cast<int>(bdt)];
        bd -= bdt * base36[i];
      }
      for(int i=0;i<5 && str[i]=='0'; i++) {
        str[i]=' ';
      }
      str[6]='\0';
    } else {
      double bd=static_cast<double>(b & 0x03FFFFFF);
      for(int i=1; i<6; i++) {
        bdt=floor(bd * invbase36[i] );
        str[i-1]=charMap[static_cast<int>(bdt)];
        bd -= bdt * base36[i];
      }
      for(int i= 0; i<4 && str[i]=='0'; i++) {
        str[i]=' ';
      }
      str[5]='\0';
    }
  }

  GuralpCompressedBlock::GuralpCompressedBlock()
  {
    TRACE;
    _nSamples=0;
    _samples=nullptr;
    _samplingFrequency=0.0;
    _systemID[0]='\0';
    _device[0]='\0';
  }

  GuralpCompressedBlock::GuralpCompressedBlock(const GuralpCompressedBlock& o)
  {
    TRACE;
    _samples=nullptr;
    operator=(o);
  }

  GuralpCompressedBlock::~GuralpCompressedBlock()
  {
    TRACE;
    delete [] _samples;
  }

  /*!
    Copy operator.
  */
  void GuralpCompressedBlock::operator=(const GuralpCompressedBlock& o)
  {
    TRACE;
    _hash=o._hash;
    strcpy(_systemID, o._systemID);
    strcpy(_device, o._device);
    _component=o._component;
    _channel=o._channel;
    _startTime=o._startTime;
    _samplingFrequency=o._samplingFrequency;
    _compressionCode=o._compressionCode;
    _nSamples=o._nSamples;
    delete [] _samples;
    _samples=nullptr;
  }

  /*!
    Checks streamID, systemID, sampling frequency, channel and component.
  */
  bool GuralpCompressedBlock::operator==(const GuralpCompressedBlock& o) const
  {
    if(strcmp(_systemID, o._systemID)!=0) return false;
    if(_component!=o._component) return false;
    if(_channel!=o._channel) return false;
    if(strcmp(_device,o._device)!=0) return false;
    if(_samplingFrequency!=o._samplingFrequency) return false;
    return true;
  }

  bool GuralpCompressedBlock::parseHeader(const char * gcfHeader)
  {
    TRACE;
    char str[7];
    // All GCF header are stored in big endian
    qint32 gcfNativeHeader[4];
    for(int i= 0; i<4; i++) {
      gcfNativeHeader[i]=File::fromBigEndian((reinterpret_cast<const qint32 *>(gcfHeader))[i]);
    }
    // Hash for fast comparisons. native header index 2 and 3 must be partially included in comparison
    _hash=static_cast<quint32>(gcfNativeHeader[0]);
    _hash+=static_cast<quint32>(gcfNativeHeader[1]);
    // system ID
    base36ToAscii(gcfNativeHeader[0], str);
    strcpy(_systemID, str);
    // stream ID
    base36ToAscii(gcfNativeHeader[1], str);
    strncpy(_device, str, 4);
    _device[4]='\0';
    _component=str[4];
    _channel=str[5];
    switch (_component) {
    case 'Z':
    case 'N':
    case 'E':
    case 'X':
    case 'C':
      if(( _channel > 'D' && _channel < 'G' ) || _channel > 'S') {
        if(_component=='C') {
          App::log(tr("Error: Channel Data stream, bad stream ID: %1 (expected CD)\n").arg(str) );
          return false;
        } else {
          App::log(tr("Error: Data stream, bad output tap: %1 (expected 0-9,A-C,G-S)\n").arg(_channel) );
          return false;
        }
      }
      break;
    case 'M':
      if(_channel > 'F') {
        App::log(tr("Error: Mux channel stream, bad channel number: %1 (expected 0-9,A-F)\n").arg(_channel) );
        return false;
      }
      App::log(tr("Error: mux channel stream are currently not supported\n") );
      return false;
    case '0':
      if(_channel!='0') {
        App::log(tr("Error: Status stream, bad stream ID: %1 (expected OO)\n").arg(str) );
        return false;
      }
      return true;
    case 'B':
      if(_channel!='P') {
        App::log(tr("Error: Byte Pipe stream, bad stream ID: %1 (expected BP)\n").arg(str) );
        return false;
      }
      App::log(tr("Error: byte pipe stream are currently not supported\n") );
      return false;
    case 'I':
      if(_channel!='B') {
        App::log(tr("Error: Information Block, bad stream ID: %1 (expected IB)\n").arg(str) );
        return false;
      }
      return true;
    default:
      App::log(tr("Error: bad stream ID: %1\n").arg(str) );
      return false;
    }
    // Date code
    _startTime.setDate(QDate(1989, 11, 17));
    _startTime.setTime(QTime(0, 0));
    _startTime.addDays((static_cast<quint32>(gcfNativeHeader[2]) & 0xFFFE0000) >> 17);
    _startTime.addSeconds(static_cast<quint32>(gcfNativeHeader[2]) & 0x0001FFFF);
    // Data format
    uint intSamplingFrequency=(static_cast<quint32>(gcfNativeHeader[3]) & 0x00FF0000) >> 16;
    _samplingFrequency=intSamplingFrequency;
    if(_samplingFrequency==0.0) {
      App::log(tr("Error: null sampling frequency\n") );
      return false;
    }
    _hash+=intSamplingFrequency;
    _compressionCode=(static_cast<quint32>(gcfNativeHeader[3]) & 0x0000FF00) >> 8;
    int newNSamples;
    switch(_compressionCode) {
    case 1:
      newNSamples=gcfNativeHeader[3] & 0x000000FF;
      break;
    case 2:
      newNSamples=(gcfNativeHeader[3] & 0x000000FF) << 1;
      break;
    case 4:
      newNSamples=(gcfNativeHeader[3] & 0x000000FF) << 2;
      break;
    default:
      App::log(tr("Error: bad compression code: %1 (expected 1, 2 or 4)\n").arg(_compressionCode) );
      return false;
    }
    if(newNSamples<1) {
      App::log(tr("Error: negative or null number of samples\n") );
      return false;
    }
    if(newNSamples!=_nSamples) {
      delete [] _samples;
      _samples=new int[newNSamples];
    }
    _nSamples=newNSamples;
    return true;
  }

  bool GuralpCompressedBlock::parseBody(const char * gcfBody)
  {
    TRACE;
    _samples[0]=File::fromBigEndian(*reinterpret_cast<const qint32 *>(gcfBody));
    gcfBody+=4;
    switch(_compressionCode) {
    case 1:
      if(*reinterpret_cast<const qint32 *>(gcfBody)!=0) {
        App::log(tr("Warning: first sample difference must be null.\n") );
      }
      gcfBody+=4;
      for(int i=1; i<_nSamples; i++) {
        _samples[i]=_samples[i-1]+File::fromBigEndian(*reinterpret_cast<const qint32 *>(gcfBody));
        gcfBody+=4;
      }
      break;
    case 2:
      if(*reinterpret_cast<const qint16 *>(gcfBody)!=0) {
        App::log(tr("Warning: first sample difference must be null.\n") );
      }
      gcfBody+=2;
      for(int i=1; i<_nSamples; i++) {
        _samples[i]=_samples[i-1]+File::fromBigEndian(*reinterpret_cast<const qint16 *>(gcfBody));
        gcfBody+=2;
      }
      break;
    case 4:
      if(*gcfBody!=0) {
        App::log(tr("Warning: first sample difference must be null.\n") );
      }
      gcfBody++;
      for(int i=1; i<_nSamples; i++) {
        _samples[i]=_samples[i-1]+*gcfBody;
        gcfBody++;
      }
      break;
    default:
      return false;
    }
    // Reverse Integrating Constant check
    if(File::fromBigEndian(*reinterpret_cast<const qint32 *>(gcfBody))!=_samples[_nSamples-1]) {
      App::log(tr("Error: bad Reverse Integrating Constant: %1 (read from block=%2)\n")
                    .arg(_samples[_nSamples-1]).arg(File::fromBigEndian(*reinterpret_cast<const qint32 *>(gcfBody))));
      return false;
    } else {
      return true;
    }
  }

  GuralpCompressedBlock::StreamType GuralpCompressedBlock::streamType() const
  {
    switch(_component) {
    case 'Z':
    case 'N':
    case 'E':
    case 'X':
      return Data;
    case 'C':
      if(_channel=='D' ) {
        return ChannelData;
      } else {
        return Data;
      }
    case 'M':
      return MuxChannel;
    case 'O':
      return Status;
    case 'B':
      return BytePipe;
    case 'I':
      return Information;
    default:
      return Unknown;
    }
  }

  const char * GuralpCompressedBlock::streamTypeString() const
  {
    switch(streamType()) {
    case Data:
      return "Data";
    case MuxChannel:
      return "MuxChannel";
    case Status:
      return "Status";
    case ChannelData:
      return "ChannelData";
    case BytePipe:
      return "BytePipe";
    case Information:
      return "Information";
    case Unknown:
      break;
    }
    return "Unknown";
  }

  Signal::Components GuralpCompressedBlock::component() const
  {
    switch(_component) {
    case 'Z':
      return Signal::Vertical;
    case 'N':
      return Signal::North;
    case 'E':
      return Signal::East;
    default:
      return Signal::UndefinedComponent;
    }
  }

  TimeRange GuralpCompressedBlock::timeRange() const
  {
    TRACE;
    return TimeRange(_startTime, _nSamples/_samplingFrequency);
  }

  void GuralpCompressedBlock::debugPrint() const
  {
    TRACE;
    printf("------ Guralp compressed block -----------\n");
    printf("System ID=%s\n",_systemID);
    printf("Device serial number=%4s\n",_device);
    printf("Stream type=%s\n",streamTypeString());
    printf("  Component=%c\n",_component);
    printf("  Channel number/Output tap=%c\n",_channel);
    printf("Start time=%s\n",_startTime.toString("yyyy-MM-dd hh:mm:ss").toLatin1().data());
    printf("Sampling frequency=%lf\n", _samplingFrequency);
    printf("Compression code=%i\n", _compressionCode);
    printf("Number of samples=%i\n", _nSamples);
  }

} // namespace GeopsyCore
