#include "DiffusionSolverBiofilmFE.h"

#include <CompuCell3D/Simulator.h>
#include <CompuCell3D/ClassRegistry.h>
#include <CompuCell3D/Automaton/Automaton.h>
#include <CompuCell3D/Potts3D/Potts3D.h>
#include <CompuCell3D/Potts3D/CellInventory.h>
#include <CompuCell3D/Field3D/WatchableField3D.h>
#include <CompuCell3D/Field3D/Field3DImpl.h>
#include <CompuCell3D/Field3D/Field3D.h>
#include <BasicUtils/BasicClassGroup.h>
#include <BasicUtils/BasicClassAccessor.h>
#include <CompuCell3D/plugins/Concentration/ConcentrationPlugin.h>

#include <CompuCell3D/BlobFieldInitializer.h>


#include <XMLCereal/XMLPullParser.h>
#include <XMLCereal/XMLSerializer.h>
#include <BasicUtils/BasicString.h>
#include <BasicUtils/BasicException.h>
#include <string>
#include <limits>

#include <iostream>

using namespace CompuCell3D;
using namespace std;
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
DiffusionSolverBiofilmFE::DiffusionSolverBiofilmFE()
 : Diffusable<float>(),
   scratchField(0)
   
{
   concentrationFieldName="Concentration";
   avoidMedium=false;
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
DiffusionSolverBiofilmFE::~DiffusionSolverBiofilmFE()
{
   if(scratchField) delete scratchField;
   scratchField=0;
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
void DiffusionSolverBiofilmFE::init(Simulator *simulator) {
  simPtr=simulator;
  potts = simulator->getPotts();
   
  ///getting cell inventory
  cellInventoryPtr=& potts->getCellInventory(); 
  
  ///getting field ptr from Potts3D
  cellFieldG=potts->getCellFieldG();
  Dim3D fieldDim=cellFieldG->getDim();
  
  ///constructing concentration and scratch fields
  //concentrationField= new Field3DImpl<float>(fieldDim, 0.0);
  allocateDiffusableField(fieldDim);
  scratchField= new Field3DImpl<float>(fieldDim , 0.0);

  
  ///setting member function pointers
  //secretePtr=&DiffusionSolverBiofilmFE::secrete;
  secretePtr=&DiffusionSolverBiofilmFE::secreteInCM;
  diffusePtr=&DiffusionSolverBiofilmFE::diffuse;

  ///initialize field ptr in the conncentration plugin 
  ConcentrationPlugin * concentrationPlugin=(ConcentrationPlugin*)(Simulator::pluginManager.get("Concentration"));
  
  concentrationAccessorPtr=concentrationPlugin->getConcentrationAccessorPtr();
  ///initialize field ptr in the plugin
  concentrationPlugin->setConcentrationFieldPtr(concentrationField);
    
}

//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
void DiffusionSolverBiofilmFE::start() {

   if(diffConst> (1.0/6.0-0.05) ){ //hard coded condtion for stability of the solutions - assume dt=1 dx=dy=dz=1
                      
      cerr<<"CANNOT SOLVE DIFFUSION EQUATION: STABILITY PROBLEM - DIFFUSION CONSTANT TOO LARGE. EXITING..."<<endl;
      exit(0);

   }

    
    BlobFieldInitializer *blobFieldInitializerPtr=(BlobFieldInitializer*)simPtr->getClassRegistry()->getStepper("BlobInitializerBiofilm");
    blobDim=blobFieldInitializerPtr->getBlobDim();

    
   Dim3D dim = concentrationField->getDim(); 
   Point3D pt;
   Point3D ptCenter(dim.x/2,dim.y/2,dim.z/2);
   float distanceToCenter;

   for (pt.z = 0; pt.z < dim.z; pt.z++)
    for (pt.y = 0; pt.y < dim.y; pt.y++)
      for (pt.x = 0; pt.x < dim.x; pt.x++){
      
         distanceToCenter=distance(ptCenter,pt);
         if(distanceToCenter>insideRadius){///assume blob is spherical
            concentrationField->set(pt,ambientConcentration);
         }else{
            concentrationField->set(pt,insideInitialConcentration);
         }

         

      }
   

   //(this->*secretePtr)();
   
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
void DiffusionSolverBiofilmFE::updateBoundaryConditions(){
Point3D pt;
Dim3D dim = concentrationField->getDim();

Point3D ptCenter(dim.x/2,dim.y/2,dim.z/2);
float distanceToCenter;

   for (pt.z = 0; pt.z < dim.z; pt.z++)
    for (pt.y = 0; pt.y < dim.y; pt.y++)
      for (pt.x = 0; pt.x < dim.x; pt.x++){
      
         distanceToCenter=distance(ptCenter,pt);
         if(distanceToCenter>insideRadius){///assume blob is spherical
            concentrationField->set(pt,ambientConcentration);
            //cerr<<"setting concentration to "<<ambientConcentration<<" at:"<<pt<<endl;
         }
                         

      }
   

}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
float DiffusionSolverBiofilmFE::distance(Point3D a, Point3D b) {
  
  return sqrt((float)(a.x - b.x) * (a.x - b.x) +
              (float)(a.y - b.y) * (a.y - b.y) +
              (float)(a.z - b.z) * (a.z - b.z));
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
void DiffusionSolverBiofilmFE::updateCumulativeConcentration(){

   CellInventory::cellInventoryIterator cInvItr;
   ///loop over all the cells in the inventory
   Point3D com;
   CellG * cell;
   
   ///will need to zero cumulative concentrration for every cell before updating it   
   for(cInvItr=cellInventoryPtr->cellInventoryBegin() ; cInvItr !=cellInventoryPtr->cellInventoryEnd() ;++cInvItr ){
      
      cell=*cInvItr;
      
      concentrationAccessorPtr->get(cell->extraAttribPtr)->concentration=0.0;
        
   }

   ///Now will update cumulative concentration
   Point3D pt;
   Dim3D dim = concentrationField->getDim();
   CellG *currentCell=0;
   
   for (pt.z = 0; pt.z < dim.z; pt.z++)
    for (pt.y = 0; pt.y < dim.y; pt.y++)
      for (pt.x = 0; pt.x < dim.x; pt.x++){
         currentCell=cellFieldG->get(pt);
         if(!currentCell) continue;/// go to the next iteration in case you get Medium cell - they do not count

         concentrationAccessorPtr->get(currentCell->extraAttribPtr)->concentration+=concentrationField->get(pt);
         
      }   

   unsigned int counter=0;  
   for(cInvItr=cellInventoryPtr->cellInventoryBegin() ; cInvItr !=cellInventoryPtr->cellInventoryEnd() ;++cInvItr ){
      
      cell=*cInvItr;
      
      if(concentrationAccessorPtr->get(cell->extraAttribPtr)->concentration/cell->volume < 7.0){
         if(!concentrationAccessorPtr->get(cell->extraAttribPtr)->flag)  
            ++counter;
      }
        
   }
   cerr<<"THERE SHOULD BE "<<counter<<" cells sentenced to death by low concentration"<<endl;

}

//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
void DiffusionSolverBiofilmFE::step(const unsigned int _currentStep) {
   currentStep=_currentStep;
   (this->*diffusePtr)();
   updateBoundaryConditions();
   updateCumulativeConcentration();///update cumulative concentration in every cell - has to be done once diffusion field has changed
   //(this->*secretePtr)();

}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
void DiffusionSolverBiofilmFE::secrete() {

Point3D pt;
Dim3D dim = concentrationField->getDim();
CellG *currentCell=0;
short currentCellType=0;
float currentConcentration=0.0;



   for (pt.z = 0; pt.z < dim.z; pt.z++)
    for (pt.y = 0; pt.y < dim.y; pt.y++)
      for (pt.x = 0; pt.x < dim.x; pt.x++){
      
         currentCell=cellFieldG->get(pt);
         if(!currentCell) continue;/// go to the next iteration in case you get Medium cell - they do not secrete
         
         currentCellType = static_cast<short>( currentCell->type );

         if(currentCellType==2){///assume that only type 2 secretes something
            currentConcentration=concentrationField->get(pt);
            ///CHANGE HERE CELL SECTRETION
            concentrationField->set(pt,currentConcentration+secrConst);
         }
         //cerr<<"type="<<currentCellType<<" concentr="<<currentConcentration<<endl;
         
      }

}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
void DiffusionSolverBiofilmFE::secreteInCM() {

float currentConcentration=0.0;
CellInventory::cellInventoryIterator cInvItr;
///loop over all the cells in the inventory
Point3D com;
CellG * cell;
   
   
   for(cInvItr=cellInventoryPtr->cellInventoryBegin() ; cInvItr !=cellInventoryPtr->cellInventoryEnd() ;++cInvItr ){
      
      cell=*cInvItr;
      
      com.x=cell->xCM/ cell->volume ;
      com.y=cell->yCM/ cell->volume;
      com.z=cell->zCM/ cell->volume;
      
      currentConcentration=concentrationField->get(com);
      
      /// insert here cell type dependent decondition
      ///if(cell->type==//insert type)
      
      concentrationField->set(com,currentConcentration+secrConst); ///may change currentConcentration+secrConst to more sophisticated expression
   }



}


//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
void DiffusionSolverBiofilmFE::diffuse() {




/// 'n' denotes neighbor

///this is the diffusion equation
///C_{xx}+C_{yy}+C_{zz}=(1/a)*C_{t}
///a - diffusivity - diffConst

///Finite difference method:
///T_{0,\delta \tau}=F*\sum_{i=1}^N T_{i}+(1-N*F)*T_{0}
///N - number of neighbors
///will have to double check this formula


Point3D pt, n, neighborPoint;
unsigned int token = 0;
double distance;
Dim3D dim = concentrationField->getDim();
CellG * currentCellPtr=0,*nCell=0;

short currentCellType=0;
float concentrationSum=0.0;
float updatedConcentration=0.0;
float currentConcentration=0.0;
short neighborCounter=0;


float F;/// F=a*\delta \tau / (\delta x)^2
        /// in the simplest case (\delta*\tau=1 \delta x=1) F=a;
float G=0.0;

float gLocal=0.0;

   F=diffConst;
   G=decConst;

   for (pt.z = 0; pt.z < dim.z; pt.z++)
    for (pt.y = 0; pt.y < dim.y; pt.y++)
      for (pt.x = 0; pt.x < dim.x; pt.x++){

         currentCellPtr=cellFieldG->get(pt);
         //if(!currentCellPtr) continue; /// go to the next iteration in case you get Medium cell - medium does not participate in the diffusion
         //                              ///(model dependent may be changed if necessary

         updatedConcentration=0.0;
         concentrationSum=0.0;
         neighborCounter=0;
         token=0; ///have to zero the token every time we access neighbors - otherwise the neighbor distance will keep growing up
         ///loop over nearest neighbors
         while (true) {
            n = cellFieldG->getNeighbor(pt, token, distance, false);


            ///cerr<<"pt="<<pt<<" n="<<n<<" distance="<<distance<<" token="<<token<<endl;

            if (distance > 1) break;

               nCell = cellFieldG->get(n); //pulls out cell from a field
               if( ! nCell && avoidMedium ) continue; /// assume that medium does not participate in the diffusion
                                    ///(model dependent may be changed if necessary)

               if (nCell && avoidType(nCell->type)) continue; /// have to make sure that neighbor has ptr != 0
               
               ///cerr<<"neighborPoint="<<neighborPoint<<" nCell="<<nCell<<endl;
               concentrationSum += concentrationField->get(n);
               ++neighborCounter;

         }

         if( currentCellPtr && (currentCellPtr->type == 1 || currentCellPtr->type == 2)){   ///if bacteria
            gLocal=G;

         }else{
            gLocal=0.0;
         }
         
         currentConcentration=concentrationField->get(pt);
         //updatedConcentration = F*concentrationSum+(1.0-F*(neighborCounter+gLocal))*currentConcentration;
         updatedConcentration = F*concentrationSum+(1.0-F*neighborCounter-gLocal)*currentConcentration;
         
         
         //cerr<<"updatedConcentration="<<updatedConcentration<<endl;
         ///changed F*concentrationSum+(1.0-(neighborCounter*F+G))*currentConcentration; 
         ///to F*concentrationSum+(1.0-F*(neighborCounter+G))*currentConcentration; 
         
         if (updatedConcentration < 0.0) {
            cerr<<"got negative updated Concentration="<<updatedConcentration<<endl;
            updatedConcentration = 0.0; ///
         }
         scratchField->set(pt,updatedConcentration);
      }
      ///copy updated values from scratch to concentration field
      scratch2Concentration(scratchField,concentrationField);


/*      if(!(currentStep%10)){

         outputField(cerr,concentrationField);
      }*/
      

}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
void DiffusionSolverBiofilmFE::scratch2Concentration( Field3DImpl<float> *scratchField, Field3DImpl<float> *concentrationField){

Point3D pt;
Dim3D dim = concentrationField->getDim();
float tempValue;

   for (pt.z = 0; pt.z < dim.z; pt.z++)
    for (pt.y = 0; pt.y < dim.y; pt.y++)
      for (pt.x = 0; pt.x < dim.x; pt.x++){
         tempValue=scratchField->get(pt);
         concentrationField->set(pt,tempValue);
         scratchField->set(pt,0.0);
      }
}

//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
void DiffusionSolverBiofilmFE::outputField( std::ostream & _out, Field3DImpl<float> *_concentrationField){
Point3D pt;
Dim3D dim = concentrationField->getDim();
float tempValue;

   for (pt.z = 0; pt.z < dim.z; pt.z++)
    for (pt.y = 0; pt.y < dim.y; pt.y++)
      for (pt.x = 0; pt.x < dim.x; pt.x++){
         tempValue=concentrationField->get(pt);
         _out<<pt<<" "<<tempValue<<endl;
      }
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
void DiffusionSolverBiofilmFE::readXML(XMLPullParser &in) {
  in.skip(TEXT);
  while (in.check(START_ELEMENT)) {
    if (in.getName() == "DiffusionConstant") {
      diffConst = BasicString::parseDouble(in.matchSimple());

    }else if (in.getName() == "SecretionConstant") {
      secrConst = BasicString::parseDouble(in.matchSimple());

    }else if (in.getName() == "DecayConstant") {  /// NIKO
      decConst = BasicString::parseDouble(in.matchSimple());
      
    }else if (in.getName() == "AmbientConcentration") {  /// NIKO
      ambientConcentration = BasicString::parseDouble(in.matchSimple());
      
    }
    else if (in.getName() == "InsideInitialConcentration") {  /// NIKO
      insideInitialConcentration = BasicString::parseDouble(in.matchSimple());
    }
    else if (in.getName() == "InsideRadius") {  /// NIKO
      insideRadius = BasicString::parseInteger(in.matchSimple());
    }

    else if (in.getName() == "TypeToAvoid") {  /// NIKO
      unsigned short type = BasicString::parseInteger(in.matchSimple());
      if(type < numeric_limits<unsigned char>::min() || type > numeric_limits<unsigned char>::max() ){
         cerr<<"Type value is: "<<type<<" but should be between "<<(int)numeric_limits<unsigned char>::min()
         <<" and "<<(int)numeric_limits<unsigned char>::max()<<endl;
         ASSERT_OR_THROW("Out of range type value",false);
      }
      
      if(type==0){
         avoidMedium=true;
      }else{
         typesToAvoid.push_back(type);
      }
    }
    
    
    else if (in.getName() == "FieldName") {
      concentrationFieldName = in.matchSimple();
      ///register concentration field to make it accesible from external viewers etc.
      simPtr->registerConcentrationField(concentrationFieldName , concentrationField );


      
      
    }
     else {
      throw BasicException(string("Unexpected element '") + in.getName() + 
            "'!", in.getLocation());
    }

    in.skip(TEXT);
  }

}

//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
bool DiffusionSolverBiofilmFE::avoidType(unsigned char type){
   static unsigned int i;
   
   for(i=0 ; i < typesToAvoid.size() ; ++i){
      if(type==typesToAvoid[i])
         return true;
   }

   return false;

}

//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

