package com.srbenoit.modeling.cell;

import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import javax.swing.JFrame;
import javax.swing.SwingUtilities;
import com.srbenoit.geom.Point2;
import com.srbenoit.geom.Vector2;
import com.srbenoit.log.LoggedObject;
import com.srbenoit.modeling.grid.FastLennardJones;
import com.srbenoit.modeling.grid.FastSoftSphere;
import com.srbenoit.modeling.grid.Grid2D;
import com.srbenoit.modeling.grid.GridMember2Int;
import com.srbenoit.ui.UIUtilities;

/**
 * A cell simulation.
 */
public class Simulation extends LoggedObject implements Runnable {

    /** the singleton instance */
    private static final Simulation INSTANCE;

    /** percentage of border to add around cells when rendering */
    private static final double BORDER = 0.05;

    /** the cell radius, in microns */
    private static final double CELL_RADIUS = 5;

    /** the number of membrane elements per cell */
    private static final int MEMBRANE_PER_CELL = 200;

    /** the number of actin layers in each cell */
    private static final int ACTIN_LAYERS = 4;

    /** the number of frames to process between renderings */
    private static final double FRAMES_PER_RENDER = 100;

    /** the number of frames to process between renderings (negative to not output anything) */
    private static final double FRAMES_PER_OUTPUT = 10000;

    /** a soft-sphere force calculator for use within the simulation */
    public static final FastSoftSphere SS_FORCE;

    /** a soft-sphere force calculator for use within the simulation */
    public static final FastSoftSphere ACTIN_FORCE1;

    /** a soft-sphere force calculator for use within the simulation */
    public static final FastLennardJones ACTIN_FORCE2;

    /** a Lennard-Jones force calculator for use within the simulation */
    public static final FastLennardJones LJ_FORCE;

    /** hand of God force to add to emitters */
    public static final Vector2 HAND_OF_GOD;

    /** flag to control whether signals are emitted */
    public static final boolean EMIT_SIGNALS = true;

    /** Hooke's law spring constant for filaments */
    public static final double FILAMENT_SPRING = 1e-8;

    /** the panel that will render the cells */
    private CellPanel panel;

    /** the list of cells being simulated */
    private final List<Cell> cells;

    /** the list of signal emitters */
    private final List<Emitter> emitters;

    /** the list of fixed walls */
    private final List<Wall> walls;

    /** the bounds of the simulation region */
    private final Rectangle2D bounds;

    /** the minimum permitted separation between model elements */
    private final double avgSep;

    /** the minimum permitted separation between model elements */
    private final double minSep;

    /** the maximum permitted separation between model elements */
    private final double maxSep;

    /** the maximum permitted separation between model elements */
    private final double maxMove;

    /** the grid to do neighbor testing */
    private Grid2D grid;

    static {
        INSTANCE = new Simulation();
        INSTANCE.buildScene();

        SS_FORCE = new FastSoftSphere(1e-1);
        ACTIN_FORCE1 = new FastSoftSphere(1e-5);
        ACTIN_FORCE2 = new FastLennardJones(1e-12);
        LJ_FORCE = new FastLennardJones(1e-11);

// HAND_OF_GOD = new Vector2(-1e-11, 0); // Left-moving pusher
        HAND_OF_GOD = new Vector2(0, 6e-12); // upward-moving "chase me"
    }

    /**
     * Gets the singleton simulation instance.
     *
     * @return  the instance
     */
    public static Simulation getInstance() {

        return INSTANCE;
    }

    /**
     * Constructs a new <code>Simulation</code>.
     */
    private Simulation() {

        super();

        this.avgSep = 2 * Math.PI * CELL_RADIUS / MEMBRANE_PER_CELL;
        this.maxSep = 4 * this.avgSep / 3;
        this.minSep = this.maxSep / 2;
        this.maxMove = this.minSep / 50;

        this.cells = new ArrayList<Cell>(10);
        this.emitters = new ArrayList<Emitter>(10);
        this.walls = new ArrayList<Wall>(10);
        this.bounds = new Rectangle2D.Double();
    }

    /**
     * Builds the scene.
     */
    private void buildScene() {

        double neighborhood;
        int width;
        int height;

        // We set the neighborhood to twice the maximum separation size, for the grid
        neighborhood = 2 * this.maxSep;

        this.cells.add(new Cell(new Point2(-2, 0), CELL_RADIUS, MEMBRANE_PER_CELL, ACTIN_LAYERS,
                ACTIN_LAYERS * this.avgSep, this.avgSep, this.maxMove));
        this.emitters.add(new Emitter(7, 0, this.maxMove * 0.1,
                EMIT_SIGNALS ? (10 * MEMBRANE_PER_CELL) : 0, this.minSep, 1,
                (int) ((2 * Math.PI / this.minSep) + 0.5)));
        this.walls.add(new Wall(2 * CELL_RADIUS, -1.5 * CELL_RADIUS, -2 * CELL_RADIUS,
                -1.5 * CELL_RADIUS, this.avgSep, true));
        this.walls.add(new Wall(-2 * CELL_RADIUS, -1.5 * CELL_RADIUS, -2 * CELL_RADIUS,
                3 * CELL_RADIUS, this.avgSep, true));
        this.walls.add(new Wall(-2 * CELL_RADIUS, 3 * CELL_RADIUS, 2 * CELL_RADIUS,
                3 * CELL_RADIUS, this.avgSep, true));
        this.walls.add(new Wall(2 * CELL_RADIUS, -1.5 * CELL_RADIUS, 2 * CELL_RADIUS,
                3 * CELL_RADIUS, this.avgSep, true));

        computeBounds(this.bounds, this.cells, this.emitters);

        width = (int) (this.bounds.getWidth() / neighborhood) + 1;
        height = (int) (this.bounds.getHeight() / neighborhood) + 1;
        this.grid = new Grid2D(this.bounds.getX(), this.bounds.getY(), neighborhood, width,
                height);

        for (Cell cell : this.cells) {
            cell.addToGrid(this.grid);
        }

        for (Emitter emitter : this.emitters) {
            emitter.addToGrid(this.grid);
        }

        for (Wall wall : this.walls) {
            wall.addToGrid(this.grid);
        }
    }

    /**
     * Get the maximum separation between model elements.
     *
     * @return  the maximum separation
     */
    public double getMaxSep() {

        return this.maxSep;
    }

    /**
     * Computes the bounding rectangle that contains all cell elements and emitters, with a border.
     * We assume actin is contained in the cell, and if the rectangle contains the cell, it
     * automatically contains the actin.
     *
     * @param  bounds    the rectangle to populate with bounds
     * @param  cells     the list of cells
     * @param  emitters  the list of emitters
     */
    private void computeBounds(final Rectangle2D bounds, final List<Cell> cells,
        final List<Emitter> emitters) {

        MembraneElement mem;

        if (emitters.size() > 0) {
            bounds.setFrame(emitters.get(0).getPosX(), emitters.get(0).getPosY(), 0, 0);
        } else if (cells.size() > 0) {
            bounds.setFrame(cells.get(0).getMembrane().getPosX(),
                cells.get(0).getMembrane().getPosY(), 0, 0);
        }

        for (Cell cell : cells) {
            mem = cell.getMembrane();

            for (;;) {
                bounds.add(mem.getPosX(), mem.getPosY());
                mem = mem.getNext();

                if (mem == cell.getMembrane()) {
                    break;
                }
            }
        }

        for (Emitter emitter : emitters) {
            bounds.add(emitter.getPosX(), emitter.getPosY());

            for (int i = 0; i < emitter.getNumFixed(); i++) {
                bounds.add(emitter.getFixed(i).getPosX(), emitter.getFixed(i).getPosY());
            }
        }

        for (Wall wall : walls) {

            for (int i = 0; i < wall.getNumFixed(); i++) {
                bounds.add(wall.getFixed(i).getPosX(), wall.getFixed(i).getPosY());
            }
        }

        bounds.setFrame(bounds.getX() - (bounds.getWidth() * BORDER),
            bounds.getY() - (bounds.getHeight() * BORDER),
            bounds.getWidth() + (bounds.getWidth() * 2 * BORDER),
            bounds.getHeight() + (bounds.getHeight() * 2 * BORDER));
    }

    /**
     * Constructs the user interface in the AWT thread.
     */
    public void run() {

        JFrame frame;

        frame = new JFrame("Cell Simulation");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        this.panel = new CellPanel(500, 550);
        frame.setContentPane(this.panel);
        frame.pack();
        UIUtilities.positionFrame(frame, 0.5, 0.5);
        frame.setVisible(true);
    }

    /**
     * Runs the simulation after the user interface is created.
     */
    public void go() {

        MembraneElement elem;
        int count;
        ActinFilament actin;
        int frameNum;
        double max;
        int outputFrame;

        // Set an external force on emitters if we want them to move
        for (Emitter emitter : this.emitters) {
            emitter.getHandOfGod().setVec(HAND_OF_GOD);
        }

        outputFrame = 0;

        for (frameNum = 0; frameNum < Integer.MAX_VALUE; frameNum++) {

            // Do things that change the positions of objects in the grid...
            evolveSignals();
            restructureActin();
            restructureMembrane();

            // Recompute neighbor relationships since nodes may have been added/removed
            for (Cell cell : this.cells) {
                elem = cell.getMembrane();

                do {
                    this.grid.getNeighborsOf(elem);

                    count = elem.getNumActin();

                    for (int i = 0; i < count; i++) {
                        actin = elem.getActin(i);

                        while (actin != null) {
                            this.grid.getNeighborsOf(actin);
                            actin = actin.getDownstream();
                        }
                    }

                    elem = elem.getNext();
                } while (elem != cell.getMembrane());
            }

            // Do force-based forces and motions
            computeInnerForces();
            max = maxForce();
// LOG.fine("Max: " + max);
            computeInteractionForces(max);
            reactToForces(max);

            // Recompute membrane element normal vectors
            recomputeNormals();

            // Act on adjacencies in the grid, but don't move anything
            detectSignalsAtMembrane();

            if ((frameNum % FRAMES_PER_RENDER) == 0) {
                this.panel.render(this.bounds, this.cells, this.emitters, this.walls);

                if ((FRAMES_PER_OUTPUT > 0) && ((frameNum % FRAMES_PER_OUTPUT) == 0)) {
                    outputFrame++;
                    this.panel.exportFrame(outputFrame);
                }
            }
        }
    }

    /**
     * Emits new signals, ages and deletes expired signals, and allows them to diffuse.
     */
    private void evolveSignals() {

        for (Emitter emitter : this.emitters) {
            emitter.emit(5);
            emitter.age();
            emitter.diffuse(this.bounds);
        }
    }

    /**
     * Scan for signals in contact with membrane, and activate the corresponding membrane,
     * consuming the signal in the process.
     *
     * @param  iter  an iterator that can be used to walk the grid
     */
    private void detectSignalsAtMembrane() {

        MembraneElement elem;
        GridMember2Int nbr;
        Signal sig;
        int count;
        double distSq;
        double rangeSq;

        rangeSq = this.maxSep * this.maxSep;

        for (Cell cell : this.cells) {
            elem = cell.getMembrane();

            do {

                count = elem.getNumNeighbors();

                for (int i = 0; i < count; i++) {
                    nbr = elem.getNeighbor(i);

                    if (nbr instanceof Signal) {
                        sig = (Signal) nbr;

                        if (sig.isLiving()) {
                            distSq = sig.distSquared(elem);

                            if (distSq < rangeSq) {
                                elem.activate(sig.getActivation());
                                sig.die();
                            }
                        }
                    }
                }

                elem = elem.getNext();
            } while (elem != cell.getMembrane());
        }
    }

    /**
     * React to signal levels in the membrane to cause changes in actin polymerization.
     */
    private void restructureActin() {

        for (Cell cell : this.cells) {
            cell.restructureActin();
        }
    }

    /**
     * Adjust the membrane as needed to ensure adjacent elements are neither too close together nor
     * too far apart.
     */
    private void restructureMembrane() {

        for (Cell cell : this.cells) {
            cell.restructureMembrane(this.minSep, this.maxSep);
        }
    }

    /**
     * Compute the force on every element in the model due to internal factors.
     */
    private void computeInnerForces() {

        for (Cell cell : this.cells) {
            cell.computeInnerForces();
        }

        for (Emitter emitter : this.emitters) {
            emitter.computeInnerForces();
        }
    }

    /**
     * Computes the maximum force anywhere in the system.
     *
     * @return  the maximum force
     */
    private double maxForce() {

        double max;
        double force;

        max = 0;

        for (Cell cell : this.cells) {
            force = cell.maxForce();

            if (force > max) {
                max = force;
            }
        }

        for (Emitter emitter : this.emitters) {
            force = emitter.getForce().length();

            if (force > max) {
                max = force;
            }
        }

        return max;
    }

    /**
     * Compute the force on every element in the model due to interactions.
     *
     * @param  maxForce  the maximum force from non-interaction sources
     */
    private void computeInteractionForces(final double maxForce) {

        for (Cell cell : this.cells) {
            cell.computeInteractionForces(maxForce);
        }

        for (Emitter emitter : this.emitters) {
            emitter.computeInteractionForces(maxForce);
        }
    }

    /**
     * Move elements in the direction of their forces.
     *
     * @param  maxForce  the maximum force in the system
     */
    private void reactToForces(final double maxForce) {

        double mobility;

        if (maxForce > Double.MIN_NORMAL) {
            mobility = this.maxMove / maxForce;

            for (Cell cell : this.cells) {
                cell.reactToForces(mobility);
            }

            for (Emitter emitter : this.emitters) {
                emitter.reactToForces(mobility);
            }
        }
    }

    /**
     * Computes the normal vector at each membrane element based on its neighbors.
     */
    private void recomputeNormals() {

        for (Cell cell : this.cells) {
            cell.recomputeNormals();
        }
    }

    /**
     * Main method to run the simulation.
     *
     * @param  args  command-line arguments
     */
    public static void main(final String... args) {

        Simulation sim;

        sim = Simulation.getInstance();

        try {
            SwingUtilities.invokeAndWait(sim);
            sim.go();
        } catch (Exception ex) {
            LOG.log(Level.SEVERE, "Exception in simulation", ex);
        }
    }
}
