/***************************************************************************
**
**  This file is part of DinverDCCore.
**
**  DinverDCCore 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.
**
**  DinverDCCore 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: 2004-10-18
**  Copyright: 2004-2019
**    Marc Wathelet
**    Marc Wathelet (ULg, Liège, Belgium)
**    Marc Wathelet (LGIT, Grenoble, France)
**
***************************************************************************/

#include <DinverCore.h>

#include "ParamLayer.h"
#include "ParamProfile.h"
#include "GroundParameter.h"
#include "BottomValueCondition.h"
#include "UniformProfile.h"
#include "LinearProfile.h"
#include "PowerLawProfile.h"

namespace DinverDCCore {

const QString ParamLayer::xmlParamLayerTag="ParamLayer";

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

  Full description of class still missing
*/

/*!
  Copy constructor. Profile ownership is left free. Used only by
  copy constructor of ParamProfile.
*/
ParamLayer::ParamLayer(const ParamLayer& o)
  : XMLClass()
{
  _profile=nullptr;
  _index=o._index;

  initParameters();

  _shape=o._shape;
  _gradientParameter=o._gradientParameter;
  _topMin=o._topMin;
  _topMax=o._topMax;
  _bottomMin=o._bottomMin;
  _bottomMax=o._bottomMax;
  _lastParamCondition=o._lastParamCondition;

  _linkedTo=o._linkedTo;
  _isDepth=o._isDepth;
  _dhMin=o._dhMin;
  _dhMax=o._dhMax;

  _topDepthIndex=o._topDepthIndex;
  _gradientProfile=o._gradientProfile->clone();
}

/*!
  Constructs a default layer. Default options are acquired from \a profile.
*/
ParamLayer::ParamLayer(ParamProfile * profile, int index)
{
  TRACE;
  _profile=profile;
  _index=index;

  initParameters();
  _topDepthIndex=0;

  _shape=Uniform;
  //_gradientParameter=BottomValue;
  _gradientParameter=GradientValue;
  _gradientProfile=new UniformProfile;

  _topMin=profile->defaultMinimum();
  _topMax=profile->defaultMaximum();
  _bottomMin=_topMin;
  _bottomMax=_topMax;
  _lastParamCondition=true;

  _linkedTo="Not linked";
  _isDepth=true;
  _dhMin=1.0;
  _dhMax=100.0;
}

/*!
  Create a new layer with a uniform value between \a minValue and \a maxValue. The depth is linked to profile \a linkedTo.
  Basically used to build a ground model from a Seismic1DModel.
*/
ParamLayer::ParamLayer(ParamProfile * profile, int index, double minValue, double maxValue, ParamProfile * linkedTo)
{
  TRACE;
  _profile=profile;
  _index=index;

  initParameters();
  _topDepthIndex=0;

  _shape=Uniform;
  _gradientParameter=GradientValue;
  _gradientProfile=new UniformProfile;

  _lastParamCondition=false;
  _topMin=minValue;
  _topMax=maxValue;
  _bottomMin=minValue;
  _bottomMax=maxValue;
  _linkedTo=name(linkedTo->shortName(), index);
  _isDepth=true;
  _dhMin=0.0;
  _dhMax=0.0;
}

/*!
  Create a new layer with a uniform value betweem \a minValue and \a maxValue. The depth can vary from \a minDepth to \a maxDepth.
  Basically used to build a ground model from a Seismic1DModel.
*/
ParamLayer::ParamLayer(ParamProfile * profile, int index, double minValue, double maxValue, double minDepth, double maxDepth)
{
  TRACE;
  _profile=profile;
  _index=index;

  initParameters();
  _topDepthIndex=0;

  _shape=Uniform;
  _gradientParameter=GradientValue;
  _gradientProfile=new UniformProfile;

  _lastParamCondition=false;
  _topMin=minValue;
  _topMax=maxValue;
  _bottomMin=minValue;
  _bottomMax=maxValue;
  _linkedTo="Not linked";
  _isDepth=true;
  _dhMin=minDepth;
  _dhMax=maxDepth;
}

ParamLayer::~ParamLayer()
{
  delete _gradientProfile;
}

void ParamLayer::initParameters()
{
  TRACE;
  _ptrLink=nullptr;
  _topP=nullptr;
  _gradientP=nullptr;
  _bottomP=nullptr;
  _dhP=nullptr;
}

/*!
  Set link name from link pointer. Used after inserting a new layer
*/
void ParamLayer::setLinkedTo()
{
  TRACE;
  if(_ptrLink) {
    _linkedTo=_ptrLink->name();
  }
}

QString ParamLayer::name() const
{
  TRACE;
  return name(_profile->shortName(), _index);
}

QString ParamLayer::unit() const
{
  TRACE;
  return _profile->unit();
}

void ParamLayer::setShape(Shape s)
{
  TRACE;
  if(s!=_shape) {
    _shape=s;
    int n=_gradientProfile->subLayerCount();
    delete _gradientProfile;
    if(n<=1) {
      n=10;
    }
    switch(_shape) {
    case Uniform:
      _gradientProfile=new UniformProfile;
      break;
    case Linear:
    case LinearIncrease:
    case LinearDecrease:
      _gradientProfile=new LinearProfile(n);
      break;
    case PowerLaw:
      _gradientProfile=new PowerLawProfile(n);
      break;
    }
  }
}

bool ParamLayer::valueParameters(RealSpace& ps, const ParamGroundModel * groundModel)
{
  TRACE;
  _topP=ps.addParameter(new GroundParameter(groundModel, this, GroundParameter::ValueTop,
                                            _topMin, _topMax));
  _topP->setScale(ParameterGrid::Log);
  _topP->setPrecision(0.01);
  if(!_topP->initGrid()) {
    return false;
  }
  // TODO: if both top and bottom are fixed, compute a fixed gradient
  switch(_gradientParameter) {
  case GradientValue:
    _bottomP=nullptr;
    switch(_shape) {
    case Uniform:
      _gradientP=nullptr;
      break;
    case Linear:
      _gradientP=ps.addParameter(new GroundParameter(groundModel, this, GroundParameter::ValueGradient,
                                                     -std::numeric_limits<double>::infinity(),
                                                     std::numeric_limits<double>::infinity()));
      _gradientP->setScale(ParameterGrid::Linear);
      break;
    case PowerLaw:
      _gradientP=ps.addParameter(new GroundParameter(groundModel, this, GroundParameter::ValueGradient,
                                                     0.0, 1.0));
      _gradientP->setScale(ParameterGrid::Linear);
      _gradientP->setFussy(true);
      break;
    case LinearIncrease:
      _gradientP=ps.addParameter(new GroundParameter(groundModel, this, GroundParameter::ValueGradient,
                                                     0.0, std::numeric_limits<double>::infinity()));
      _gradientP->setScale(ParameterGrid::Linear);
      break;
    case LinearDecrease:
      _gradientP=ps.addParameter(new GroundParameter(groundModel, this, GroundParameter::ValueGradient,
                                                     -std::numeric_limits<double>::infinity(), 0.0));
      _gradientP->setScale(ParameterGrid::Linear);
      break;
    }
    if(_gradientP) {
      return _gradientP->initGrid();
    }
    break;
  case BottomValue:
    _gradientP=nullptr;
    switch(_shape) {
    case Uniform:
      _bottomP=nullptr;
      break;
    case Linear:
    case LinearIncrease:
    case LinearDecrease:
    case PowerLaw:
      _bottomP=ps.addParameter(new GroundParameter(groundModel,  this, GroundParameter::ValueBottom,
                                                   _bottomMin, _bottomMax));
      break;
    }
    switch(_shape) {
    case Uniform:
    case Linear:
      break;
    case LinearIncrease:
    case PowerLaw:
      ps.addCondition(new SimpleCondition (_bottomP, SimpleCondition::GreaterThan, 1.0, _topP, 0.0));
      break;
    case LinearDecrease:
      ps.addCondition(new SimpleCondition (_bottomP, SimpleCondition::LessThan, 1.0, _topP, 0.0));
      break;
    }
    if(_bottomP) {
      _bottomP->setScale(ParameterGrid::Log);
      _bottomP->setPrecision(0.01);
      return _bottomP->initGrid();
    }
  }
  return true;
}

bool ParamLayer::depthParameters(bool last, ParamLayer * prev, RealSpace& ps,
                                 const ParamGroundModel * groundModel)
{
  TRACE;
  if(!_ptrLink && !last) {
    _dhP=ps.addParameter(new GroundParameter(groundModel,  this,
                         _isDepth ? GroundParameter::Depth : GroundParameter::Thickness, _dhMin, _dhMax) );
    // Currently a debate: log or lin? log has a higher probability to generate shallow models...
    _dhP->setScale(ParameterGrid::Log);
    _dhP->setPrecision(0.01);
    //_dhP->setScale(ParameterGrid::Linear);
    //_dhP->setPrecision(0.5);
    if(!_dhP->initGrid()) {
      return false;
    }
    _dhP->setFussy(true);
    if (_isDepth && prev && prev->isDepth() && prev->dhParam()) {
      // Strict condition to avoid null thicknesses
      //ps.addCondition(new SimpleCondition (_dhP, SimpleCondition::GreaterThan, 1.0, prev->dhParam(), 0.0 ));
      // Condition to avoid thin layers: thickness cannot be less than 5% of average depth of layer
      ps.addCondition(new SimpleCondition (_dhP, SimpleCondition::GreaterThan, (2.0+0.05)/(2.0-0.05), prev->dhParam(), 0.0));
    }
  } else {
    _dhP=nullptr;
  }
  return true;
}

bool ParamLayer::valueConditions(RealSpace& ps)
{
  TRACE;
  ASSERT(_index<profile()->nLayers()-1);
  ParamLayer * nextLayer=profile()->layer(_index+1);
  switch(_gradientParameter) {
  case GradientValue:
    switch(_shape) {
    case Uniform:
      break;
    case Linear:
    case PowerLaw:
    case LinearIncrease:
    case LinearDecrease: {
        BottomValueCondition * cond=new BottomValueCondition(this);
        if(cond->isValid()) {
          ps.addCondition(cond);
          return true;
        } else {
          delete cond;
          App::log(tr("Bottom value condition cannot be defined for layer '%1', mixture of thickness and depths "
                      "not allowed with gradient layers.\n").arg(name()));
          return false;
        }
      }
    }
    break;
  case BottomValue:
    switch(_shape) {
    case Uniform:
      break;
    case Linear:
    case PowerLaw:
    case LinearIncrease:
    case LinearDecrease:
      switch (profile()->defaultCondition()) {
      case SimpleCondition::LessThan:
        ps.addCondition(new SimpleCondition(_bottomP, SimpleCondition::LessThan, 1.0, nextLayer->topParam(), 0.0));
        break;
      case SimpleCondition::GreaterThan:
        ps.addCondition(new SimpleCondition(_bottomP, SimpleCondition::GreaterThan, 1.0, nextLayer->topParam(), 0.0));
        break;
      case SimpleCondition::NoCondition:
        break;
      }
      break;
    }
    break;
  }
  // Uniform shapes
  if(!nextLayer->isLastParamCondition()) {
    return true;
  }
  switch (profile()->defaultCondition()) {
  case SimpleCondition::LessThan:
    ps.addCondition(new SimpleCondition(_topP, SimpleCondition::LessThan, 1.0, nextLayer->topParam(), 0.0));
    break;
  case SimpleCondition::GreaterThan:
    ps.addCondition(new SimpleCondition(_topP, SimpleCondition::GreaterThan, 1.0, nextLayer->topParam(), 0.0));
    break;
  case SimpleCondition::NoCondition:
    break;
  }
  return true;
}

/*!
  Add top and bottom parameters if they are not fixed to \a params.
*/
void ParamLayer::collectValueParameters(QList<Parameter *>& params) const
{
  TRACE;
  if(_topP) {
    if(!_topP->isFixed()) {
      params.append(_topP);
    }
  }
  if(_gradientP) {
    if(!_gradientP->isFixed() && _shape!=PowerLaw) {
      params.append(_gradientP);
    }
  }
  if(_bottomP) {
    if(!_bottomP->isFixed()) {
      params.append(_bottomP);
    }
  }
}

/*!
  Check and add condition for depth across linked depths.
*/
void ParamLayer::setLinkedDepth(bool last, ParamLayer * prev, RealSpace& ps)
{
  TRACE;
  if(_ptrLink && !last) {
    static_cast<GroundParameter *>(_ptrLink->_dhP)->addLayer(this);
    if(_ptrLink->_isDepth) {
      _isDepth=true;
      _dhP=_ptrLink->_dhP;
      if(prev && prev->_isDepth && prev->_dhP)
        ps.addCondition(new SimpleCondition (_dhP, SimpleCondition::GreaterThan, 1.0, prev->_dhP, 0.0) );
    }
  }
}

/*!
  Return the fixed thickness to the upper most layers with variable depth or thickness returned in \a ref (left)
*/
double ParamLayer::fixedThickness(ParamLayer *& ref)
{
  TRACE;
  if(_dhMin==_dhMax) {
    if(_isDepth || _index==0) {
      ref=nullptr;
      return _dhMin;
    } else {
      return _dhMin+_profile->layer(_index-1)->fixedThickness(ref);
    }
  }
  ref=this;
  return 0.0;
}

/*!
  Check if the two layers have a common interface, even through fixed depths and thicknesses
*/
bool ParamLayer::isLinkedTo(ParamLayer * o)
{
  TRACE;
  // layers are linked together (this towards o)
  if(_ptrLink==o) return true;
  // layers are both linked to another layer (chained links have already been solved)
  if(_ptrLink==o->_ptrLink && _ptrLink!=nullptr) {
    return true;
  }
  // layers are linked together (o towards this)
  if(this==o->_ptrLink) return true;
  // layers have fixed depths or thicknesses
  ParamLayer * lThis, *lo;
  double thickness=fixedThickness(lThis);
  if(thickness==o->fixedThickness(lo) && thickness>0.0) {
    if(lThis && lo) {
      if(!lThis->isLinkedTo(lo)) return false;
    } else if( !lThis && !lo);
    else return false;
  } else return false;
  return true;
}

/*!
  Set the index of the top sub layer in the final profile, \a index is incremented by the number of sub layers for this layer.
*/
void ParamLayer::setTopDepthIndex(int& index)
{
  TRACE;
  _topDepthIndex=index;
  index+=_gradientProfile->subLayerCount();
}

/*!
  Called routinely during inversion, it inits the final bottom depth of the layer.

  If the top of the layer is below the bottom, a quasi null thickness is set. This may happen only during initialization phase.
  At any other time, the conditions on depths must effectively forbid this situation.

  Links are not taken into account.
*/
void ParamLayer::setFinalDepths(double & z)
{
  TRACE;
  if(_dhP) {
    _gradientProfile->setTopDepth(z);
    if(_isDepth) {
      _gradientProfile->setBottomDepth(_dhP->realValue());
      if(!_gradientProfile->hasValidDepths()) {
        App::log(tr("Null or negative thickness for layer %1: top=%2, bottom=%3, very thin layer instead (1 mm).\n")
                 .arg(name())
                 .arg(_gradientProfile->topDepth())
                 .arg(_gradientProfile->bottomDepth()));
        _gradientProfile->setBottomDepth(_gradientProfile->topDepth()+0.001);
      }
    } else {
      _gradientProfile->setBottomDepth(z+_dhP->realValue());
    }
    z=_gradientProfile->bottomDepth();
  }
}

/*!
  Called routinely during inversion, it sets the depths of final \a profile for parameter layers
*/
void ParamLayer::setFinalProfileDepthsParam(Profile& profile, double & z)
{
  TRACE;
  if( !_dhP) { // Set final depth for halfspace or layers linked to thickness param
    _gradientProfile->setTopDepth(z);
    if(_ptrLink) {
      _gradientProfile->setBottomDepth(_ptrLink->gradientProfile().bottomDepth());
    } else {
      _gradientProfile->setBottomDepth(std::numeric_limits<double>::infinity());
    }
  }
  z=_gradientProfile->bottomDepth();

  if(_gradientP) {
    _gradientProfile->setGradient(_gradientP->realValue());
  } else if(_bottomP) {
    _gradientProfile->setTopValue(_topP->realValue());
    _gradientProfile->setBottomValue(_bottomP->realValue());
  }
  _gradientProfile->setDepths(_topDepthIndex, profile);
}

/*!
  Called routinely during inversion, it sets the depths of final profiles \a min and \a max for condition layers
*/
void ParamLayer::setFinalProfileDepthsCondition(Profile& min, Profile& max, double & z)
{
  TRACE;
  if( !_dhP) { // Set final depth for halfspace or layers linked to thickness param
    _gradientProfile->setTopDepth(z);
    if(_ptrLink) {
      _gradientProfile->setBottomDepth(_ptrLink->gradientProfile().bottomDepth());
    } else {
      _gradientProfile->setBottomDepth(std::numeric_limits<double>::infinity());
    }
  }
  z=_gradientProfile->bottomDepth();

  _gradientProfile->setTopValue(_topMin);
  _gradientProfile->setBottomValue(_bottomMin);
  _gradientProfile->setDepths(_topDepthIndex, min);

  _gradientProfile->setTopValue(_topMax);
  _gradientProfile->setTopValue(_bottomMax);
  _gradientProfile->setDepths(_topDepthIndex, max);
}

/*!
  Set intermediate depths according to shape
*/
void ParamLayer::setFinalProfileValuesParam(Profile& profile)
{
  TRACE;
  _gradientProfile->setTopValue(_topP->realValue());
  if(_gradientP) {
    if(_gradientProfile->setGradient(_gradientP->realValue())) {
      _gradientProfile->setDepths(_topDepthIndex, profile);
    }
  } else if(_bottomP) {
    if(_gradientProfile->setBottomValue(_bottomP->realValue())) {
      _gradientProfile->setDepths(_topDepthIndex, profile);
    }
  }
  _gradientProfile->setValues(_topDepthIndex, profile);
}

void ParamLayer::setFinalProfileValuesCondition(Profile& min, Profile& max)
{
  TRACE;
  _gradientProfile->setTopValue(_topMin);
  if(_gradientProfile->setBottomValue(_bottomMin)) {
    _gradientProfile->setDepths(_topDepthIndex, min);
  }
  _gradientProfile->setValues(_topDepthIndex, min);

  _gradientProfile->setTopValue(_topMax);
  if(_gradientProfile->setBottomValue(_bottomMax)) {
    _gradientProfile->setDepths(_topDepthIndex, max);
  }
  _gradientProfile->setValues(_topDepthIndex, max);
}

/*!
  \fn ParamLayer::topDepth() const
  Returns the current depth of the top
*/

/*!
  \fn ParamLayer::bottomDepth() const
  Returns the current depth of the bottom
*/

/*!
  Returns the current depth of next fixed depth. If the next layers have thickness parameters rather than depth, then the returned
  value is the bottom of the layer with a depth parameter below these thickness parameter layers, or std::numeric_limits<double>::infinity() if the half space is reached.
  If the next layer has a depth parameter, the returned value is the same as bottomDepth().
*/
double ParamLayer::fixedBottomDepth() const
{
  TRACE;
  int nLayers=profile()->nLayers();
  for(int i=_index+1; i<nLayers; i++) {
    ParamLayer * layer=profile()->layer(i);
    if(layer->isDepth()) {
      return layer->gradientProfile().bottomDepth();
    }
  }
  return std::numeric_limits<double>::infinity(); // No fixed bottom depth, half space reached
}

void ParamLayer::xml_writeProperties(XML_WRITEPROPERTIES_ARGS) const
{
  TRACE;
  Q_UNUSED(context)
  switch (_shape) {
  case Linear: writeProperty(s, "shape", "Linear"); break;
  case LinearIncrease: writeProperty(s, "shape", "LinearIncrease"); break;
  case LinearDecrease: writeProperty(s, "shape", "LinearDecrease"); break;
  case PowerLaw: writeProperty(s, "shape", "PowerLaw"); break;
  case Uniform: writeProperty(s, "shape", "Uniform"); break;
  }
  writeProperty(s, "lastParamCondition", _lastParamCondition);
  writeProperty(s, "nSubayers", _gradientProfile->subLayerCount());
  writeProperty(s, "topMin", _topMin);
  writeProperty(s, "topMax", _topMax);
  if(_shape!=Uniform) {
    writeProperty(s, "bottomMin", _bottomMin);
    writeProperty(s, "bottomMax", _bottomMax);
  }
  writeProperty(s, "linkedTo", _linkedTo);
  writeProperty(s, "isDepth", _isDepth);
  writeProperty(s, "dhMin", _dhMin);
  writeProperty(s, "dhMax", _dhMax);
}

XMLMember ParamLayer::xml_member(XML_MEMBER_ARGS)
{
  TRACE;
  Q_UNUSED(attributes)
  Q_UNUSED(context)
  if(tag=="shape") return XMLMember(0);
  else if(tag=="lastParamCondition") return XMLMember(1);
  else if(tag=="nSubayers") return XMLMember(2);
  else if(tag=="topMin") return XMLMember(3);
  else if(tag=="topMax") return XMLMember(4);
  else if(tag=="bottomMin") return XMLMember(5);
  else if(tag=="bottomMax") return XMLMember(6);
  else if(tag=="linkedTo") return XMLMember(7);
  else if(tag=="isDepth") return XMLMember(8);
  else if(tag=="dhMin") return XMLMember(9);
  else if(tag=="dhMax") return XMLMember(10);
  else if(tag=="tilted") return XMLMember(11); // Kept for compatibility
  else if(tag=="dhMinLeft") return XMLMember(9); // Kept for compatibility
  else if(tag=="dhMaxLeft") return XMLMember(10); // Kept for compatibility
  else if(tag=="dhMinRight") return XMLMember(11); // Kept for compatibility
  else if(tag=="dhMaxRight") return XMLMember(11); // Kept for compatibility
  else if(tag=="greaterThanPrevious") return XMLMember(1); // Kept for compatibility
  else return XMLMember(XMLMember::Unknown);
}

bool ParamLayer::xml_setProperty(XML_SETPROPERTY_ARGS)
{
  TRACE;
  Q_UNUSED(tag)
  Q_UNUSED(attributes)
  Q_UNUSED(context)
  switch (memberID) {
  case 0:
    if(content=="Uniform") setShape(Uniform);
    else if(content=="Linear") setShape(Linear);
    else if(content=="LinearIncrease") setShape(LinearIncrease);
    else if(content=="LinearDecrease") setShape(LinearDecrease);
    else if(content=="PowerLaw") setShape(PowerLaw);
    else if(content=="LinearIncr") setShape(LinearIncrease); // Kept for compatibility
    else if(content=="LinearDecr") setShape(LinearDecrease); // Kept for compatibility
    else {
      App::log(tr("Unknown value %1 for shape\n").arg(content.toString()));
      return false;
    }
    return true;
  case 1:
    _lastParamCondition=content.toBool();
    return true;
  case 2:
    setSubLayerCount(content.toInt());
    return true;
  case 3:
    _topMin=content.toDouble();
    return true;
  case 4:
    _topMax=content.toDouble();
    return true;
  case 5:
    _bottomMin=content.toDouble();
    return true;
  case 6:
    _bottomMax=content.toDouble();
    return true;
  case 7:
    _linkedTo=content.toString();
    return true;
  case 8:
    _isDepth=content.toBool();
    return true;
  case 9:
    _dhMin=content.toDouble();
    return true;
  case 10:
    _dhMax=content.toDouble();
    return true;
  case 11:
    return true;
  default:
    return false;
  }
}

} // namespace DinverDCCore
