package com.srbenoit.modeling.grid;

import java.util.logging.Level;
import com.srbenoit.geom.Vector2;

/**
 * A base class for objects that may be contained in a 2-dimensional grid. Each object has an X and
 * Y position, a radius, and a grid address.
 */
public class DynamicGridMember2D extends PointGridMember2 {

    /** a type so we can search for neighbors of a single type, ignoring others */
    private final EnumElementType type;

    /** the radius of the object */
    private double radius;

    /** the ID of the structure this member belongs to */
    private final int structId;

    /** the object velocity (r') */
    private final Vector2 vel;

    /** the object acceleration (r'') */
    private final Vector2 accel;

    /** the force on the object */
    private final Vector2 force;

    /** the object mass */
    private double mass;

    /**
     * Constructs a new <code>GridMember2D</code>.
     *
     * @param  xCoord       the X coordinate
     * @param  yCoord       the Y coordinate
     * @param  rad          the radius of the object
     * @param  theType      the element type
     * @param  structureId  the ID of the structure this member belongs to
     * @param  theMass      the mass of the object
     */
    public DynamicGridMember2D(final double xCoord, final double yCoord, final double rad,
        final EnumElementType theType, final int structureId, final double theMass) {

        super(xCoord, yCoord);

        this.radius = rad;

        this.vel = new Vector2();
        this.accel = new Vector2();
        this.force = new Vector2();

        this.type = theType;
        this.structId = structureId;
        this.mass = theMass;
    }

    /**
     * Sets the radius of the object.
     *
     * @param  rad  the radius
     */
    public void setRadius(final double rad) {

        this.radius = rad;
    }

    /**
     * Gets the radius of the object.
     *
     * @return  the radius
     */
    @Override public double getRadius() {

        return this.radius;
    }

    /**
     * Gets the member type.
     *
     * @return  the type
     */
    public EnumElementType getType() {

        return this.type;
    }

    /**
     * Gets this structure ID of this object.
     *
     * @return  the structure ID
     */
    public int getStructId() {

        return this.structId;
    }

    /**
     * Updates the object velocity.
     *
     * @param  newX  the new X component of velocity
     * @param  newY  the new Y component of velocity
     */
    public void setVel(final double newX, final double newY) {

        if (Double.isNaN(newX) || Double.isNaN(newY)) {
            LOG.log(Level.WARNING, "Attempt to set grid member velocity to NaN", new Exception());
            System.exit(1);
        }

        if (Double.isInfinite(newX) || Double.isInfinite(newX)) {
            LOG.log(Level.WARNING, "Attempt to set grid member velocity by Infinity",
                new Exception());
            System.exit(1);
        }

        this.vel.setVec(newX, newY);
    }

    /**
     * Adjusts the object velocity by a specified amount.
     *
     * @param  deltaX  the change in X component of velocity
     * @param  deltaY  the change in Y component of velocity
     */
    public void addVel(final double deltaX, final double deltaY) {

        if (Double.isNaN(deltaX) || Double.isNaN(deltaY)) {
            LOG.log(Level.WARNING, "Attempt to move grid member velocity by NaN", new Exception());
            System.exit(1);
        }

        if (Double.isInfinite(deltaX) || Double.isInfinite(deltaY)) {
            LOG.log(Level.WARNING, "Attempt to move grid member velocity by Infinity",
                new Exception());
            System.exit(1);
        }

        this.vel.addVec(deltaX, deltaY);
    }

    /**
     * Gets the current X component of the object velocity.
     *
     * @return  the X component
     */
    public double getXVel() {

        return this.vel.getVecX();
    }

    /**
     * Gets the current Y component of the object velocity.
     *
     * @return  the Y component
     */
    public double getYVel() {

        return this.vel.getVecY();
    }

    /**
     * Gets the speed of the particle.
     *
     * @return  the speed
     */
    public double getSpeed() {

        return this.vel.length();
    }

    /**
     * Gets the acceleration of the particle.
     *
     * @return  the acceleration
     */
    public double getAccel() {

        return this.accel.length();
    }

    /**
     * Gets the current X component of the object force.
     *
     * @return  the X component
     */
    public double getXForce() {

        return this.force.getVecX();
    }

    /**
     * Gets the current Y component of the object force.
     *
     * @return  the Y component
     */
    public double getYForce() {

        return this.force.getVecY();
    }

    /**
     * Gets the magnitude of the object force.
     *
     * @return  the magnitude of the force
     */
    public double getForceMag() {

        return this.force.length();
    }

    /**
     * Sets the object force.
     *
     * @param  newX  the new X component
     * @param  newY  the new Y component
     */
    public void setForce(final double newX, final double newY) {

        if (Double.isNaN(newX) || Double.isNaN(newY)) {
            LOG.log(Level.WARNING, "Attempt to set grid member force to NaN", new Exception());
            System.exit(1);
        }

        if (Double.isInfinite(newX) || Double.isInfinite(newY)) {
            LOG.log(Level.WARNING, "Attempt to set grid member force to Infinity",
                new Exception());
            System.exit(1);
        }

        this.force.setVec(newX, newY);
    }

    /**
     * Adds velocity components to the current force.
     *
     * @param  deltaX  the change in X component
     * @param  deltaY  the change in Y component
     */
    public void addForce(final double deltaX, final double deltaY) {

        if (Double.isNaN(deltaX) || Double.isNaN(deltaY)) {
            LOG.log(Level.WARNING, "Attempt to adjust grid member force by NaN", new Exception());
            System.exit(1);
        }

        if (Double.isInfinite(deltaX) || Double.isInfinite(deltaY)) {
            LOG.log(Level.WARNING, "Attempt to adjust grid member force by Infinity",
                new Exception());
            System.exit(1);
        }

        this.force.addVec(deltaX, deltaY);
    }

    /**
     * Clears any existing forces and computes the forces on the element due to interactions with
     * outside elements. Subclasses should override. This does not include the internal forces of
     * the structure to which the element belongs, which should be done after this method is
     * called.
     */
    public void interactionForce() {

        this.vel.setVec(0, 0);
    }

    /**
     * Performs the first step in Velocity Verlet integration. Here, we compute new positions based
     * on velocities and accelerations computed in the last step.
     *
     * <pre>
     *   x(t + dt) = x(t) + v(t) dt + (1/2) a(t) dt^2
     * </pre>
     *
     * @param  deltaT  the time step
     */
    public void predict(final double deltaT) {

        double dt2over2;
        double deltaX;
        double deltaY;

        dt2over2 = deltaT * deltaT / 2;

        deltaX = (deltaT * this.vel.getVecX()) + (dt2over2 * this.accel.getVecX());
        deltaY = (deltaT * this.vel.getVecY()) + (dt2over2 * this.accel.getVecY());

        move(deltaX, deltaY);
    }

    /**
     * Performs the second step in Velocity Verlet integration, to be done after forces are
     * computed.
     *
     * <pre>
     *   v(t + dt) = v(t) + (1/2)(a(t) + f/m) dt
     *   a(t + dt) = f/m
     * </pre>
     *
     * @param  deltaT  the time step
     */
    public void correct(final double deltaT) {

        double realAccelX;
        double realAccelY;

        realAccelX = this.force.getVecX() / this.mass;
        realAccelY = this.force.getVecY() / this.mass;

        // the 0.1 in the next should be 0.5 - testing damping
        addVel(0.1 * (this.accel.getVecX() + realAccelX) * deltaT,
            0.1 * (this.accel.getVecY() + realAccelY) * deltaT);

        this.accel.setVec(realAccelX, realAccelY);
    }
}
