/***************************************************************************
**
**  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: 2017-07-10
**  Copyright: 2017-2019
**    Marc Wathelet (ISTerre, Grenoble, France)
**
***************************************************************************/

#include "BottomValueCondition.h"
#include "ParamLayer.h"
#include "ParamProfile.h"

namespace DinverDCCore {

  /*!
    \class BottomValueCondition BottomValueCondition.h
    \brief Manage conditions of a gragient and with next layer

    Full description of class still missing
  */

  const QString BottomValueCondition::xmlBottomValueConditionTag="BottomValueCondition";

  /*!
    Description of constructor still missing
  */
  BottomValueCondition::BottomValueCondition()
  {
    TRACE;
    _layer=nullptr;
    _isDepth=true;
    _nextLayer=nullptr;
    _nextLayerCondition=NoCondition;
  }

  /*!
    Description of constructor still missing
  */
  BottomValueCondition::BottomValueCondition(ParamLayer * layer)
  {
    TRACE;
    _layer=layer;
    _layerName=_layer->name();
    addParameter(_layer->topParam());
    addParameter(_layer->gradientParam());
    addParameter(_layer->dhParam());
    _isDepth=(_layer->isDepth() && _layer->index()>0);
    if(_isDepth) {
      addParameter(_layer->profile()->layer(_layer->index()-1)->dhParam());
    }
    // Condition between bottom value and next layer
    // Last layer of a profile, cannot have a gradient shape
    Q_ASSERT(_layer->index()<_layer->profile()->nLayers()-1);
    _nextLayer=_layer->profile()->layer(_layer->index()+1);
    if(_nextLayer->isLastParamCondition()) {
      addParameter(_nextLayer->topParam());
      _nextLayerCondition=_layer->profile()->defaultCondition();
    } else {
      _nextLayerCondition=NoCondition;
    }
  }

  /*!
    If the layer has a depth parameter, the above layer must have also a depth parameter.
    If not, the thickness of the current layer is too complex, linked to more parameters.
  */
  bool BottomValueCondition::isValid() const
  {
    TRACE;
    if(_isDepth) {
      return _layer->profile()->layer(_layer->index()-1)->isDepth();
    } else {
      return true;
    }
  }

  /*!
    Print human readable information about condition to stream.
  */
  void BottomValueCondition::humanInfo() const
  {
    TRACE;
    App::log("        gradient defined by ");
    int n1=count()-1;
    for(int i=0; i<n1; i++) {
      App::log(parameter(i).name()+",");
    }
    App::log(parameter(n1).name());
    App::log(" between "+QString::number(_layer->bottomMinimumValue())
             +" and "+QString::number(_layer->bottomMaximumValue())+" at bottom layer.\n");
  }

  void BottomValueCondition::xml_writeProperties(XML_WRITEPROPERTIES_ARGS) const
  {
    TRACE;
    Q_UNUSED(context)
    writeProperty(s, "layer", _layer->name());
  }

  XMLMember BottomValueCondition::xml_member(XML_MEMBER_ARGS)
  {
    TRACE;
    if(tag=="layer") {
      return XMLMember(0);
    } else {
      return BottomValueCondition::xml_member(tag, attributes, context)+1;
    }
  }

  bool BottomValueCondition::xml_setProperty(XML_SETPROPERTY_ARGS)
  {
    TRACE;
    switch (memberID) {
    case 0:
      _layerName=content.toString();
      return true;
    default:
      return AbstractCondition::xml_setProperty(memberID-1, tag, attributes, content, context);
    }
  }

  double BottomValueCondition::thickness() const
  {
    TRACE;
    if(_layer->isDepth() && _layer->index()>0) {
      return parameter(2).realValue()-parameter(3).realValue();
    } else {
      return parameter(2).realValue();
    }
  }

  /*!
    Return true if a positive minimum thickness can be set.
    For depths parameters, a minimum thickness can be obtained if there is a
    condition set to avoid thin layers (default is 5% of average depth).
    At the beginning of range adjustment, the effect of the thin layer is not
    effective. Naturally after some iterations, the depths limits are correct
    and this function can return true.
  */
  bool BottomValueCondition::setMinimumThickness()
  {
    TRACE;
    Parameter& p2=parameter(2);
    if(_layer->isDepth() && _layer->index()>0) {
      Parameter& p3=parameter(3);
      if(p3.minimum()<p2.minimum()) {
        p2.setRealValue(p2.minimum());
        p3.setRealValue(p3.minimum());
        return true;
      } else {
        return false;
      }
    } else {
      p2.setRealValue(p2.minimum());
      return true;
    }
  }

  void BottomValueCondition::setMaximumThickness()
  {
    TRACE;
    Parameter& p2=parameter(2);
    if(_layer->isDepth() && _layer->index()>0) {
      Parameter& p3=parameter(3);
      p2.setRealValue(p2.minimum());
      p3.setRealValue(p3.maximum());
    } else {
      p2.setRealValue(p2.maximum());
    }
  }

  /*!
    Return the minimum acceptable value at bottom of layer
  */
  double BottomValueCondition::minimumBottomValue() const
  {
    TRACE;
    double m;
    SAFE_UNINITIALIZED(m, 0.0)
    switch(_nextLayerCondition) {
    case NoCondition:
    case LessThan:
      m=-std::numeric_limits<double>::infinity();
      break;
    case GreaterThan:
      m=_nextLayer->topParam()->realValue();
      break;
    }
    if(m<_layer->bottomMinimumValue()) {
      return _layer->bottomMinimumValue();
    } else {
      return m;
    }
  }

  /*!
    Return the maximum acceptable value at bottom of layer
  */
  double BottomValueCondition::maximumBottomValue() const
  {
    TRACE;
    double m;
    SAFE_UNINITIALIZED(m, 0.0)
    switch(_nextLayerCondition) {
    case NoCondition:
    case GreaterThan:
      m=std::numeric_limits<double>::infinity();
      break;
    case LessThan:
      m=_nextLayer->topParam()->realValue();
      break;
    }
    if(m>_layer->bottomMaximumValue()) {
      return _layer->bottomMaximumValue();
    } else {
      return m;
    }
  }

  inline void BottomValueCondition::getLimitsTopValue(double& min, double& max) const
  {
    TRACE;
    double threshold, dv;
    switch (_layer->shape()) {
    case ParamLayer::Uniform:
      break;
    case ParamLayer::Linear:
    case ParamLayer::LinearIncrease:
    case ParamLayer::LinearDecrease: // V=V0+alpha*(z-z0)
      dv=thickness()*parameter(1).realValue();
      threshold=minimumBottomValue()-dv;
      if(threshold>min) {
        min=threshold;
      }
      threshold=maximumBottomValue()-dv;
      if(threshold<max) {
        max=threshold;
      }
      break;
    case ParamLayer::PowerLaw:  // V=V0*(1.0+z-z0)^alpha
      dv=pow(1.0+thickness(), parameter(1).realValue());
      threshold=minimumBottomValue()/dv;
      if(threshold>min) {
        min=threshold;
      }
      threshold=maximumBottomValue()/dv;
      if(threshold<max) {
        max=threshold;
      }
      break;
    }
  }

  inline void BottomValueCondition::getLimitsGradient(double& min, double& max) const
  {
    TRACE;
    double threshold, v0, cste;
    switch (_layer->shape()) {
    case ParamLayer::Uniform:
      break;
    case ParamLayer::Linear:
    case ParamLayer::LinearIncrease:
    case ParamLayer::LinearDecrease: // V=V0+alpha*(z-z0)
      cste=1.0/thickness();
      v0=parameter(0).realValue();
      threshold=cste*(minimumBottomValue()-v0);
      if(threshold>min) {
        min=threshold;
      }
      threshold=cste*(maximumBottomValue()-v0);
      if(threshold<max) {
        max=threshold;
      }
      break;
    case ParamLayer::PowerLaw:  // V=V0*(1.0+z-z0)^alpha
      cste=1.0/log(1.0+thickness());
      v0=parameter(0).realValue();
      threshold=log(minimumBottomValue()/v0)*cste;
      if(threshold>min) {
        min=threshold;
      }
      threshold=log(maximumBottomValue()/v0)*cste;
      if(threshold<max) {
        max=threshold;
      }
      break;
    }
  }

  inline void BottomValueCondition::getLimitsDepth(double& min, double& max) const
  {
    TRACE;
    double threshold, v0, cste, invSlope;
    switch (_layer->shape()) {
    case ParamLayer::Uniform:
      break;
    case ParamLayer::Linear:
    case ParamLayer::LinearIncrease:
    case ParamLayer::LinearDecrease: // V=V0+alpha*(z-z0)
      v0=parameter(0).realValue();
      invSlope=1.0/parameter(1).realValue();
      if(_isDepth) {
        cste=parameter(3).realValue();
      } else {
        cste=0.0;
      }
      cste-=v0*invSlope;
      threshold=invSlope*minimumBottomValue()+cste;
      if(threshold>min) {
        min=threshold;
      }
      threshold=invSlope*maximumBottomValue()+cste;
      if(threshold<max) {
        max=threshold;
      }
      break;
    case ParamLayer::PowerLaw:  // V=V0*(1.0+z-z0)^alpha
      v0=parameter(0).realValue();
      invSlope=1.0/parameter(1).realValue();
      if(_isDepth) {
        cste=parameter(3).realValue();
      } else {
        cste=0.0;
      }
      threshold=exp(invSlope*log(minimumBottomValue()/v0))-1.0+cste;
      if(threshold>min) {
        min=threshold;
      }
      threshold=exp(invSlope*log(maximumBottomValue()/v0))-1.0+cste;
      if(threshold<max) {
        max=threshold;
      }
      break;
    }
  }

  inline void BottomValueCondition::getLimitsPreviousDepth(double& min, double& max) const
  {
    TRACE;
    double threshold, v0, invSlope, cste;
    switch (_layer->shape()) {
    case ParamLayer::Uniform:
      break;
    case ParamLayer::Linear:
    case ParamLayer::LinearIncrease:
    case ParamLayer::LinearDecrease: // V=V0+alpha*(z-z0), (V0-V)/alpha+z=z0
      v0=parameter(0).realValue();
      invSlope=1.0/parameter(1).realValue();
      cste=parameter(2).realValue()+v0*invSlope;
      threshold=cste-invSlope*minimumBottomValue();
      if(threshold<max) {
        max=threshold;
      }
      threshold=cste-invSlope*maximumBottomValue();
      if(threshold>min) {
        min=threshold;
      }
      break;
    case ParamLayer::PowerLaw:  // V=V0*(1.0+z-z0)^alpha
      v0=parameter(0).realValue();
      invSlope=1.0/parameter(1).realValue();
      cste=parameter(2).realValue();
      threshold=1.0+cste-exp(invSlope*log(minimumBottomValue()/v0));
      if(threshold<max) {
        max=threshold;
      }
      threshold=1.0+cste-exp(invSlope*log(maximumBottomValue()/v0));
      if(threshold>min) {
        min=threshold;
      }
      break;
    }
  }

  inline void BottomValueCondition::getLimitsNextTopValue(double& min, double& max) const
  {
    TRACE;
    double threshold, v0, alpha, h;
    switch (_layer->shape()) {
    case ParamLayer::Uniform:
      break;
    case ParamLayer::Linear:
    case ParamLayer::LinearIncrease:
    case ParamLayer::LinearDecrease: // V=V0+alpha*(z-z0)
      v0=parameter(0).realValue();
      alpha=parameter(1).realValue();
      h=thickness();
      threshold=v0+alpha*h;
      switch(_nextLayerCondition) {
      case NoCondition:
      case GreaterThan:
        if(threshold<max) {
          max=threshold;
        }
        break;
      case LessThan:
        if(threshold>min) {
          min=threshold;
        }
        break;
      }
      break;
    case ParamLayer::PowerLaw:  // V=V0*(1.0+z-z0)^alpha
      v0=parameter(0).realValue();
      alpha=parameter(1).realValue();
      h=thickness();
      threshold=v0*pow(1.0+h, alpha);
      switch(_nextLayerCondition) {
      case NoCondition:
      case GreaterThan:
        if(threshold<max) {
          max=threshold;
        }
        break;
      case LessThan:
        if(threshold>min) {
          min=threshold;
        }
        break;
      }
      break;
    }
  }

  /*!
    Adjust \a min or/and \a max to fulfill the condition. \a paramIndex must be 0 to 2 or 3.
  */
  void BottomValueCondition::getLimits(int paramIndex, double& min, double& max) const
  {
    TRACE;
    switch (paramIndex) {
    case 0:  // TopV
      getLimitsTopValue(min, max);
      break;
    case 1: // Gradient
      getLimitsGradient(min, max);
      break;
    case 2: // Layer depth or thickness
      getLimitsDepth(min, max);
      break;
    case 3: // Previous layer depth or next layer top
      if(_isDepth) {
        getLimitsPreviousDepth(min, max);
      } else if(_nextLayerCondition!=NoCondition) {
        getLimitsNextTopValue(min, max);
      } else {
        ASSERT(false);
      }
      break;
    case 4: // Next layer top
      if(_nextLayerCondition!=NoCondition) {
        getLimitsNextTopValue(min, max);
      } else {
        ASSERT(false);
      }
      break;
    default:
      Q_ASSERT(false);
    }
  }

  /*!
    Return true if original ranges of parameter are touched. Ranges can be reduced, never enlarged
  */
  bool BottomValueCondition::adjustRanges()
  {
    TRACE;
    double min, max;
    bool modified=false;
    Parameter& p0=parameter(0);
    Parameter& p1=parameter(1);
    switch (_layer->shape()) {
    case ParamLayer::Uniform:
      return false;
    case ParamLayer::Linear:
      if(_nextLayerCondition) {
        Parameter * pn=_nextLayer->topParam();
        switch(_nextLayerCondition) {
        case NoCondition:
          break;
        case LessThan:
          min=_layer->bottomMinimumValue();
          if(pn->minimum()<min) {
            pn->setMinimum(min);
            modified=true;
          }
          break;
        case GreaterThan:
          max=_layer->bottomMaximumValue();
          if(pn->maximum()>max) {
            pn->setMaximum(max);
            modified=true;
          }
          break;
        }
      }
      break;
    case ParamLayer::PowerLaw:
    case ParamLayer::LinearIncrease:
      // Adjust maximum of top value
      if(_nextLayerCondition) {
        Parameter * pn=_nextLayer->topParam();
        pn->setRealValue(pn->maximum());
      }
      max=maximumBottomValue();
      if(p0.maximum()>max) {
        p0.setMaximum(max);
        modified=true;
      }
      // top value of next layers can be adjusted without using the gradient (not yet adjusted)
      if(_nextLayerCondition) {
        Parameter * pn=_nextLayer->topParam();
        switch(_nextLayerCondition) {
        case NoCondition:
          break;
        case LessThan:
          min=p0.minimum();
          if(min<_layer->bottomMinimumValue()) {
            min=_layer->bottomMinimumValue();
          }
          if(pn->minimum()<min) {
            pn->setMinimum(min);
            modified=true;
          }
          break;
        case GreaterThan:
          max=_layer->bottomMaximumValue();
          if(pn->maximum()>max) {
            pn->setMaximum(max);
            modified=true;
          }
          break;
        }
      }
      break;
    case ParamLayer::LinearDecrease:
      // Adjust minimum of top value
      if(_nextLayerCondition) {
        Parameter * pn=_nextLayer->topParam();
        pn->setRealValue(pn->minimum());
      }
      min=minimumBottomValue();
      if(p0.minimum()<min) {
        p0.setMinimum(min);
        modified=true;
      }
      // Adjust maximum of top value of next layer
      if(_nextLayerCondition) {
        Parameter * pn=_nextLayer->topParam();
        switch(_nextLayerCondition) {
        case NoCondition:
          break;
        case LessThan:
          min=_layer->bottomMinimumValue();
          if(pn->minimum()<min) {
            pn->setMinimum(min);
            modified=true;
          }
          break;
        case GreaterThan:
          max=p0.maximum();
          if(max>_layer->bottomMaximumValue()) {
            max=_layer->bottomMaximumValue();
          }
          if(pn->maximum()>max) {
            pn->setMaximum(max);
            modified=true;
          }
          break;
        }
      }
      break;
    }
    // If not possible to adjust (depth not yet adjusted), maybe during the next iterations
    if(setMinimumThickness()) {
      // Get maximum gradient
      switch (_layer->shape()) {
      case ParamLayer::Linear:
      case ParamLayer::LinearIncrease:
      case ParamLayer::PowerLaw:
        p0.setRealValue(p0.minimum());
        if(_nextLayerCondition) {
          Parameter * pn=_nextLayer->topParam();
          pn->setRealValue(pn->maximum());
        }
        max=std::numeric_limits<double>::infinity();
        getLimitsGradient(min, max);
        if(p1.maximum()>max) {
          p1.setMaximum(max);
          modified=true;
        }
        break;
      case ParamLayer::Uniform:
      case ParamLayer::LinearDecrease:
        break;
      }
      // Get minimum gradient
      switch (_layer->shape()) {
      case ParamLayer::Linear:
      case ParamLayer::LinearDecrease:
        p0.setRealValue(p0.maximum());
        if(_nextLayerCondition) {
          Parameter * pn=_nextLayer->topParam();
          pn->setRealValue(pn->minimum());
        }
        min=-std::numeric_limits<double>::infinity();
        getLimitsGradient(min, max);
        if(p1.minimum()<min) {
          p1.setMinimum(min);
          modified=true;
        }
        break;
      case ParamLayer::Uniform:
      case ParamLayer::LinearIncrease:
      case ParamLayer::PowerLaw:
        break;
      }
    }
    if(modified) {
      max=_layer->gradientParam()->maximum();
      if(_layer->gradientParam()->minimum()<-max) {
        max=-_layer->gradientParam()->minimum();
      }
      _layer->gradientParam()->setPrecision(max/500);
    }
    return modified;
  }


} // namespace DinverDCCore

