package com.srbenoit.modeling.grid;

import java.util.Random;
import javax.swing.JFrame;
import com.srbenoit.log.LoggedObject;

/**
 * Run a time evolution of a collection of granules.
 */
public class Granular2D extends LoggedObject {

    /** the number of frames to compute between renderings */
    public static final int FRAMES_PER_RENDER = 200;

    /** the number of frames to compute between exported frames (0 to skip exports) */
    public static final int FRAMES_PER_EXPORT = 500000; // must be nultiple of FRAMES_PER_RENDER

    /** the frame showing the evolution */
    private JFrame frame;

    /** the panel showing the evolution */
    private GridPanel2D panel;

    /** a grid used to accelerate neighbor testing */
    private final transient Grid2D grid;

    /**
     * Constructs a new <code>Granular2D</code>.
     *
     * @param  width   the number of granules to place in each row
     * @param  height  the number of rows of granules
     */
    public Granular2D(final int width, final int height) {

        Random rnd;
        LinkedListGridMember2D prior;
        LinkedListGridMember2D mem;
        Granule gran;
        double angle;

        rnd = new Random();

        // Spheres are radius 1, diameter 2.  We will place the array of spheres on a grid with
        // spacing 4 between centers, and a boundary of fixed spheres outside that.  The boundary
        // runs from 0 to 4 * (width + 1) in the X direction, and from 0 to 4 * (height + 1) in
        // the Y direction.  The interaction distance of spheres is 2.5 (r1+r2) = 5, so we use a
        // grid of cell size 6

        int xSize;
        int ySize;

        xSize = 4 * (width + 1);
        ySize = 4 * (height + 1);

        this.grid = new Grid2D(-2, -2, 6, (xSize + 6) / 6, (ySize + 6) / 6);

        // Build the fixed walls
        prior = new LinkedListGridMember2D(0, 0, 1, EnumElementType.FIXED, 0, 1);
        prior.installInGrid(this.grid);

        for (int x = 2; x < xSize; x += 2) {
            mem = new LinkedListGridMember2D(x, 0, 1, EnumElementType.FIXED, 0, 1);
            mem.addAfter(prior);
            mem.installInGrid(this.grid);
            prior = mem;
        }

        for (int y = 0; y < ySize; y += 2) {
            mem = new LinkedListGridMember2D(xSize, y, 1, EnumElementType.FIXED, 0, 1);
            mem.addAfter(prior);
            mem.installInGrid(this.grid);
            prior = mem;
        }

        for (int x = xSize; x > 0; x -= 2) {
            mem = new LinkedListGridMember2D(x, ySize, 1, EnumElementType.FIXED, 0, 1);
            mem.addAfter(prior);
            mem.installInGrid(this.grid);
            prior = mem;
        }

        for (int y = ySize; y > 0; y -= 2) {
            mem = new LinkedListGridMember2D(0, y, 1, EnumElementType.FIXED, 0, 1);
            mem.addAfter(prior);
            mem.installInGrid(this.grid);
            prior = mem;
        }

        // Build the granules
        for (int i = 0; i < width; i++) {

            for (int j = 0; j < height; j++) {
                gran = new Granule(4 + (i * 4), 4 + (j * 4), 1, 1, 1);
                angle = Math.PI * 2 * rnd.nextDouble();
                gran.setVel(Math.cos(angle), Math.sin(angle));
                gran.installInGrid(this.grid);
            }
        }

        buildFrame();
    }

    /**
     * Constructs the frame and panel
     */
    private void buildFrame() {

        this.frame = new JFrame("Granular Dynamics Simulation");
        this.frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        this.panel = new GridPanel2D(600, 600, this.grid);
        this.frame.setContentPane(this.panel);
        this.frame.pack();
        this.frame.setVisible(true);
    }

    /**
     * Runs the evolution simulation.
     */
    public void evolve() {

        double maxMove;
        long frameCount;
        int frameNum;
        GridIterator2D iter;
        DynamicGridMember2D elem;
        long start;
        long end;
        double speed;
        double maxSpeed;
        double accel;
        double maxAccel;
        double force;
        double maxForce;
        double timestep;
        double time1;
        double time2;

        // Now to do the evolution
        iter = new GridIterator2D(this.grid);
        maxMove = 1.0 / 200;

        start = System.currentTimeMillis() - 1;
        frameCount = 0;
        frameNum = 1;

        while (this.frame.isVisible()) {

            // Since our max move (set above) is 1/20 of a grid cell size, we scan for neighbors in
            // a radius of 3 times our object's radius, then those neighbor relations are good for
            // at least 10 loops ("good" means we won't miss any neighbors; we may get extras)
            if ((frameCount % 20) == 0) {

                // recompuate neighbor relationships for all but fixed and moving elements
                iter.reset();

                while (iter.hasNext()) {
                    elem = (DynamicGridMember2D)iter.next();

                    if (elem.getType() != EnumElementType.FIXED) {
                        this.grid.getNeighborsOf(elem);
                    }
                }
            }

            // Scan for the maximum velocity and acceleration to choose a time step
            iter.reset();

            maxSpeed = 0;
            maxAccel = 0;

            while (iter.hasNext()) {
                elem = (DynamicGridMember2D)iter.next();
                speed = elem.getSpeed();
                accel = elem.getAccel();

                if (speed > maxSpeed) {
                    maxSpeed = speed;
                }

                if (accel > maxAccel) {
                    maxAccel = accel;
                }
            }

            if (maxSpeed == 0) {
                timestep = 1e-10;
            } else {
                time1 = maxMove / maxSpeed;
                time2 = Math.sqrt(2 * maxMove / maxAccel);
                timestep = (time1 < time2) ? time1 : time2;
            }

// LOG.warning("Max speed = " + maxSpeed + ", time step: " + timestep);

            // Run the predictor step in the Gear algorithm
            iter.reset();

            while (iter.hasNext()) {
                elem = (DynamicGridMember2D)iter.next();
                elem.predict(timestep);
            }

            // Compute all element  forces
            iter.reset();

            while (iter.hasNext()) {
                elem = (DynamicGridMember2D)iter.next();
                elem.interactionForce();
            }

            // Diagnostic output: print the largest force computed
            iter.reset();

            maxForce = 0;

            while (iter.hasNext()) {
                elem = (DynamicGridMember2D)iter.next();
                force = elem.getForceMag();

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

// LOG.info("LARGEST FORCE: " + maxForce);

            // Run the corrector step in the Gear algorithm
            iter.reset();

            while (iter.hasNext()) {
                elem = (DynamicGridMember2D)iter.next();
                elem.correct(timestep);
            }

            if ((frameCount % FRAMES_PER_RENDER) == 0) {
                end = System.currentTimeMillis();
                this.panel.update(frameCount * 1000.0 / (end - start));

                if ((FRAMES_PER_EXPORT != 0) && ((frameCount % FRAMES_PER_EXPORT) == 0)) {
                    this.panel.exportFrame(frameNum);
                    frameNum++;
                }
            }

            frameCount++;
        }
    }

    /**
     * Main method to run the test.
     *
     * @param  args  Command-line arguments.
     */
    public static void main(final String[] args) {

        new Granular2D(20, 20).evolve();
    }
}
