package simulate;
import java.util.Observable;
import java.awt.Color;
import simulate.units.*;

/**
 * General-purpose integrator for hard potentials.
 * Integrates equations of motion through time by advancing atoms from one collision to the
 * next.  Determination of time of collision and implementation of collision
 * dynamics is handled by the potential between the atoms, which therefore must
 * implement PotentialHard.  Each atom keeps in its Agent (obtained from this integrator) the shortest
 * time to collision with any of the atoms uplist of it (as defined by the iterator from 
 * the phase).
 *
 * @see PotentialHard
 *
 */
 
public final class IntegratorHard extends IntegratorMD {

//convenience handle to the agent holding information about the next collision
private Agent nextCollider;
//iterators for looping through atoms
private AtomPair.Iterator upPairIterator, downPairIterator;
private Atom.Iterator upAtomIterator, downAtomIterator;
private AtomPair atomPair;
private PotentialHard spacePotential;
//first of a linked list of meters that are called each time a collision is processed
private IntegratorHard.Meter firstIMeter = null;

//boolean bb = true;  used in debugging
            
public IntegratorHard() {
    super();
    setTimeStep(Default.TIME_STEP,Default.TIME_STEP_UNIT);
}

/**
 * Identifies to this integrator the phase containing the atoms it is integrating.
 */
 public void registerPhase(Phase p) {
    super.registerPhase(p);
    upPairIterator = p.iteratorFactory.makeAtomPairIteratorUp();
    downPairIterator = p.iteratorFactory.makeAtomPairIteratorDown();
    upAtomIterator = p.iteratorFactory.makeAtomIteratorUp();
    downAtomIterator = p.iteratorFactory.makeAtomIteratorDown();
    atomPair = new AtomPair(p);
    spacePotential = (PotentialHard)Simulation.space().makePotential(p);
}

          
/** 
 * Steps all atoms across time interval timeStep, handling all intervening collisions.
 */
public void doStep() {doStep(timeStep);}

/**
 * Steps all atoms across time interval tStep, handling all intervening collisions.
 * This method is called recursively, each time with a timestep equal to the amount of 
 * time between the latest collision and the endpoint of the timestep identified in its
 * highest-level call
 */
public void doStep(double tStep) {

    if(tStep < nextCollider.getCollisionTime()) {
        advanceAcrossTimeStep(tStep);
        if(isothermal) {
            scaleMomenta(Math.sqrt(this.temperature/(firstPhase.kineticTemperature())));
        }
//        debugMethod();
        return;
    }

    double tStepNew = tStep - nextCollider.getCollisionTime();
    advanceToCollision();
    doStep(tStepNew);
    return;
}

//debugging tools
// Colors atoms according to whether they are in the up or down neighborlist of the first atom
    private void debugMethod() {
        for(Atom a=firstPhase.firstAtom(); a!=null; a=a.nextAtom()) {
            a.setColor(Color.black);
        }
        Atom central = firstPhase.firstAtom();
//        central.setColor(((Space2DCell.Coordinate)central.coordinate).cell.color);
        upPairIterator.reset(central);
        while(upPairIterator.hasNext()) {
            AtomPair pair = upPairIterator.next();
            pair.atom2.setColor(Color.blue);
        }
        downPairIterator.reset(central);
        while(downPairIterator.hasNext()) {
            AtomPair pair = downPairIterator.next();
            pair.atom2.setColor(Color.green);
        }
        /*
        
        upAtomIterator.reset(central);
        while(upAtomIterator.hasNext()) {
            Atom atom = upAtomIterator.next();
            atom.setColor(Color.blue);
        }
        downAtomIterator.reset(central);
        while(downAtomIterator.hasNext()) {
            Atom atom = downAtomIterator.next();
            atom.setColor(Color.green);
        }        
        */
//        central.setColor(((Space2DCell.Coordinate)central.coordinate).cell.color);
        central.setColor(Color.red);
    }


/**
 * Loops through all atoms to identify the one with the smallest value of collisionTime
 * Collision time is obtained from the value stored in the Integrator.Agent from each atom.
 */
protected void findNextCollider() {
    //find next collision pair by looking for minimum collisionTime
    double minCollisionTime = Double.MAX_VALUE;
    upAtomIterator.reset();
    while(upAtomIterator.hasNext()) {
        Agent ia = (Agent)upAtomIterator.next().ia;
        double ct = ia.getCollisionTime();
        if( ct < minCollisionTime) {
            minCollisionTime = ct;
            nextCollider = ia;
        }
    }
}

/**
 * Advances to next collision, applies collision dynamics to colliders and updates collision time/partners.
 * Also invokes collisionAction method of all registered integrator meters.
 */
protected void advanceToCollision() {
            
    advanceAcrossTimeStep(nextCollider.getCollisionTime());
    Atom partner = nextCollider.getCollisionPartner();
    if(partner == nextCollider.atom) {  //"self-collision" is code for "collision" with space
        Atom a = nextCollider.atom;
        atomPair.reset(a,a);
        nextCollider.getCollisionPotential().bump(atomPair);
        upList(a);
        downList(a);
        
        //reset collision partners of atoms that are now up from this atom but still list it as their
        //collision partner.  Assumes this atom was moved down list, but this won't always be the case
        //This bit could be made more efficient
        upPairIterator.reset(a);
        while(upPairIterator.hasNext()) {
            AtomPair pair = upPairIterator.next();
            if(((Agent)pair.atom2().ia).getCollisionPartner() == a) {  //upList atom could have atom as collision partner if atom was just moved down list
                upList(pair.atom2());
            }
        }
        
        //to keep collision lists perfect, should do an upList on atoms that had this
        //atom on its neighbor list, but no longer do because it has moved away
    }
    else {
//        Atom partnerNextAtom = partner.nextMoleculeFirstAtom();  //put this back in for multiatomic speciesSwitch; also need to do more work with loop below
//        Atom partnerNextAtom = partner.coordinate.nextNeighbor().atom();
        Atom partnerNextAtom = null;  //remove this -- temporary
        atomPair.reset(nextCollider.atom,partner);
        PotentialHard potential = nextCollider.getCollisionPotential();
        potential.bump(atomPair);
        for(IntegratorHard.Meter m=firstIMeter; m!=null; m=m.nextIMeter) {m.collisionAction(potential);}
        Atom a1N = atomPair.atom1();  //bump might have changed nextCollider or partner to new atoms in atomPair
        Atom a2P = atomPair.atom2();
//        nextCollider.getCollisionPotential().bump(nextCollider.atom,partner);
                
//        boolean upListedN = false;
//        boolean upListedP = false;
//        for(Atom a=firstPhase.firstAtom(); a!=partnerNextAtom; a=a.nextAtom()) {  //note that nextCollider's or partner's position in linked-list may have been moved by the bump method
//        upAtomIterator.reset(firstPhase.firstAtom());

//   Do upList for any atoms that were scheduled to collide with atoms colliding now
        upAtomIterator.reset();  //first atom in first cell
        while(upAtomIterator.hasNext()) {
            Atom a = upAtomIterator.next();
            if(a == a1N || a == a2P) continue;
            if(a == partnerNextAtom) break;
            Atom aPartner = ((Agent)a.ia).getCollisionPartner();
            if(aPartner==nextCollider.atom || aPartner==partner) {
                upList(a);
//                if(a == nextCollider.atom) {upListedN = true;}
//                else if(a == partner) {upListedP = true;}
            }
        }
//        if(!upListedN) {upList(atomPair.atom1());}  //nextCollider.atom
//        if(!upListedP) {upList(atomPair.atom2());}  //partner
        upList(a1N);
        upList(a2P);
        downList(a1N);
        downList(a2P);
    }

    findNextCollider();
}



/**
 * Advances all atom coordinates by tStep, without any intervening collisions.
 * Uses free-flight kinematics with or without the presence of a graviational field
 * (as defined in the phase)
 */
protected void advanceAcrossTimeStep(double tStep) {
            
    if(firstPhase.noGravity) {
        for(Atom a=firstPhase.firstAtom(); a!=null; a=a.nextAtom()) {
            ((Agent)a.ia).decrementCollisionTime(tStep);
            if(a.isStationary()) {continue;}  //skip if atom is stationary
            a.translateBy(tStep*a.rm(),a.momentum());
        }
    }
    else {
        double t2 = tStep*tStep;
        for(Atom a=firstPhase.firstAtom(); a!=null; a=a.nextAtom()) {
            ((Agent)a.ia).decrementCollisionTime(tStep);
            if(a.isStationary()) {continue;}  //skip if atom is stationary
            a.translateBy(tStep*a.rm(),a.momentum());
            a.translateBy(t2,firstPhase.gravity.gVector);
            a.accelerateBy(tStep*a.mass(),firstPhase.gravity.gVector);
        }
    }
}

/**
 * Update of collision list when gravity is changed.
 * Implementation of Observer interface
 */
public void update(Observable o, Object arg) {
    if(o instanceof Gravity) {
    for(Atom a=firstPhase.firstAtom(); a!=null; a=a.nextAtom()) {
        if(a.isStationary() || ((Agent)a.ia).getCollisionPartner().isStationary()) {
            upList(a);
        }
        if(a.isStationary()) {downList(a);}
    }
    findNextCollider();
    }
}
          

/**
 * Looks for collision of this atom with all atoms upList of the given atom, 
 * and sets this atom's agent to the next collision time and partner found there.
 */
protected void upList(Atom atom) {  
            
    double minCollisionTime = Double.MAX_VALUE;
    Agent aia = (Agent)atom.ia;
    aia.setCollision(minCollisionTime, null, null);
            
    upPairIterator.reset(atom);
    while(upPairIterator.hasNext()) {
        AtomPair pair = upPairIterator.next();
        PotentialHard potential = (PotentialHard)Simulation.getPotential(pair);
        double time = potential.collisionTime(pair);
        if(time < minCollisionTime) {
            minCollisionTime = time;
            aia.setCollision(time,pair.atom2(),potential);  //atom2 is inner loop
        }
    }
        
    //Examine interaction with space
    atomPair.reset(atom,atom);
    double time = spacePotential.collisionTime(atomPair);
    if(time < minCollisionTime) {
        minCollisionTime = time;
        aia.setCollision(time,atom,spacePotential);  //"self-collision" is code for collision with space
    }
}

/**
 * Looks for collision of this atom with all atoms downList of the given atom, 
 * and updates the partner atom's agent if collision time with this is less than
 * its current collision time.
 */
protected void downList(Atom atom) {
            
    downPairIterator.reset(atom);
    while(downPairIterator.hasNext()) {
        AtomPair pair = downPairIterator.next();
        Agent aia = (Agent)pair.atom2().ia;  //atom2 is inner loop
        PotentialHard potential = (PotentialHard)Simulation.getPotential(pair);
        double time = potential.collisionTime(pair);
        if(time < aia.getCollisionTime()) {
            aia.setCollision(time,atom,potential);
        }
    }
}
          

/**
 * Sets up the integrator for action.
 * Deploy agents, do an upList call for each atom and find the next collider
 */
public void initialize() {
    deployAgents();
    if(isothermal) {
        scaleMomenta(Math.sqrt(this.temperature/(firstPhase.kineticTemperature())));
    }
    Atom.Iterator iterator = firstPhase.iteratorFactory.makeAtomIteratorUp();
    iterator.reset();
    while(iterator.hasNext()) {upList(iterator.next());}
    findNextCollider();
}
          
/**
 * Crude method to enforce constant-temperature constraint
 * Scales momenta of all atoms by a constant factor so that 
 * phase adheres to setpoint temperature.  Updates collision times appropriately.
 */
    public void scaleMomenta(double s) {
        double rs = 1.0/s;
        for(Atom a=firstPhase.firstAtom(); a!=null; a=a.nextAtom()) {
            a.coordinate.scaleMomentum(s);
            ((Agent)a.ia).collisionTime *= rs;
        }
    }

 /**
  * Produces the Agent defined by this integrator.
  * One instance of an Agent is placed in each atom controlled by this integrator.
  */
    public final Integrator.Agent makeAgent(Atom a) {
        return new Agent(a);
    }
 
 /**
  * Agent defined by this integrator.
  * Holds information about the time to next collision (considering only atoms
  * uplist of the atom), the collisionPartner (atom that this one will be colliding with),
  * and a handle to the potential governing the interactions of this atom and its collision partner
  * (this is kept for convenience, so it won't have to be determined again when the collision
  * is processed).
  */
    public final static class Agent implements Integrator.Agent {  //need public so to use with instanceof
        public Atom atom;
 //       public double time0;  //time since last collision
        private double collisionTime = Double.MAX_VALUE; //time to next collision
        private Atom collisionPartner;  //next atom scheduled for collision by atom containing this Agent
        private PotentialHard collisionPotential;  //potential governing interaction between collisionPartner and atom containing this Agent
 
 //Momentum and heat accumulators used by some meters.  These are deprecated in favor of a corresponding IMeter
        public double pAccumulator;  //accumulated momentum, for calculation of pressure
        public double qAccumulator;  //accumulated energy, for calculation of heat transfer

        public Agent(Atom a) {
            atom = a;
            zeroAccumulators();
        }
  
    /**
     * Sets parameters associated with next collision
     *
     * @param time    time to collision of this agent's atom with an atom uplist of it
     * @param partner the atom this one will collide with next
     * @param p       the potential for interactions between this atom and its collision partner
     */
        public final void setCollision(double time, Atom partner, PotentialHard p) {
            collisionTime = time;
            collisionPartner = partner;
            collisionPotential = p;
        }

    /**
     * Decreases the recorded time to collision of this atom
     * This action is performed when the atom is advanced without a collision
     */
        public final void decrementCollisionTime(double interval) {
            collisionTime -= interval;
//            time0 += interval;
        }
    /**
     * Accessor method for the time to next collision of this atom
     */
        public final double getCollisionTime() {return collisionTime;}
    /**
     * Accessor method to the collision partner of this atom
     */
        public final Atom getCollisionPartner() {return collisionPartner;}
    /**
     * Accessor method to the potential between this atom and its collision partner
     */
        public final PotentialHard getCollisionPotential() {return collisionPotential;}

     /**
      * Sets to zero the qAccumulator and pAccumulator
      */
        public final void zeroAccumulators() {
            qAccumulator = 0.0;
            pAccumulator = 0.0;
        }
        
    }
    
    /**
     * This colorScheme acts to color differently the two atoms that are scheduled to collide next
     * Highlight colors are specified by the colliderColor and partnerColor fields; all other
     * atoms are colored with the baseColor
     */
    public class HighlightColliders extends Phase.ColorScheme {
        
        public HighlightColliders() {super();}
        public HighlightColliders(Phase p) {super(p);}
        /**
         * Color applied to the downList atom of the colliding pair
         */
        public Color colliderColor = Color.red;
        /**
         * Color applied to the upList atom of the colliding pair
         */
        public Color partnerColor = Color.blue;
        /**
         * Applies the special colors to the colliding pair while coloring all other atoms with baseColor.
         */ 
        public void colorAtoms() {
            iterator.reset();
            while(iterator.hasNext()) {
                Atom a = iterator.next();
                if(a == nextCollider.atom) {a.setColor(colliderColor);}
                else if(a == nextCollider.collisionPartner) {a.setColor(partnerColor);}
                else {a.setColor(baseColor);}
            }
        }
    }
    
    /**
     * Superclass of meters that need to perform some action each time a collision is processed
     */
    public abstract class Meter extends simulate.Meter {
    
        /**
         * Pointer to next meter in a linked list of integrator meters
         * First such meter is designated firstIMeter in the IntegratorHard class
         */
        public Meter nextIMeter;
        
        public Meter() {
            super();
            nextIMeter = firstIMeter;  //put this new meter at beginning of list of integrator meters
            firstIMeter = this;        //firstIMeter is a property of IntegratorHard
        }
        /**
         * Method to perform action each time a collision is processed
         * This method is invoked by the advanceToCollision method of IntegratorHard
         */
        public abstract void collisionAction(PotentialHard p);
    }
    
    /**
     * Meter for the pressure of a hard potential.
     * Performs sum of collision virial over all collisions, dividing by 
     * appropriate terms to obtain the pressure.
     */
    public class MeterPressure extends IntegratorHard.Meter {
        
        double accumulator;
        double timeSum;
        public MeterPressure() {
            super();
            setLabel("PV/Nk");
            accumulator = 0.0;
            timeSum = 0.0;
        }
        /**
         * Implementation of Integrator.IntervalListener interface.
         * Updates sums and zeros accumulators at each interval event
         */
        public void intervalAction(Integrator.IntervalEvent evt) {
            timeSum += timeStep * interval;
            updateSums();
            timeSum = 0.0; 
            accumulator = 0.0;
        }
        /**
         * Returns P*V/N*kB = T - (virial sum)/(elapsed time)/(space dimension)/(number of atoms)
         */
        public double currentValue() {
            double flux = -accumulator/(timeSum*Simulation.space().D()*phase.atomCount());   //divide by time interval
            return temperature + flux;
        }
        /**
         * Implementation of integrator-meter abstract method
         * Adds collision virial (from potential) to accumulator
         */
        public void collisionAction(PotentialHard p) {
            accumulator += p.lastCollisionVirial();
        }
    }
}

