/***************************************************************************
**
**  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: 2006-12-11
**  Copyright: 2006-2019
**    Marc Wathelet
**    Marc Wathelet (LGIT, Grenoble, France)
**
***************************************************************************/

#include "TimeRangeList.h"
#include "SparseKeepSignal.h"
#include "WindowingParameters.h"
#include "KeepSignal.h"
#include "SeismicEventTable.h"

namespace GeopsyCore {

/*!
  \class TimeRangeList TimeRangeList.h
  \brief List of time ranges

  Subclass this class to store time ranges with more features (e.g. color) and
  re-implement newTimeRange(), cloneTimeRange(), deleteTimeRange(). TimeRange is
  not virtual to keep it as basic as possible.
*/

TimeRangeList::TimeRangeList(const TimeRangeList& o, int firstIndex)
  : QList<TimeRange *>()
{
  TRACE;
  int n=o.count();
  for(int i=firstIndex;i<n;i++) {
    const TimeRange& r=o.at(i);
    append(newTimeRange(r.start(), r.end()));
  }
  _verbosityLevel=1;
}

/*!
  This function must be re-impleted in all subclasses. Virtual mechanism for deleteTimeRange() does
  not work for destructors. In each subclass, the list must be cleared to avoid re-delete in lower
  level classes.
*/
TimeRangeList::~TimeRangeList()
{
  TRACE;
  for(iterator it=begin(); it!=end(); ++it) {
    delete *it;
  }
  internalClear();
}

/*!

*/
TimeRange TimeRangeList::timeRange() const
{
  TRACE;
  if(isEmpty()) return TimeRange();
  const_iterator it=begin();
  TimeRange globalRange=**it;
  for(it++;it!=end();++it) {
    const TimeRange& r=**it;
    if(r.start()<globalRange.start()) {
      globalRange.setStart(r.start());
    }
    if(r.end()>globalRange.end()) {
      globalRange.setEnd(r.end());
    }
  }
  return globalRange;
}

/*!
  Returns the length in seconds of the longest time window
*/
double TimeRangeList::longestWindow() const
{
  TRACE;
  double dtmax=0.0;
  for(const_iterator it=begin();it!=end();++it) {
    double dt=(*it)->lengthSeconds();
    if(dt > dtmax)
      dtmax=dt;
  }
  return dtmax;
}

/*!
  Add windows according to \a param in available blanks
*/
void TimeRangeList::addBlanks(double frequency, const WindowingParameters& param, const SparseKeepSignal& keep,
                              const TimeRange& r)
{
  TRACE;
  SparseKeepSignal localKeep=keep;
  localKeep.copySamplesFrom(keep);
  blanksToKeep(localKeep, param.overlap());
  add(frequency, param, localKeep, r);
}

/*!
  Add windows in \a tw time interval according to windowing parameters \a param and keep vectors \a keeps.
*/
void TimeRangeList::add(double frequency, const WindowingParameters& param, const SparseKeepSignal& keep,
                        const TimeRange& r)
{
  TRACE;
  //App::log(tr("add time windows from %1 to %2\n").arg(r.start()).arg(r.end()) );
  const QVector<KeepSignal *>& keeps=keep.keeps();
  for(QVector<KeepSignal *>::const_iterator it=keeps.begin(); it!=keeps.end(); it++) {
    const KeepSignal * keepSig=*it;

    double samplingPeriod=keepSig->samplingPeriod();
    double freqSamp=1.0/samplingPeriod;

    int nMin=qRound(param.minimumLength(frequency)*freqSamp);
    int nMax=qRound(param.maximumLength(frequency)*freqSamp);
    if(nMin==nMax && param.powerOfTwo()) {
      nMin=Number::nextPowerOfTwo(nMin);
      nMax=nMin;
    }
    if(nMax<=0) {
      continue;
    }
    int nOverlap=qRound((param.overlap()*0.01)*(double)nMax);
    int nBadTol=qRound(param.badSampleTolerance()*freqSamp);
    int nBadGap=qRound(param.badSampleGap()*freqSamp);
    // Gap and tolerance are mutualy exclusive
    ASSERT((nBadTol>=0 && nBadGap==0) || (nBadTol==0 && nBadGap>=0));

    int nStart=qRound(keepSig->startTime().secondsTo(r.start())*freqSamp);
    if(nStart>keepSig->nSamples()) {
      continue;
    } else if(nStart<0) {
      nStart=0;
    }
    int nEnd=qRound(keepSig->startTime().secondsTo(r.end())*freqSamp);
    if(nEnd<0) {
      continue;
    } else if(nEnd>keepSig->nSamples()) {
      nEnd=keepSig->nSamples();
    }
    //printf("nStart %i nEnd %i\n",nStart, nEnd);
    CONST_LOCK_SAMPLES(int, kSamp, keepSig)
      int i=nStart;
      int * badSamples=new int[nMax];
      int nBadSample, winStart, winLength;
      bool lastSampleIsBad=false;
      while(i<nEnd && (param.maximumWindowCount()==0 || count()<param.maximumWindowCount())) {
        int iCurrent=i;
        while(i<nEnd && kSamp[i]==-1) { // find beginning of first window
          i++;
        }
        int acceptableSample=i;
        while(i<nEnd && kSamp[i]==0) { // finds beginning of first window
          i++;
        }
        if(i>iCurrent) {
          lastSampleIsBad=true;
        }
        i-=nBadTol; // Admits nbad_tol bad samples so go back
        if(i<acceptableSample) {
          i=acceptableSample;
        }
        if(nBadGap>0 && lastSampleIsBad) { // Tries to find a clean section of nBadGap long
          int n;
          do {
            n=i+nBadGap;
            if(n>=nEnd) {
              break;
            }
            while(i<n && kSamp[i]==1) {
              i++;
            }
          } while(i<n);
          lastSampleIsBad=false;
        }
        nBadSample=0;
        winStart=-1;
        winLength=0;
        // Try to find a window at least with ntmin samples and with less than
        // bad_tol number of bad samples
        do {
          if(winStart==-1) {
            winStart=i;
          } else {
            // Restarting because n_bad_sample==bad_tol and i-start_w+1<ntmin
            // This means a too short window with too much bad samples
            // Restart window at the first bad_sample
            winLength-=badSamples[0]+1-winStart;
            winStart=badSamples[0]+1;
            // move all bad samples in the vector
            for(int j=1; j<nBadSample; j++) {
              badSamples[j-1]=badSamples[j];
            }
            nBadSample--;
            if(nBadGap>0) { // Tries to find a clean section of nBadGap long
              ASSERT(nBadSample==0 && winStart==i && winLength==0);
              int n=i;
              do {
                if(i<n) {
                  i++;
                }
                n=i+nBadGap;
                if(n>=nEnd) {
                  break;
                }
                while(i<n && kSamp[i]==1) {
                  i++;
                }
              } while(i<n);
              winStart=i;
              lastSampleIsBad=false;
            }
            //printf("Restarting i=%i winStart=%i winLength=%i nBadSample=%i\n",i,
            //     winStart,winLength,nBadSample);
          }
          while(i<nEnd && nBadSample<=nBadTol && winLength<nMax+nBadGap) {
            if(kSamp[i]==0) {
              badSamples[nBadSample]=i;
              nBadSample++;
              //printf("Found bad sample at %i, nBadSample=%i\n",i,nBadSample);
            } else if(kSamp[i]==-1) {
              // This sample cannot be tolerated
              //printf("Found very bad sample at %i, restarting\n",i);
              lastSampleIsBad=true;
              break;
            }
            winLength++;
            i++;
          }
          if(kSamp[i]==-1) {
            break;
          }
        } while(i<nEnd && winLength<nMin);
        // Found a window
        if(winLength<nMax+nBadGap && i<nEnd && nBadGap>0) {
          winLength-=nBadGap;
          lastSampleIsBad=true;
        } else {
          winLength-=nBadGap;
          if(i<nEnd) {
            i-=nBadGap;
          }
          lastSampleIsBad=false;
        }
        if(winLength>=nMin) {
          TimeRange * newRange=newTimeRange(keepSig->startTime().shifted(winStart*samplingPeriod),
                                            winLength*samplingPeriod);
          //App::log(tr("good win: start=%1 end=%2\n").arg(newRange->start()).arg(newRange->end()) << flush;
          append(newRange);
          App::log(_verbosityLevel, tr("Adding window from %1 to %2 s (duration %3 s).\n")
                                          .arg(newRange->start().toString())
                                          .arg(newRange->end().toString())
                                          .arg(newRange->lengthSeconds(), 0, 'f', 2));
          // Due to overlap, the next window can start before current i
          int effOverlap=nOverlap-1;
          if(effOverlap>=winLength) {
            effOverlap=winLength-1;
          }
          i-=effOverlap;
        }
      }
      delete [] badSamples;
    UNLOCK_SAMPLES(keepSig)
  }
}

/*!
  Add windows starting at all events occuring during \a r if \a param option seismicEventTrigger is true.
  If not, the general add is used.
*/
void TimeRangeList::add(double frequency, const WindowingParameters& param, const SparseKeepSignal& keep,
                        const TimeRange& r, const SeismicEventTable * events)
{
  TRACE;
  if(param.seismicEventTrigger()) {
    double winMaxLen=param.maximumLength(frequency);
    QList<const SeismicEvent *> list=events->events(r);
    for(QList<const SeismicEvent *>::iterator it=list.begin(); it!=list.end(); it++) {
      add(frequency, param, keep,
          TimeRange((*it)->time().shifted(param.seismicEventDelay()), winMaxLen));
    }
  } else {
    add(frequency, param, keep, r);
  }
}

/*!
  \a endPattern is a regular expression
*/
bool TimeRangeList::add(QTextStream& s, const QString& beginPattern, const QString& endPattern)
{
  TRACE;
  File::readUntilPattern(s, beginPattern);
  QString line;
  while(!s.atEnd()) {
    line=s.readLine().simplified();
    if(!endPattern.isEmpty() && line.contains(endPattern)) {
      return true;
    }
    while(!s.atEnd() && line.left(1)=="#") {
      line=s.readLine().trimmed().simplified();
      if(!endPattern.isEmpty() && line.contains(endPattern)) {
        return true;
      }
    }
    DateTime winStart, winEnd;
    bool ok;
    ok=winStart.fromString(line.section(QRegExp("[ \t]"), 0, 0));
    ok&=winEnd.fromString(line.section(QRegExp("[ \t]"), 1, 1));
    if(ok && winStart<winEnd) {
      append(newTimeRange(winStart, winEnd));
      App::log(_verbosityLevel, tr("Adding window from %1 to %2 s.\n")
               .arg(winStart.toString())
               .arg(winEnd.toString()));
    } else {
      App::log(tr("Adding windows: error reading line '%1'\n").arg(line));
      return false;
    }
  }
  return true;
}

void TimeRangeList::add(const TimeRangeList& list)
{
  TRACE;
  for(const_iterator it=list.begin();it!=list.end();++it) {
    append(cloneTimeRange(*it));
  }
}

/*!
  Remove all time windows inside or partially inside \a tw.
*/
void TimeRangeList::remove(const TimeRange& globalRange)
{
  TRACE;
  for(int i=0;i < count(); ) {
    TimeRange& r=at(i);
    if(r.intersects(globalRange)) {
      App::log(_verbosityLevel, tr("Removing window from %1 to %2 s.\n")
               .arg(r.start().toString())
               .arg(r.end().toString()));
      deleteTimeRange(&r);
      removeAt(i);
    } else
      ++i;
  }
}

void TimeRangeList::remove(int index)
{
  TRACE;
  TimeRange& r=at(index);
  App::log(_verbosityLevel, tr("Removing window from %1 to %2 s.")
           .arg(r.start().toString())
           .arg(r.end().toString()));
  deleteTimeRange(&r);
  QList<TimeRange *>::removeAt(index);
}

void TimeRangeList::inverse(double frequency, const WindowingParameters& param, const SparseKeepSignal& keep,
                            const TimeRange& r)
{
  TRACE;
  SparseKeepSignal localKeep=keep;
  localKeep.initValues(1);
  blanksToKeep(localKeep, param.overlap());
  clear();
  App::log(_verbosityLevel, tr("Inverse windows\n"));
  add(frequency, param, localKeep, r);
}

/*!
  Ideally \a keep must be initialized with values of 1. For each time window, the corresponding
  keep samples are set to 0 (taking overlap into account).
*/
void TimeRangeList::blanksToKeep(SparseKeepSignal& keep, double overlap) const
{
  TRACE;
  //GeopsyCoreEngine::instance()->showSignal(keep.keeps().at(0), "before", "blanksToKeep");
  double overlapRatio=overlap * 0.01;
  for(const_iterator it=begin();it!=end();++it) {
    const TimeRange& r=**it;
    double overlapLength=overlapRatio * r.lengthSeconds();
    keep.initValues(0, r.start().shifted(overlapLength), r.end().shifted(-overlapLength));
  }
  //GeopsyCoreEngine::instance()->showSignal(keep.keeps().at(0), "after", "blanksToKeep");
}

QString TimeRangeList::toString() const
{
  TRACE;
  QString str;
  str+=tr("# Number= %1\n").arg(count());
  str+=tr("# Start time \t End Time \t Window length\n");
  for(const_iterator it=begin();it!=end();++it) {
    TimeRange& w=**it;
    str+=QString("%1\t%2\t%3\n")
        .arg(w.start().toString())
        .arg(w.end().toString())
        .arg(w.lengthSeconds());
  }
  return str;
}

/*!
  Returns false if it is already empty
*/
bool TimeRangeList::clear()
{
  TRACE;
  if(isEmpty()) {
    return false;
  } else {
    for(iterator it=begin(); it!=end(); ++it) {
      deleteTimeRange(*it);
    }
    internalClear();
    return true;
  }
}

} // namespace GeopsyCore
