/***************************************************************************
**
**  This file is part of QGpCoreMath.
**
**  QGpCoreMath 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.
**
**  QGpCoreMath 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: 2020-04-02
**  Copyright: 2020
**    Marc Wathelet (ISTerre, Grenoble, France)
**
***************************************************************************/

#include "WindowFunction.h"

namespace QGpCoreMath {

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

    Source: http://en.wikipedia.org/wiki/Window_function
    Same notation is used here.
  */

  /*!
    Description of constructor still missing
  */
  WindowFunction::WindowFunction(int n, const WindowFunctionParameters& param)
  {
    TRACE;
    ASSERT(n>1);
    _N=n-1;
    _data=new double[n];
    _dataPtr=_data;
    double width=_N;
    switch(param.shape()) {
    case WindowFunctionParameters::Rectangular:
      for(int x=0; x<=_N; x++) {
        _data[x]=1.0;
      }
      break;
    case WindowFunctionParameters::Bartlett: {
        double n2=width*0.5;
        double invn2=1.0/n2;
        for(int x=0; x<=_N; x++) {
          _data[x]=1.0-invn2*fabs(x-n2);
        }
      }
      break;
    case WindowFunctionParameters::Triangular: {
        double n2=width*0.5;
        double invl2=2.0/(width+1.0);
        for(int x=0; x<=_N; x++) {
          _data[x]=1.0-invl2*fabs(x-n2);
        }
      }
      break;
    case WindowFunctionParameters::Parzen: {
        double n2=width*0.5;
        double n1=width+1.0;
        double invl2=2.0/n1;
        double l4=0.25*n1;
        for(int x=0; x<=_N; x++) {
          _data[x]=parzen(x-n2, invl2, l4);
        }
      }
      break;
    case WindowFunctionParameters::Welch: {
        double n2=width*0.5;
        double invn2=1.0/n2;
        for(int x=0; x<=_N; x++) {
          double a=invn2*(x-n2);
          _data[x]=1.0-a*a;
        }
      }
      break;
    case WindowFunctionParameters::Cosine: {
        double pin=M_PI/width;
        for(int x=0; x<=_N; x++) {
          _data[x]=sin(pin*x);
        }
      }
      break;
    case WindowFunctionParameters::Hann: {
        double pi2n=2.0*M_PI/width;
        for(int x=0; x<=_N; x++) {
          _data[x]=0.5*(1.0-cos(pi2n*x));
        }
      }
      break;
    case WindowFunctionParameters::Hamming: {
        double pi2n=2.0*M_PI/width;
        const double a0=25.0/46.0;
        const double a1=1.0-a0;
        for(int x=0; x<=_N; x++) {
          _data[x]=a0-a1*cos(pi2n*x);
        }
      }
      break;
    case WindowFunctionParameters::Blackman: {
        double a0=(1.0-param.alpha())*0.5;
        const double a1=0.5;
        double a2=param.alpha()*0.5;
        double f=2.0*M_PI/width;
        for(int x=0; x<=_N; x++) {
          double a=f*x;
          _data[x]=a0-a1*cos(a)+a2*cos(2.0*a);
        }
      }
      break;
    case WindowFunctionParameters::Nuttall: {
        double f=2.0*M_PI/width;
        const double a0=0.355768;
        const double a1=0.487396;
        const double a2=0.144232;
        const double a3=0.012604;
        for(int x=0; x<=_N; x++) {
          double a=f*x;
          _data[x]=a0-a1*cos(a)+a2*cos(2.0*a)-a3*cos(3.0*a);
        }
      }
      break;
    case WindowFunctionParameters::BlackmanNuttall: {
        double f=2.0*M_PI/width;
        const double a0=0.3635819;
        const double a1=0.4891775;
        const double a2=0.1365995;
        const double a3=0.0106411;
        for(int x=0; x<=_N; x++) {
          double a=f*x;
          _data[x]=a0-a1*cos(a)+a2*cos(2.0*a)-a3*cos(3.0*a);
        }
      }
      break;
    case WindowFunctionParameters::BlackmanHarris: {
        double f=2.0*M_PI/width;
        const double a0=0.35875;
        const double a1=0.48829;
        const double a2=0.14128;
        const double a3=0.01168;
        for(int x=0; x<=_N; x++) {
          double a=f*x;
          _data[x]=a0-a1*cos(a)+a2*cos(2.0*a)-a3*cos(3.0*a);
        }
      }
      break;
    case WindowFunctionParameters::FlatTop: {
        double f=2.0*M_PI/width;
        const double a0=0.21557895;
        const double a1=0.41663158;
        const double a2=0.277263158;
        const double a3=0.083578947;
        const double a4=0.006947368;
        for(int x=0; x<=_N; x++) {
          double a=f*x;
          _data[x]=a0-a1*cos(a)+a2*cos(2.0*a)-a3*cos(3.0*a)+a4*cos(4.0*a);
        }
      }
      break;
    case WindowFunctionParameters::Gaussian: {
        ASSERT(param.sigma()<=0.5);
        double n2=width*0.5;
        double invSigma=1.0/(param.sigma()*n2);
        for(int x=0; x<=_N; x++) {
          double a=invSigma*(x-n2);
          _data[x]=exp(-0.5*a*a);
        }
      }
      break;
    case WindowFunctionParameters::Tukey: {
        ASSERT(param.alpha()<=1.0);
        ASSERT(param.alpha()>0.0);
        double L=width+1.0;
        double al2=0.5*param.alpha()*L;
        double Nal2=width-al2;
        double f=M_PI/al2;
        int x;
        for(x=0; x<al2; x++) {
          _data[x]=0.5*(1.0-cos(f*x));
        }
        for(; x<=Nal2; x++) {
          _data[x]=1.0;
        }
        for(; x<=_N; x++) {
          _data[x]=0.5*(1.0-cos(f*(width-x)));
        }
      }
      break;
    case WindowFunctionParameters::BartlettHann: {
        const double a0=0.62;
        const double a1=0.48;
        const double a2=0.38;
        double f=1.0/width;
        for(int x=0; x<=_N; x++) {
          double a=f*x;
          _data[x]=a0-a1*fabs(a-0.5)-a2*cos(2.0*M_PI*a);
        }
      }
      break;
    case WindowFunctionParameters::Lanczos: {
        double f=2.0*M_PI/width;
        for(int x=0; x<=_N; x++) {
          double a=f*x-M_PI;
          _data[x]=sin(a)/a;
        }
      }
      break;
    case WindowFunctionParameters::KonnoOhmachi: {
        double logx0=log10(sqrt(_N));
        double b=konnoOhmachiExponent(1.0, _N);
        _data[0]=0.0;  // Avoid log(0)
        for(int x=1; x<=_N; x++) {
          double a=b*(log10(x)-logx0);
          if(a==0.0) {
            a=1.0;
          } else {
            a=sin(a)/a;
          }
          a*=a;
         _data[x]=a*a;
        }
      }
      break;
    }
    if(param.reversed()) {
      for(int x=0; x<=_N; x++) {
        _data[x]=1.0-_data[x];
      }
    }
    initActions(param.reversed());
  }

  /*!
    Description of destructor still missing
  */
  WindowFunction::~WindowFunction()
  {
    TRACE;
    delete [] _data;
  }

  /*!
    Return function value at any \a x.
    The window is centered around \a x0.
    The width is defined by x0-xMin
    For symetric functions, \a xMax is ignored and replaced by x0+width

    Check consistency with discretized version with check().
  */
  double WindowFunction::value(double x, double x0, double xMin, double xMax,
                               const WindowFunctionParameters& param)
  {
    double y;
    if(x<xMin || x>xMax) {
      y=0.0;
    } else {
      double width;
      switch(param.shape()) {
      case WindowFunctionParameters::KonnoOhmachi:
      case WindowFunctionParameters::Rectangular:
        break;
      default:
        width=x0-xMin;
        xMax=x0+width;
        width*=2.0;
        x-=xMin;
        break;
      }
      switch(param.shape()) {
      case WindowFunctionParameters::Rectangular:
        y=1.0;
        break;
      case WindowFunctionParameters::Bartlett: {
          double n2=width*0.5;
          double invn2=1.0/n2;
          y=1.0-invn2*fabs(x-n2);
        }
        break;
      case WindowFunctionParameters::Triangular: {
          double n2=width*0.5;
          double invl2=2.0/(width+1.0);
          y=1.0-invl2*fabs(x-n2);
        }
        break;
      case WindowFunctionParameters::Parzen: {
          double n2=width*0.5;
          double n1=width+1.0;
          double invl2=2.0/n1;
          double l4=0.25*n1;
          y=parzen(x-n2, invl2, l4);
        }
        break;
      case WindowFunctionParameters::Welch: {
          double n2=width*0.5;
          double invn2=1.0/n2;
          double a=invn2*(x-n2);
          y=1.0-a*a;
        }
        break;
      case WindowFunctionParameters::Cosine: {
          double pin=M_PI/width;
          y=sin(pin*x);
        }
        break;
      case WindowFunctionParameters::Hann: {
          double pi2n=2.0*M_PI/width;
          y=0.5*(1.0-cos(pi2n*x));
        }
        break;
      case WindowFunctionParameters::Hamming: {
          double pi2n=2.0*M_PI/width;
          const double a0=25.0/46.0;
          const double a1=1.0-a0;
          y=a0-a1*cos(pi2n*x);
        }
        break;
      case WindowFunctionParameters::Blackman: {
          double a0=(1.0-param.alpha())*0.5;
          const double a1=0.5;
          double a2=param.alpha()*0.5;
          double f=2.0*M_PI/width;
          double a=f*x;
          y=a0-a1*cos(a)+a2*cos(2.0*a);
        }
        break;
      case WindowFunctionParameters::Nuttall: {
          double f=2.0*M_PI/width;
          const double a0=0.355768;
          const double a1=0.487396;
          const double a2=0.144232;
          const double a3=0.012604;
          double a=f*x;
          y=a0-a1*cos(a)+a2*cos(2.0*a)-a3*cos(3.0*a);
        }
        break;
      case WindowFunctionParameters::BlackmanNuttall: {
          double f=2.0*M_PI/width;
          const double a0=0.3635819;
          const double a1=0.4891775;
          const double a2=0.1365995;
          const double a3=0.0106411;
          double a=f*x;
          y=a0-a1*cos(a)+a2*cos(2.0*a)-a3*cos(3.0*a);
        }
        break;
      case WindowFunctionParameters::BlackmanHarris: {
          double f=2.0*M_PI/width;
          const double a0=0.35875;
          const double a1=0.48829;
          const double a2=0.14128;
          const double a3=0.01168;
          double a=f*x;
          y=a0-a1*cos(a)+a2*cos(2.0*a)-a3*cos(3.0*a);
        }
        break;
      case WindowFunctionParameters::FlatTop: {
          double f=2.0*M_PI/width;
          const double a0=0.21557895;
          const double a1=0.41663158;
          const double a2=0.277263158;
          const double a3=0.083578947;
          const double a4=0.006947368;
          double a=f*x;
          y=a0-a1*cos(a)+a2*cos(2.0*a)-a3*cos(3.0*a)+a4*cos(4.0*a);
        }
        break;
      case WindowFunctionParameters::Gaussian: {
          ASSERT(param.sigma()<=0.5);
          double n2=width*0.5;
          double invSigma=1.0/(param.sigma()*n2);
          double a=invSigma*(x-n2);
          y=exp(-0.5*a*a);
        }
        break;
      case WindowFunctionParameters::Tukey: {
          ASSERT(param.alpha()<=1.0);
          ASSERT(param.alpha()>0.0);
          double L=width+1.0;
          double al2=0.5*param.alpha()*L;
          double Nal2=width-al2;
          double f=M_PI/al2;
          if(x<al2) {
            y=0.5*(1.0-cos(f*x));
          } else if(x<Nal2) {
            y=1.0;
          } else {
            y=0.5*(1.0-cos(f*(width-x)));
          }
        }
        break;
      case WindowFunctionParameters::BartlettHann: {
          const double a0=0.62;
          const double a1=0.48;
          const double a2=0.38;
          double f=1.0/width;
          double a=f*x;
          y=a0-a1*fabs(a-0.5)-a2*cos(2.0*M_PI*a);
        }
        break;
      case WindowFunctionParameters::Lanczos: {
          double f=2.0*M_PI/width;
          double a=f*x-M_PI;
          y=sin(a)/a;
        }
        break;
      case WindowFunctionParameters::KonnoOhmachi:
        if(x<=0.0) { // Avoid log(0)
          y=0.0;
        } else {
          double logx0=log10(x0);
          double b=konnoOhmachiExponent(xMin, xMax);
          double a=b*(log10(x)-logx0);
          if(a==0.0) {
            y=1.0;
          } else {
            a=sin(a)/a;
            a*=a;
            y=a*a;
          }
        }
        break;
      }
    }
    if(param.reversed()) {
      y=1.0-y;
    }
    return y;
  }

  bool WindowFunction::check()
  {
    WindowFunctionParameters param;
    param.setAlpha(0.2);
    for(int i=WindowFunctionParameters::Rectangular;
        i<=WindowFunctionParameters::KonnoOhmachi; i++) {
      param.setShape(static_cast<WindowFunctionParameters::Shape>(i));
      App::log(tr("Testing %1...\n").arg(param.shapeString()));
      int width=50;
      WindowFunction f(width, param);
      double n1=width-1.0;
      double n2=0.5*n1;
      for(int j=0; j<width; j++) {
        double valInt=f.value(j);
        double valDouble=value(j, n2, 0.0, n1, param);
        if(valInt!=valDouble) {
          App::log(tr("  error at %1: %2 (int) != %3 (double)\n")
                   .arg(j).arg(valInt).arg(valDouble));
          return false;
        }
      }
    }
    return true;
  }

  /*!
    If sinc argument is larger than M_PI, the function oscillates.
    So we define the width as the location of first zero at M_PI.

    M_PI=log10((x/x0)^b)
  */
  double WindowFunction::konnoOhmachiExponent(double xMin, double xMax)
  {
    return 2.0*M_PI/log10(xMax/xMin);
  }

  double WindowFunction::parzen(double x, const double& invl2, const double& l4)
  {
    double an=fabs(x);
    double b=1.0-an*invl2;
    if(an<=l4) {
      double a=x*invl2;
      return 1.0-6.0*a*a*b;
    } else {
      return 2.0*b*b*b;
    }
  }

  /*!
    Identify constant ranges (0 or 1) and create corresponding actions.
  */
  void WindowFunction::initActions(bool reversed)
  {
    int actionBegin=0;
    Action::Type outsideType;
    if(reversed) {
      outsideType=Action::Untouch;
    } else {
      outsideType=Action::Vanish;
    }
    Action::Type type=outsideType;
    _actions.clear();

    for(int i=0; i<=_N; i++) {
      if(_data[i]==0.0) {
        if(type!=Action::Vanish) {
          _actions.append(Action(type, actionBegin, i-1));
          type=Action::Vanish;
          actionBegin=i;
        }
      } else if(_data[i]==1.0) {
        if(type!=Action::Untouch) {
          _actions.append(Action(type, actionBegin, i-1));
          type=Action::Untouch;
          actionBegin=i;
        }
      } else {
        if(type!=Action::Multiply) {
          _actions.append(Action(type, actionBegin, i-1));
          type=Action::Multiply;
          actionBegin=i;
        }
      }
    }
    if(type!=outsideType) {
      _actions.append(Action(type, actionBegin, _N));
      actionBegin=_N+1;
    }
    _actions.append(Action(outsideType, actionBegin, _N));
  }

  QString WindowFunction::Action::toString() const
  {
    QString t;
    switch(_type) {
    case Vanish:
      t="Vanish";
      break;
    case Untouch:
      t="Untouch";
      break;
    case Multiply:
      t="Multiply";
      break;
    }
    return QString("%1 from %2 to %3 (%4 values)")
        .arg(t)
        .arg(_startIndex)
        .arg(_endIndex)
        .arg(_endIndex-_startIndex+1);
  }

  QString WindowFunction::actionToString() const
  {
    QString s;
    for(int i=0; i<_actions.count(); i++) {
      s+=_actions.at(i).toString()+"\n";
    }
    return s;
  }

  QString WindowFunction::toString() const
  {
    QString s;
    for(int i=0; i<=_N; i++) {
      s+=QString("%1 %2\n").arg(i).arg(_data[i]);
    }
    return s;
  }

  /*!
    Return the correction factor corresponding to the loss of energy from taper shape.
    This correction factor is to required for PSD computation, which is based on a rectangular
    window.
    For instance for a 10% Tukey window, the correction factor is 1.142857 (see McNamara 2004)
  */
  double WindowFunction::correctionFactor() const
  {
    TRACE;
    double sum=0.0, val;
    for(int i=0; i<_N; i++) {
      val=_data[i];
      sum+=val*val;
    }
    return _N/sum;
  }

  /*!
    Must be called after setStartIndex()

    Remove action outside range and make sure that no action will be
    executed outside \a beginIndex and \a endIndex.
  */
  void WindowFunction::setSignalRange(int beginIndex, int endIndex)
  {
    _actions.first().setStart(beginIndex);
    _actions.last().setEnd(endIndex);
    int i=0;
    while(i<_actions.count()) {
      Action& action=_actions[i];
      if(action.end()<beginIndex ||
         action.start()>endIndex) {
        _actions.remove(i);
      } else {
        if(action.start()<beginIndex) {
          action.setStart(beginIndex);
        }
        if(action.end()>endIndex) {
          action.setEnd(endIndex);
        }
        i++;
      }
    }
  }

  void WindowFunction::setStartIndex(int beginIndex)
  {
    int n=_actions.count();
    for(int i=0; i<n; i++) {
      _actions[i].add(beginIndex);
    }
    _dataPtr=_data-beginIndex;
  }

} // namespace QGpCoreMath

