/*************************************************************************
 *    CompuCell - A software framework for multimodel simulations of     *
 * biocomplexity problems Copyright (C) 2003 University of Notre Dame,   *
 *                             Indiana                                   *
 *                                                                       *
 * This program is free software; IF YOU AGREE TO CITE USE OF CompuCell  *
 *  IN ALL RELATED RESEARCH PUBLICATIONS according to the terms of the   *
 *  CompuCell GNU General Public License RIDER 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 2 of the   *
 *         License, or (at your option) any later version.               *
 *                                                                       *
 * This program 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 this program; if not, write to the Free Software       *
 *      Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.        *
 *************************************************************************/

#include "NeighborTrackerPlugin.h"

#include <CompuCell3D/Simulator.h>
#include <CompuCell3D/Potts3D/Potts3D.h>
// #include <CompuCell3D/Field3D/AdjacentNeighbor.h>
#include <CompuCell3D/plugins/Volume/VolumePlugin.h>
#include <CompuCell3D/plugins/Volume/VolumeEnergy.h>
//#include <CompuCell3D/plugins/BoundaryTracker/BoundaryTracker.h>
#include <CompuCell3D/Potts3D/CellInventory.h>

using namespace CompuCell3D;

#include <XMLCereal/XMLPullParser.h>
#include <XMLCereal/XMLSerializer.h>

#include <iostream>
#include <cmath>
using namespace std;

NeighborTrackerPlugin::NeighborTrackerPlugin() :
   cellFieldG(0),
   periodicX(false),
   periodicY(false),
   periodicZ(false),
   checkSanity(false)
   {}

NeighborTrackerPlugin::~NeighborTrackerPlugin() {
  //if (surfaceEnergy) delete surfaceEnergy;
}

void NeighborTrackerPlugin::init(Simulator *_simulator) {

  cout<<"INITIALIZING CELL BOUNDARYTRACKER PLUGIN"<<endl;
  simulator=_simulator;
  Potts3D *potts = simulator->getPotts();
  cellFieldG = potts->getCellFieldG();

  ///getting cell inventory
  cellInventoryPtr=& potts->getCellInventory(); 



  ///will register NeighborTracker here
  BasicClassAccessorBase * cellBTAPtr=&neighborTrackerAccessor;
   ///************************************************************************************************  
  ///REMARK. HAVE TO USE THE SAME BASIC CLASS ACCESSOR INSTANCE THAT WAS USED TO REGISTER WITH FACTORY
   ///************************************************************************************************  
  potts->getCellFactoryGroupPtr()->registerClass(cellBTAPtr);
  //potts->registerCellDynamicClassNode(&classNode);

  //surfaceEnergy = new SurfaceEnergy(classNode, cellField);
  //potts->registerEnergyFunction(surfaceEnergy);

  potts->registerCellGChangeWatcher(this);
  
  fieldDim=cellFieldG->getDim();
  
  adjNeighbor = AdjacentNeighbor(fieldDim);
  if(potts->getBoundaryXName()=="Periodic"){ 
      adjNeighbor.setPeriodicX();
      periodicX=true;
  }
  if(potts->getBoundaryYName()=="Periodic"){
      adjNeighbor.setPeriodicY();
      periodicY=true;
  }
  if(potts->getBoundaryZName()=="Periodic"){
      adjNeighbor.setPeriodicZ();
      periodicZ=true;
  }

  
  
  maxIndex=adjNeighbor.getField3DIndex().getMaxIndex();
  
  
}

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

void NeighborTrackerPlugin::field3DChange(const Point3D &pt, CellG *newCell,
				  CellG *oldCell) {

   
    if(newCell==oldCell){//happens during multiple calls to se fcn on the same pixel woth current cell - should be avoided
      return;
    }

   
/*   vector<Point3D> const & adjNeighborOffsetsVec_inn=adjNeighbor.getAdjFace2FaceNeighborOffsetVec(pt);
   unsigned int neighborSize=adjNeighborOffsetsVec_inn.size();
   
   const vector<Point3D> & adjFace2FaceNeighborOffsetsVec_inn=adjNeighbor.getAdjFace2FaceNeighborOffsetVec(pt);
   unsigned int face2FaceNeighborSize=adjFace2FaceNeighborOffsetsVec_inn.size();*/
   
   const Field3DIndex & field3DIndex=adjNeighbor.getField3DIndex();
   
   long currentPtIndex=0;
   long adjNeighborIndex=0;
   long adjFace2FaceNeighborIndex=0;
   
   CellG * currentCellPtr=0;
   CellG * adjCellPtr=0;

  unsigned int token = 0;
  double distance;
  int oldDiff = 0;
  int newDiff = 0;
  Point3D n;
  Point3D ptAdj;
  CellG *nCell=0;
  
   

   set<NeighborSurfaceData> * oldCellNeighborSurfaceDataSetPtr=0;
   set<NeighborSurfaceData> * newCellNeighborSurfaceDataSetPtr=0;
   pair<set<NeighborSurfaceData>::iterator,bool > set_NSD_itr_OK_Pair;
   set<NeighborSurfaceData>::iterator set_NSD_itr;

      
   if(newCell){

      newCellNeighborSurfaceDataSetPtr =  &neighborTrackerAccessor.get(newCell->extraAttribPtr)->cellNeighbors;
   }

   if(oldCell){

      oldCellNeighborSurfaceDataSetPtr =  &neighborTrackerAccessor.get(oldCell->extraAttribPtr)->cellNeighbors;
   }

   
   currentPtIndex=field3DIndex.index(pt);
   currentCellPtr=cellFieldG->getByIndex(currentPtIndex);

//    cerr<<"old="<<oldCell<<" new="<<newCell<<endl;
//    cerr<<"CHANGE POINT="<<pt<<" flipNeighbor="<<simulator->getPotts()->getFlipNeighbor()<<endl;
//    cerr<<"currentPtIndex="<<currentPtIndex<<" flip neighborindex="<<field3DIndex.index(simulator->getPotts()->getFlipNeighbor())<<endl;

   token = 0;
   distance = 0;
         
   if(oldCell){
      
      /// Now will adjust common surface area with cell neighbors
      long temp_index;
      while (true){
         ptAdj=pt;
         //ptAdj+=adjFace2FaceNeighborOffsetsVec_inn[i];
         ptAdj= cellFieldG->getNeighbor(pt, token, distance, false);
         if (distance > 1) break;//only nearest neighbors

         adjFace2FaceNeighborIndex=field3DIndex.index(ptAdj);
         
            

               //adjCellPtr=cellFieldG->getByIndex(adjFace2FaceNeighborIndex);
               adjCellPtr=cellFieldG->get(ptAdj);
   
               if( adjCellPtr != oldCell ){ /// will decrement commSurfArea with all face 2 face neighbors
/*                  cerr<<"adjCellPtr="<<adjCellPtr<<" oldCell="<<oldCell<<endl;
                  cerr<<"ptAdj="<<ptAdj<<" pt="<<pt<<endl;*/
                  set_NSD_itr = oldCellNeighborSurfaceDataSetPtr->find(NeighborSurfaceData(adjCellPtr));
                  if( set_NSD_itr != oldCellNeighborSurfaceDataSetPtr->end() ){
                     set_NSD_itr->decrementCommonSurfaceArea(*set_NSD_itr); ///decrement commonSurfArea with adj cell
                     if(set_NSD_itr->OKToRemove()) ///if commSurfArea reaches 0 I remove this entry from cell neighbor set
                        oldCellNeighborSurfaceDataSetPtr->erase(set_NSD_itr);
   
                  }else{
                     cerr<<"Could not find cell address in the boundary - set of cellNeighbors is corrupted. Exiting ..."<<endl;
                     exit(0);
                  }
   
   
                  if(adjCellPtr){ ///now process common area for adj cell provided it is not the oldCell
                     set<NeighborSurfaceData> &set_NSD_ref = neighborTrackerAccessor.get(adjCellPtr->extraAttribPtr)->cellNeighbors;
                     set<NeighborSurfaceData>::iterator sitr;
                     sitr=set_NSD_ref.find(oldCell);
                     if(sitr!=set_NSD_ref.end()){
                        sitr->decrementCommonSurfaceArea(*sitr); ///decrement common area
                        if(sitr->OKToRemove()) ///if commSurfArea reaches 0 I remove this entry from cell neighbor set
                           set_NSD_ref.erase(sitr);
   
                     }
                  }
               }
            


      }
   }
   
   token = 0;
   distance = 0;
   
   if(newCell){
      
      
      /// Now will adjust common surface area with cell neighbors      

      while (true){
         ptAdj=pt;
         ptAdj= cellFieldG->getNeighbor(pt, token, distance, false);
         if (distance > 1) break;//only nearest neighbors

         adjFace2FaceNeighborIndex=field3DIndex.index(ptAdj);
         
         

         
              adjCellPtr=cellFieldG->get(ptAdj);

            if( adjCellPtr != newCell ){ ///if adjCellPtr denotes foreign cell we increase common area and insert set entry if necessary
//                cerr<<"inserting adjCellPtr="<<adjCellPtr <<" ptAdj="<<ptAdj<<" into newCell="<<newCell<<" pt="<<pt<<endl;
               set_NSD_itr_OK_Pair=newCellNeighborSurfaceDataSetPtr->insert(NeighborSurfaceData(adjCellPtr));/// OK to insert even if
               ///duplicate, in such a case an iterator to existing NeighborSurfaceData(adjCellPtr) obj is returned

               set_NSD_itr=set_NSD_itr_OK_Pair.first;
               set_NSD_itr->incrementCommonSurfaceArea(*set_NSD_itr); ///increment commonSurfArea with adj cell

               if(adjCellPtr){ ///now process common area for adj cell
                  set<NeighborSurfaceData> &set_NSD_ref  = neighborTrackerAccessor.get(adjCellPtr->extraAttribPtr)->cellNeighbors;
                  pair<set<NeighborSurfaceData>::iterator,bool> sitr_OK_pair=set_NSD_ref.insert(newCell);
                  set<NeighborSurfaceData>::iterator sitr=sitr_OK_pair.first;
                  sitr->incrementCommonSurfaceArea(*sitr); ///increment commonSurfArea of adj cell with current cell
               }

            }

         


      }
   }

   token = 0;
   distance = 0;

   

   if(!oldCell){ ///this special case is required in updating common Surface Area with medium
                 ///in this case we update surface of adjCell only (we do not update medium's neighbors list or its contact surfaces)

      /// Now will adjust common surface area with cell neighbors
      while (true){
         ptAdj=pt;
         ptAdj= cellFieldG->getNeighbor(pt, token, distance, false);
         if (distance > 1) break;//only nearest neighbors
         
         adjFace2FaceNeighborIndex = field3DIndex.index(ptAdj);

            adjCellPtr=cellFieldG->get(ptAdj);
      
            if( adjCellPtr != oldCell && !(ptAdj == pt)){ /// will decrement commSurfArea with all face 2 face neighbors
//                cerr<<"!old cell section  adjCellPtr="<<adjCellPtr <<" ptAdj="<<ptAdj<<"  oldCell="<<oldCell<<" pt="<<pt<<endl;
               if(adjCellPtr){ ///now process common area for adj cell provided it is not the oldCell
                  set<NeighborSurfaceData> &set_NSD_ref = neighborTrackerAccessor.get(adjCellPtr->extraAttribPtr)->cellNeighbors;
                  set<NeighborSurfaceData>::iterator sitr;
                  sitr=set_NSD_ref.find(oldCell);
                  if(sitr!=set_NSD_ref.end()){
                     sitr->decrementCommonSurfaceArea(*sitr); ///decrement common area
                     if(sitr->OKToRemove()){ ///if commSurfArea reaches 0 I remove this entry from cell neighbor set
                        set_NSD_ref.erase(sitr);
//                         cerr<<"removing from boundary"<<endl;
                        
                     }
                     
                  }
               }
            }
         
         

      }

   
   }

   token = 0;
   distance = 0;

      
   if(!newCell){  ///this special case is required in updating common Surface Area with medium
                  ///in this case we update surface of adjCell only (we do not update medium's neighbors list or its contact surfaces)
                  

      while (true){
         ptAdj=pt;
         ptAdj= cellFieldG->getNeighbor(pt, token, distance, false);
         if (distance > 1) break;//only nearest neighbors
         
         adjFace2FaceNeighborIndex = field3DIndex.index(ptAdj);

         if(cellFieldG->isValid(ptAdj)){

            adjCellPtr=cellFieldG->get(ptAdj);

            if( adjCellPtr != newCell ){ ///if adjCellPtr denotes foreign cell we increase common area and insert set entry if necessary

               if(adjCellPtr){ ///now process common area of adj cell with medium in this case
                  set<NeighborSurfaceData> &set_NSD_ref  = neighborTrackerAccessor.get(adjCellPtr->extraAttribPtr)->cellNeighbors;
                  pair<set<NeighborSurfaceData>::iterator,bool> sitr_OK_pair=set_NSD_ref.insert(newCell);
                  set<NeighborSurfaceData>::iterator sitr=sitr_OK_pair.first;
                  sitr->incrementCommonSurfaceArea(*sitr); ///increment commonSurfArea of adj cell with current cell
               }

            }

         }


      }

   }

//       if(newCellBoundarySetPtr->size())
//          cerr<<" final newCellBoundarySetPtr->begin()="<<newCellBoundarySetPtr->begin()->pixelIndex<<endl;
//       else
//          cerr<<"SORRY THE SET IN EMPTY"<<endl;

   
   ///temporarily for testing purposes I set 
   
   if(checkSanity){
   
      ++changeCounter;

      if(!(changeCounter % checkFreq)){
         //cerr<<"OLD CELL ADR: "<<oldCell<<" NEW CELL ADR: "<<newCell<<endl;
         cerr<<"ChangeCounter:"<<changeCounter<<endl;
         testLatticeSanityFull();
   
      }
   }
   
   
}

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

double distance(double x1,double y1,double z1,double x2,double y2,double z2){
   return sqrt (
                  (x1-x2)*(x1-x2)+
                  (y1-y2)*(y1-y2)+
                  (z1-z2)*(z1-z2)
               );
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
void NeighborTrackerPlugin::readXML(XMLPullParser &in) {
   
   in.skip(TEXT);
   

   while (in.check(START_ELEMENT)) {
      if (in.getName() == "CheckLatticeSanityFrequency") {
      
         checkFreq = BasicString::parseUInteger(in.matchSimple());
         checkSanity=true;
   
      }
      else {
         throw BasicException(string("Unexpected element '") + in.getName() +
                              "'!", in.getLocation());
      }
   
      in.skip(TEXT);
   }
}

void NeighborTrackerPlugin::writeXML(XMLSerializer &out) {
  
}


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

void NeighborTrackerPlugin::testLatticeSanityFull(){

//    set<CellG *> set_of_visited_cells;

   Dim3D fieldDim=cellFieldG->getDim();
   //AdjacentNeighbor adjNeighbor(fieldDim);

//   vector<long> const & neighborVecRef=adjNeighbor.getAdjNeighborOffsetVec();
   
//    const Field3DIndex & field3DIndex=adjNeighbor.getField3DIndex();
   Point3D pt(0,0,0);
   Point3D ptAdj;
//    Point3D ptBoundary;

//    const vector<long> & adjNeighborOffsetsVec=adjNeighbor.getAdjNeighborOffsetVec();
//    int neighborSize=adjNeighborOffsetsVec.size();
// 
//    const vector<long> & adjFace2FaceNeighborOffsetsVec=adjNeighbor.getAdjFace2FaceNeighborOffsetVec();
//    int face2FaceNeighborSize=adjFace2FaceNeighborOffsetsVec.size();


//    long currentPtIndex=0;
//    long adjNeighborIndex=0;
//    long adjFace2FaceNeighborIndex=0;



   ///Now will have to get access to the pointers stored in cellFieldG from Potts3D


   CellG * currentCellPtr;
   CellG * adjCellPtr;
//    int localNeighborCounter=0;
//    int localCommonSurfaceArea=0;
//    set<BoundaryData>::iterator bdsitr;
//    set<NeighborSurfaceData>::iterator nsdsitr;

//    bool isInBoundary=false;
//    bool inInNeighborSurfaceData=false;

/*   map<CellG*,set<BoundaryData> > mapCellBoundaryData;
   map<CellG*,set<BoundaryData> >::iterator mitr;*/

   map<CellG*,set<NeighborSurfaceData> > mapCellNeighborSurfaceData;
   map<CellG*,set<NeighborSurfaceData> >::iterator mitr;
   
   /// check neighbors of each cell - will loop over each lattice point and check if point belongs to boundary, then will examine neighbors

   unsigned int token = 0;
   double distance;

   set<NeighborSurfaceData> * set_NSD_ptr;
   
   pair<set<NeighborSurfaceData>::iterator,bool > set_NSD_itr_OK_Pair;
   set<NeighborSurfaceData>::iterator set_NSD_itr;

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

            token=0;
            distance=0;
            
//             currentPtIndex=field3DIndex.index(pt);
            //currentCellPtr=cellFieldG->getByIndex(currentPtIndex);
            currentCellPtr=cellFieldG->get(pt);
            if(!currentCellPtr)
               continue; //skip the loop if the current latice site does not belong to any cell
            if(!isBoundaryPixel(pt))
               continue; //inner pixel does not bring new neighbors
               
            mitr=mapCellNeighborSurfaceData.find(currentCellPtr);
            if(mitr != mapCellNeighborSurfaceData.end() ){
               set_NSD_ptr = & (mitr->second) ;
            }else{
               mapCellNeighborSurfaceData.insert(make_pair(currentCellPtr,set<NeighborSurfaceData>()));
               mitr=mapCellNeighborSurfaceData.find(currentCellPtr);
               set_NSD_ptr = &(mitr->second);
            }

            while(true){
               ptAdj= cellFieldG->getNeighbor(pt, token, distance, false);
               if (distance > 1) break;//only nearest neighbors
               
               adjCellPtr=cellFieldG->get(ptAdj);
               
               if(adjCellPtr != currentCellPtr){
                  set_NSD_itr_OK_Pair = set_NSD_ptr->insert(NeighborSurfaceData(adjCellPtr));/// OK to insert even if
                  ///duplicate, in such a case an iterator to existing NeighborSurfaceData(adjCellPtr) obj is returned
   
                  set_NSD_itr=set_NSD_itr_OK_Pair.first;
                  set_NSD_itr->incrementCommonSurfaceArea(*set_NSD_itr); ///increment commonSurfArea with adj cell

               }
               
            }

         }

         
      //Now do lattice sanity checks
      if(mapCellNeighborSurfaceData.size() != cellInventoryPtr->getCellInventorySize()){
         cerr<<"Number of cells in the mapCellNeighborSurfaceData = "<<mapCellNeighborSurfaceData.size()
         <<" is different than in cell inventory:  "<< cellInventoryPtr->getCellInventorySize()<<endl;
         exit(0);
      }
      CellInventory::cellInventoryIterator cInvItr;
      CellG * cell;
      int counter=0;
      
      for(cInvItr=cellInventoryPtr->cellInventoryBegin() ; cInvItr !=cellInventoryPtr->cellInventoryEnd() ;++cInvItr ){
         
         cell=*cInvItr;
         mitr = mapCellNeighborSurfaceData.find(cell);
         if(mitr==mapCellNeighborSurfaceData.end()){
            cerr<<"Cell "<<cell<<" does not appear in the just initialized mapCellNeighborSurfaceData"<<endl;
            exit(0);
            
         }
         set_NSD_ptr=&(mitr->second);
         
         if(! (*set_NSD_ptr == neighborTrackerAccessor.get(cell->extraAttribPtr)->cellNeighbors)){
            cerr<<"Have checked "<<counter<<" cells"<<endl;
            cerr<<"set of NeighborSurfaceData do not match for cell: "<<cell<<endl;
            exit(0);
         }
         
         ++counter;
      }
   
     cerr<<"FULL TEST: LATTICE IS SANE!!!!!"<<endl;

}

///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
bool NeighborTrackerPlugin::isBoundaryPixel(Point3D pt){
   
//     const vector<Point3D> & adjNeighborOffsetsVec=adjNeighbor.getAdjFace2FaceNeighborOffsetVec(pt);
    CellG * currentCellPtr=cellFieldG->get(pt);;
//     CellG * adjCellPtr;
    CellG * nCell;
    unsigned int token = 0;
    double distance = 0;
    Point3D n;

    

      while (true) {
         n = cellFieldG->getNeighbor(pt, token, distance, false);
         if (distance > 1) break;//only nearest neighbors
         nCell = cellFieldG->get(n);
         if(nCell != currentCellPtr)
            return true;
      }
      
   return false;
    
}

