//
// ********************************************************************
// * License and Disclaimer                                           *
// *                                                                  *
// * The  Geant4 software  is  copyright of the Copyright Holders  of *
// * the Geant4 Collaboration.  It is provided  under  the terms  and *
// * conditions of the Geant4 Software License,  included in the file *
// * LICENSE and available at  http://cern.ch/geant4/license .  These *
// * include a list of copyright holders.                             *
// *                                                                  *
// * Neither the authors of this software system, nor their employing *
// * institutes,nor the agencies providing financial support for this *
// * work  make  any representation or  warranty, express or implied, *
// * regarding  this  software system or assume any liability for its *
// * use.  Please see the license in the file  LICENSE  and URL above *
// * for the full disclaimer and the limitation of liability.         *
// *                                                                  *
// * This  code  implementation is the result of  the  scientific and *
// * technical work of the GEANT4 collaboration.                      *
// * By using,  copying,  modifying or  distributing the software (or *
// * any work based  on the software)  you  agree  to acknowledge its *
// * use  in  resulting  scientific  publications,  and indicate your *
// * acceptance of all terms of the Geant4 Software license.          *
// ********************************************************************
//
/*
 * G4DNASmoluchowskiDiffusion.hh
 *
 *  Created on: 2 févr. 2015
 *      Author: matkara
 */

#ifndef G4DNASMOLUCHOWSKIDIFFUSION_HH_
#define G4DNASMOLUCHOWSKIDIFFUSION_HH_

//#if __cplusplus >= 201103L
#include <cstdlib>
#include <cmath>
#include <vector>
#include <iostream>
#include <algorithm>

//#define DNADEV_TEST

#ifdef DNADEV_TEST
#include <TF1.h>
#endif

#include <cassert>

#ifndef DNADEV_TEST
#include "globals.hh"
#include "Randomize.hh"
#endif

#ifdef DNADEV_TEST
#include "TRandom.h"
TRandom root_random;
double G4UniformRand()
{
  return root_random.Rndm();
}

#define G4cout std::cout
#define G4endl std::endl
#endif

#include "G4Exp.hh"

class G4DNASmoluchowskiDiffusion
{
public:
  G4DNASmoluchowskiDiffusion(double epsilon = 1e-5);
  virtual ~G4DNASmoluchowskiDiffusion();

  static double ComputeS(double r, double D, double t)
  {
    double sTransform = r / (2. * std::sqrt(D * t));
    return sTransform;
  }

  static double ComputeDistance(double sTransform, double D, double t)
  {
    return sTransform * 2. * std::sqrt(D * t);
  }

  static double ComputeTime(double sTransform, double D, double r)
  {
    return std::pow(r / sTransform, 2.) / (4. * D);
  }

  //====================================================

  double GetRandomDistance(double _time, double D)
  {
    double proba = G4UniformRand();
//    G4cout << "proba = " << proba << G4endl;
    double sTransform = GetInverseProbability(proba);
//    G4cout << "sTransform = " << sTransform << G4endl;
    return ComputeDistance(sTransform, D, _time);
  }

  double GetRandomTime(double distance, double D)
  {
    double proba = G4UniformRand();
    double sTransform = GetInverseProbability(proba);
    return ComputeTime(sTransform, D, distance);
  }

  double EstimateCrossingTime(double proba,
                              double distance,
                              double D)
  {
    double sTransform = GetInverseProbability(proba);
    return ComputeTime(sTransform, D, distance);
  }

  //====================================================
  // 1-value transformation

  // WARNING : this is NOT the differential probability
  // this is the derivative of the function GetCumulativeProbability
  static double GetDifferential(double sTransform)
  {
    static double constant = -4./std::sqrt(3.141592653589793);
    return sTransform*sTransform*G4Exp(-sTransform*sTransform)*constant; // -4*sTransform*sTransform*exp(-sTransform*sTransform)/sqrt(3.141592653589793)
  }

  static double GetDensityProbability(double r, double _time, double D)
  {
    static double my_pi = 3.141592653589793;
    static double constant = 4.*my_pi/std::pow(4.*my_pi, 1.5);
    return r*r/std::pow(D * _time,1.5)*G4Exp(-r*r/(4. * D * _time))*constant;
  }

  //====================================================
  // BOUNDING BOX
  struct BoundingBox
  {
    double fXmax;
    double fXmin;
    double fXmaxDef;
    double fXminDef;
    double fToleranceY;
    double fSum{0};
    double    fIncreasingCumulativeFunction;

    enum PreviousAction
    {
      IncreaseProba,
      DecreaseProba,
      Undefined
    };

    PreviousAction fPreviousAction;

    BoundingBox(double xmin,
                double xmax,
                double toleranceY) :
     fXmax(xmax), fXmin(xmin),
     fToleranceY(toleranceY) 
    {
      if(fXmax < fXmin)
      {
        double tmp = fXmin;
        fXmin = fXmax;
        fXmax = tmp;
      }
      
      fXminDef = fXmin;
      fXmaxDef = fXmax;
      fPreviousAction = BoundingBox::Undefined;
      fIncreasingCumulativeFunction = (GetCumulativeProbability(fXmax) - GetCumulativeProbability(fXmin))/(fXmax-fXmin);
    }
    
    void Print()
    {
      G4cout << "fXmin: " << fXmin << " | fXmax: " << fXmax << G4endl;
    }

    bool Propose(double proposedXValue,
                 double proposedProba,
                 double nextProba,
                 double& returnedValue)
    {
//      G4cout << "---------------------------" << G4endl;
//      G4cout << "Proposed x value: " << proposedXValue
//          << "| proposedProba: " << proposedProba
//          << "| nextProba: " << nextProba
//          << " | fXmin: " << fXmin << " (" << G4DNASmoluchowskiDiffusion::GetCumulativeProbability(fXmin) <<")"
//          << " | fXmax: " << fXmax << " (" << G4DNASmoluchowskiDiffusion::GetCumulativeProbability(fXmax) <<")"
//          << G4endl;

      bool returnFlag = false;
      
      if(proposedProba < nextProba-fToleranceY) // proba trop petite ==> augmente
      {
        // G4cout << "proposedProba < nextProba-fToleranceY" << G4endl;

        if(fIncreasingCumulativeFunction > 0) // croissant
        {
          if(proposedXValue > fXmin)
            fXmin = proposedXValue;
        }
        else if(fIncreasingCumulativeFunction < 0) // decroissant
        {
          if(proposedXValue < fXmax)
            fXmax = proposedXValue;
        }
        
        returnedValue = (fXmax + fXmin)/2;
        returnFlag = false;
        fPreviousAction = BoundingBox::IncreaseProba;
      }
      else if(proposedProba > nextProba+fToleranceY) // proba trop grande
      {
        // G4cout << "proposedProba > nextProba+fToleranceY" << G4endl;

        if(fIncreasingCumulativeFunction>0)
        {
          if(proposedXValue < fXmax)
            fXmax = proposedXValue;
        }
        else if(fIncreasingCumulativeFunction<0)
        {
          if(proposedXValue > fXmin)
          {
            fXmin = proposedXValue;
          }
        }
        
        returnedValue = (fXmax + fXmin)/2;
        returnFlag = false;
        fPreviousAction = BoundingBox::DecreaseProba;
      }
      else
      {
        // G4cout << "IN THE INTERVAL !! : " << nextProba << G4endl;
        fSum = proposedProba;
        
        // Assuming search for next proba is increasing
        if(fIncreasingCumulativeFunction<0)
        {
         fXmin = fXminDef;
         fXmax = proposedXValue;
        }
        else if(fIncreasingCumulativeFunction>0)
        {
          fXmin = proposedXValue;
          fXmax = fXmaxDef;
        }
        returnFlag = true;
        fPreviousAction = BoundingBox::Undefined;
      }
      
      return returnFlag;
    }
  };
  // END OF BOUNDING BOX
  //==============================
  
  void PrepareReverseTable(double xmin, double xmax)
  {
    double x = xmax;
    int index = 0;
    double nextProba = fEpsilon;
    double proposedX;

    BoundingBox boundingBox(xmin, xmax, fEpsilon*1e-5);

    while(index <= fNbins)
    // in case GetCumulativeProbability is exact (digitally speaking), replace with:
    // while(index <= fNbins+1)
    {
      nextProba = fEpsilon*index;

      double newProba = GetCumulativeProbability(x);

      if(boundingBox.Propose(x, newProba, nextProba, proposedX))
      {
        fInverse[index] = x;
        index++;
      }
      else
      {
        if(x == proposedX)
        {
          G4cout << "BREAK : x= " << x << G4endl;
          G4cout << "index= " << index << G4endl;
          G4cout << "nextProba= " << nextProba << G4endl;
          G4cout << "newProba= " << newProba << G4endl;
          abort();
        }
        x = proposedX;
      }
    }
    
    fInverse[fNbins+1] = 0; // P(1) = 0, because we want it exact !
    // Tips to improve the exactness: get an better value of pi, get better approximation of erf and exp, use long double instead of double
//    boundingBox.Print();
  }

  static double GetCumulativeProbability(double sTransform)
  {
    static double constant = 2./std::sqrt(3.141592653589793);
    return erfc(sTransform) + constant*sTransform*G4Exp(-sTransform*sTransform);
  }

  double GetInverseProbability(double proba) // returns sTransform
  {
    auto  index_low = (size_t) trunc(proba/fEpsilon);
    
    if(index_low == 0) // assymptote en 0
    {
      index_low = 1;
      size_t index_up = 2;
      double low_y = fInverse[index_low];
      double up_y = fInverse[index_up];
      double low_x = index_low*fEpsilon;
      double up_x = proba+fEpsilon;
      double tangente = (low_y-up_y)/(low_x - up_x); // ou utiliser GetDifferential(proba) ?
      // double tangente = GetDifferential(proba);
      return low_y + tangente*(proba-low_x);
    }

    size_t index_up = index_low+1;
    if(index_low > fInverse.size()) return fInverse.back();
    double low_y = fInverse[index_low];
    double up_y = fInverse[index_up];

    double low_x = index_low*fEpsilon;
    double up_x = low_x+fEpsilon;

    if(up_x > 1) // P(1) = 0
    {
      up_x = 1;
      up_y = 0; // more general : fInverse.back()
    }

    double tangente = (low_y-up_y)/(low_x - up_x);

    return low_y + tangente*(proba-low_x);
  }

  double PlotInverse(double* x, double* )
  {
    return GetInverseProbability(x[0]);
  }

  double Plot(double* x, double* )
  {
    return GetDifferential(x[0]);
  }


  void InitialiseInverseProbability(double xmax = 3e28)
  {
    // x > x'
    // P'(x) = p(x') = lim(x->x') (P(x') - P(x))/(x'-x)
    // x'-x = (P(x') - P(x))/p(x')
    // x = x' - (P(x') - P(x))/p(x')

    // fInverse initialized in the constructor

    assert(fNbins !=0);
    PrepareReverseTable(0,xmax);
  }

  std::vector<double> fInverse;
  int fNbins;
  double fEpsilon;
};

#endif /* SOURCE_PROCESSES_ELECTROMAGNETIC_DNA_MODELS_G4DNASMOLUCHOWSKIDIFFUSION_HH_ */
