/***************************************************************************
**
**  This file is part of QGpCoreMath.
**
**  This library is free software; you can redistribute it and/or
**  modify it under the terms of the GNU Lesser General Public
**  License as published by the Free Software Foundation; either
**  version 2.1 of the License, or (at your option) any later version.
**
**  This file 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 Lesser General Public
**  License for more details.
**
**  You should have received a copy of the GNU Lesser General Public
**  License along with this library; if not, write to the Free Software
**  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
**
**  See http://www.geopsy.org for more information.
**
**  Created: 2004-10-21
**  Copyright: 2004-2019
**    Marc Wathelet
**    Marc Wathelet (ULg, Liège, Belgium)
**    Marc Wathelet (LGIT, Grenoble, France)
**
***************************************************************************/

#ifndef STATISTICALVALUE_H
#define STATISTICALVALUE_H

#include <math.h>
#include <QGpCoreTools.h>

#include "Value.h"
#include "QGpCoreMathDLLExport.h"

namespace QGpCoreMath {

enum MisfitType {L1, L1_Normalized, L1_LogNormalized, L1_NormalizedBySigmaOnly,
                 L2, L2_Normalized, L2_LogNormalized, L2_NormalizedBySigmaOnly,
                 RMS, Akaike, AkaikeFewSamples, Determinant};

template <class numberType>
class QGPCOREMATH_EXPORT StatisticalValue : public XMLClass
{
public:
  inline StatisticalValue();
  inline StatisticalValue (const numberType& mean, double stddev=0.0, double weight=1.0, bool valid=true);

  inline void operator=(const StatisticalValue& o);
  inline bool operator==(const StatisticalValue& o) const;

  void setMean(const numberType& v) {_mean=v;}
  void setStddev(double v) {_stddev=v;}
  void setWeight(double v) {_weight=v; if(_weight==0.0) _valid=false;}

  const numberType& mean() const {return _mean;}
  double stddev() const {return _stddev;}
  double weight() const {return _weight;}

  void setValid(bool f) {_valid=f; if(_valid && _weight==0.0) _weight=1.0;}
  bool isValid() const {return _valid;}

  static QString misfitTypeString(MisfitType type);
  static MisfitType misfitType(QString type);

  double misfit(int& nValues, int& nData, const Value<numberType>& value, MisfitType type, double min) const;

  inline void average(const StatisticalValue& p);
protected:
  numberType _mean;
  double _stddev;
  double _weight;
  bool _valid;
protected:
  virtual void xml_writeProperties(XML_WRITEPROPERTIES_ARGS) const;
  virtual XMLMember xml_member(XML_MEMBER_ARGS);
  virtual bool xml_setProperty(XML_SETPROPERTY_ARGS);
};

class QGPCOREMATH_EXPORT RealStatisticalValue : public StatisticalValue<double>
{
public:
  RealStatisticalValue() : StatisticalValue<double>() {}
  RealStatisticalValue (const double& mean, double stddev=0.0, double weight=1.0, bool valid=true)
    : StatisticalValue<double>(mean, stddev, weight, valid) {}

  virtual const QString& xml_tagName() const {return xmlRealStatisticalValueTag;}
  static const QString xmlRealStatisticalValueTag;
};

class QGPCOREMATH_EXPORT ComplexStatisticalValue : public StatisticalValue<Complex>
{
public:
  ComplexStatisticalValue() : StatisticalValue<Complex>() {}
  ComplexStatisticalValue (const double& mean, double stddev=0.0, double weight=1.0, bool valid=true)
    : StatisticalValue<Complex>(mean, stddev, weight, valid) {}

  virtual const QString& xml_tagName() const {return xmlComplexStatisticalValueTag;}
  static const QString xmlComplexStatisticalValueTag;
};

template <class numberType>
inline StatisticalValue<numberType>::StatisticalValue()
{
  TRACE;
  _mean=0.0;
  _stddev=0.0;
  _weight=1.0;
  _valid=true;
}

template <class numberType>
inline StatisticalValue<numberType>::StatisticalValue(const numberType& mean, double stddev,
                                                      double weight, bool valid)
{
  TRACE;
  _mean=mean;
  _stddev=stddev;
  _weight=weight;
  _valid=valid;
}

template <class numberType>
inline void StatisticalValue<numberType>::operator=(const StatisticalValue<numberType>& o)
{
  TRACE;
  _mean=o._mean;
  _stddev=o._stddev;
  _weight=o._weight;
  _valid=o._valid;
}

template <class numberType>
inline bool StatisticalValue<numberType>::operator==(const StatisticalValue<numberType>& o) const
{
  return _mean==o._mean && _stddev==o._stddev && _weight==o._weight && _valid==o._valid;
}

template <class numberType>
inline void StatisticalValue<numberType>::average(const StatisticalValue<numberType>& o)
{
  TRACE;
  if(isValid()) {
    if(o.isValid()) {
      double wt=_weight+o._weight;
      double sigma2=_weight*abs2(_mean);
      _mean=(_mean*_weight+o._mean*o._weight)/wt;
      if(_weight>1.0) {
        sigma2+=(_weight-1.0)*_stddev*_stddev;
      } else {
        sigma2+=_weight*_stddev*_stddev;
      }
      sigma2+=o._weight*abs2(o._mean);
      if(o._weight>1.0) {
        sigma2+=(o._weight-1.0)*o._stddev*o._stddev;
      } else {
        sigma2+=o._weight*o._stddev*o._stddev;
      }
      sigma2-=QGpCoreTools::abs2(_mean)*wt;
      if(wt>1.0) {
        sigma2/=wt-1.0;
      } else {
        sigma2/=wt;
      }
      _stddev=::sqrt(sigma2);
      _weight=wt;
    }
  } else if(o.isValid()) {
    *this=o;
  }
}

template <class numberType>
void StatisticalValue<numberType>::xml_writeProperties(XML_WRITEPROPERTIES_ARGS) const
{
  TRACE;
  Q_UNUSED(context)
  writeProperty(s,"mean",_mean);
  writeProperty(s,"stddev",_stddev);
  writeProperty(s,"weight",_weight);
  writeProperty(s,"valid",_valid);
}

template <class numberType>
XMLMember StatisticalValue<numberType>::xml_member(XML_MEMBER_ARGS)
{
  TRACE;
  Q_UNUSED(attributes)
  Q_UNUSED(context)
  if(tag=="mean") return XMLMember(0);
  else if(tag=="stddev") return XMLMember(1);
  else if(tag=="weight") return XMLMember(2);
  else if(tag=="valid") return XMLMember(3);
  else return false;
}

template <class numberType>
bool StatisticalValue<numberType>::xml_setProperty(XML_SETPROPERTY_ARGS)
{
  TRACE;
  Q_UNUSED(tag)
  Q_UNUSED(attributes)
  Q_UNUSED(context)
  switch (memberID) {
  case 0: {
      numberType tmp;
      SAFE_UNINITIALIZED(tmp, 0);
      _mean=content.to(tmp);
    }
    return true;
  case 1: _stddev=content.toDouble(); return true;
  case 2: _weight=content.toDouble(); return true;
  case 3: _valid=content.toBool(); return true;
  default: return false;
  }
}

template <class numberType>
QString StatisticalValue<numberType>::misfitTypeString(MisfitType type)
{
  TRACE;
  switch (type) {
  case L1:
    return "L1";
  case L1_Normalized:
    return "L1_Normalized";
  case L1_LogNormalized:
    return "L1_LogNormalized";
  case L1_NormalizedBySigmaOnly:
    return "L1_NormalizedBySigmaOnly";
  case L2:
    return "L2";
  case L2_Normalized:
    return "L2_Normalized";
  case L2_LogNormalized:
    return "L2_LogNormalized";
  case L2_NormalizedBySigmaOnly:
    return "L2_NormalizedBySigmaOnly";
  case Akaike:
    return "Akaike";
  case AkaikeFewSamples:
    return "AkaikeFewSamples";
  case RMS:
    return "RMS";
  case Determinant:
    return "Determinant";
  }
  return QString();
}

template <class numberType>
MisfitType StatisticalValue<numberType>::misfitType(QString type)
{
  TRACE;
  if(type.count()>1) {
    switch(type[1].unicode()) {
    case '1':
      if(type=="L1") return L1; else return L1_Normalized;
    case '2':
      if(type=="L2") return L2;
      else if(type=="L2_Normalized") return L2_Normalized;
      else return L2_NormalizedBySigmaOnly;
    case 'm':
      return RMS;
    case 'e':
      return Determinant;
    case 'k':
      if(type=="Akaike") return Akaike; else return AkaikeFewSamples;
      break;
    default:
      break;
    }
  }
  return L2_Normalized;
}

template <class numberType>
double StatisticalValue<numberType>::misfit(int& nValues, int& nData,
                                            const Value<numberType>& val,
                                            MisfitType type, double min) const
{
  TRACE;
  if(isValid()) {
    nData++;
    if(val.isValid()) {
      nValues++;
      double diff;
      switch (type) {
      case L1:
        diff=QGpCoreTools::abs(_mean-val.value());
        if(diff<min) {
          diff=min;
        }
        return diff;
      case L1_Normalized:
        if(_stddev>0.0) {
          diff=QGpCoreTools::abs((_mean-val.value())/_stddev);
        } else {
          diff=QGpCoreTools::abs((_mean-val.value())/_mean);
        }
        if(diff<min) {
          diff=min;
        }
        return diff;
      case L1_LogNormalized:
        if(_stddev>1.0) {
          diff=QGpCoreTools::abs((log(_mean/val.value()))/log(_stddev));
        } else {
          double m=QGpCoreTools::abs(_mean);
          double v=QGpCoreTools::abs(val.value());
                       // For complex numbers things are probably not correct... to be checked.
          if(m>v) {    // which is strictly equivalent to exp(abs(log(_mean/val.value()))
            diff=m/v;
          } else {
            diff=v/m;
          }
          diff-=1.0;
        }
        if(diff<min) {
          diff=min;
        }
        return diff;
      case L1_NormalizedBySigmaOnly:
        if(_stddev>0.0) {
          diff=QGpCoreTools::abs((_mean-val.value())/_stddev);
        } else {
          diff=QGpCoreTools::abs(_mean-val.value());
        }
        if(diff<min) {
          diff=min;
        }
        return diff;
      case L2:
      case RMS:
        diff=QGpCoreTools::abs(_mean-val.value());
        if(diff<min) {
          diff=min;
        }
        return diff*diff;
      case Akaike:
      case AkaikeFewSamples:
      case L2_Normalized:
        if(_stddev>0.0) {
          diff=QGpCoreTools::abs((_mean-val.value())/_stddev);
        } else {
          diff=QGpCoreTools::abs((_mean-val.value())/_mean);
        }
        if(diff<min) {
          diff=min;
        }
        return diff*diff;
      case L2_LogNormalized:
        if(_stddev>1.0) {
          diff=QGpCoreTools::abs((log(_mean/val.value()))/log(_stddev));
        } else {
          double m=QGpCoreTools::abs(_mean);
          double v=QGpCoreTools::abs(val.value());
                       // For complex numbers things are probably not correct... to be checked.
          if(m>v) {    // which is strictly equivalent to exp(abs(log(_mean/val.value()))
            diff=m/v;
          } else {
            diff=v/m;
          }
          diff-=1.0;
        }
        if(diff<min) {
          diff=min;
        }
        return diff*diff;
      case L2_NormalizedBySigmaOnly:
        if(_stddev>0.0) {
          diff=QGpCoreTools::abs((_mean-val.value())/_stddev);
        } else {
          diff=QGpCoreTools::abs(_mean-val.value());
        }
        if(diff<min) {
          diff=min;
        }
        return diff*diff;
      case Determinant:
        return 0.0;
      }
      return 0.0;
    } else {
      return 0.0;
    }
  } else {
    return 0.0;
  }
}

template <class numberType>
QDataStream& operator>> (QDataStream& s, StatisticalValue<numberType>& p)
{
  TRACE;
  numberType m;
  s >> m;
  p.setMean(m);
  double v;
  s >> v;
  p.setStddev(v);
  s >> v;
  p.setWeight(v);
  return s;
}

template <class numberType>
QDataStream& operator<< (QDataStream& s, const StatisticalValue<numberType>& p)
{
  TRACE;
  s << p.mean() << p.stddev() << p.weight();
  return s;
}

} // namespace QGpCoreMath

#endif // STATISTICALVALUE_H
