package com.srbenoit.modeling.cell;

import java.awt.geom.Rectangle2D;
import java.util.Random;
import com.srbenoit.geom.Vector2;
import com.srbenoit.modeling.grid.Grid2D;
import com.srbenoit.modeling.grid.PointGridMember2;

/**
 * An emitter of diffusing signals.
 */
public class Emitter extends PointGridMember2 {

    /** the fixed elements */
    private FixedElement[] fixed;

    /** the activation level of signals this emitter generates */
    private double activation;

    /** the lifetime of signals this emitter generates */
    private int lifetime;

    /** the diffusion step length */
    private final double stepLength;

    /** the number of signals from this emitter that are currently active */
    private int numSignals;

    /** the active signals that have been emitted from this emitter. */
    private Signal[] signals;

    /** random generator for diffusion */
    private final Random rnd;

    /** the force on the element */
    private final Vector2 force;

    /** a constant external force on the element */
    private final Vector2 handOfGodForce;

    /**
     * Constructs a new <code>Emitter</code>.
     *
     * @param  xCoord       the X coordinate of the emitter
     * @param  yCoord       the Y coordinate of the emitter
     * @param  actLevel     the activation level of signals this emitter will generate
     * @param  life         the lifetime of the emitted signals
     * @param  diffStep     the diffusion step length (for random walk)
     * @param  radius       the ring radius
     * @param  numElements  the number of elements from which to build the ring
     */
    public Emitter(final double xCoord, final double yCoord, final double actLevel, final int life,
        final double diffStep, final double radius, final int numElements) {

        super(xCoord, yCoord);

        double angle;
        double xPos;
        double yPos;

        this.activation = actLevel;
        this.lifetime = life;
        this.stepLength = diffStep;

        this.signals = new Signal[100];
        this.numSignals = 0;

        this.rnd = new Random();
        this.force = new Vector2();
        this.handOfGodForce = new Vector2();

        this.fixed = new FixedElement[numElements];

        for (int i = 0; i < numElements; i++) {
            angle = 2 * Math.PI * i / numElements;
            xPos = xCoord + (radius * Math.cos(angle));
            yPos = yCoord + (radius * Math.sin(angle));

            this.fixed[i] = new FixedElement(xPos, yPos);
        }
    }

    /**
     * Gets the force vector for the filament.
     *
     * @return  the force vector
     */
    public Vector2 getForce() {

        return this.force;
    }

    /**
     * Gets the external "hand of God" force vector for the filament.
     *
     * @return  the hand of God force vector
     */
    public Vector2 getHandOfGod() {

        return this.handOfGodForce;
    }

    /**
     * Moves the emitter and its associated fixed elements. Active signals are not affected.
     *
     * @param  deltaX  the X component by which to move the emitter
     * @param  deltaY  the Y component by which to move the emitter
     */
    @Override public void move(final double deltaX, final double deltaY) {

        super.move(deltaX, deltaY);

        for (int i = 0; i < this.fixed.length; i++) {
            this.fixed[i].move(deltaX, deltaY);
        }
    }

    /**
     * Gets the number of active signals emitted by the emitter.
     *
     * @return  the number of signals
     */
    public int getNumSignals() {

        return this.numSignals;
    }

    /**
     * Gets a particular signal.
     *
     * @param   index  the index of the signal
     * @return  the signal
     */
    public Signal getSignal(final int index) {

        return this.signals[index];
    }

    /**
     * Gets the number of fixed elements.
     *
     * @return  the number of fixed elements
     */
    public int getNumFixed() {

        return this.fixed.length;
    }

    /**
     * Gets a particular fixed element.
     *
     * @param   index  the index of the fixed element
     * @return  the fixed element
     */
    public FixedElement getFixed(final int index) {

        return this.fixed[index];
    }

    /**
     * Emits new signals.
     *
     * @param  numToEmit  the number of signals to emit.
     */
    public void emit(final int numToEmit) {

        Signal[] newArray;
        Signal sig;

        if (this.signals.length < (this.numSignals + numToEmit)) {
            newArray = new Signal[this.signals.length + 100];
            System.arraycopy(this.signals, 0, newArray, 0, this.numSignals);
            this.signals = newArray;
        }

        for (int i = 0; i < numToEmit; i++) {
            sig = new Signal(getPosX(), getPosY(), this.lifetime, this.activation);

            if (getGrid() != null) {
                sig.installInGrid(getGrid());
            }

            this.signals[this.numSignals] = sig;
            this.numSignals++;
        }
    }

    /**
     * Ages all active signals and culls those that have expired.
     */
    public void age() {

        Signal sig;
        int remain;

        for (int i = 0; i < this.numSignals;) {

            sig = this.signals[i];
            remain = sig.age();

            if (remain <= 0) {

                if (sig.getGrid() != null) {
                    sig.removeFromGrid();
                }

                this.signals[i] = this.signals[this.numSignals - 1];
                this.numSignals--;
            } else {
                i++;
            }
        }
    }

    /**
     * Allows all active signals to diffuse (this does not age them), and culls any that leave a
     * bounding rectangle.
     *
     * @param  bounds  the bounding rectangle
     */
    public void diffuse(final Rectangle2D bounds) {

        Signal sig;
        double angle;

        for (int i = 0; i < this.numSignals;) {

            angle = this.rnd.nextDouble() * 2 * Math.PI;

            sig = this.signals[i];
            sig.move(this.stepLength * Math.cos(angle), this.stepLength * Math.sin(angle));

            if (bounds.contains(sig.getPosX(), sig.getPosY())) {
                i++;
            } else {
                this.signals[i] = this.signals[this.numSignals - 1];
                this.numSignals--;
            }
        }
    }

    /**
     * Adds the emitter and any active signals to a grid, and stores the grid so emitted signals
     * can be automatically added to the grid and expiring signals can be removed.
     *
     * @param  grid  the grid
     */
    public void addToGrid(final Grid2D grid) {

        installInGrid(grid);

        for (int i = 0; i < this.numSignals;) {
            this.signals[i].installInGrid(grid);
        }

        for (FixedElement elem : this.fixed) {
            elem.installInGrid(grid);
        }
    }

    /**
     * Compute the force on every element in the model.
     */
    public void computeInnerForces() {

        for (FixedElement elem : this.fixed) {
            elem.getForce().setVec(this.handOfGodForce);
        }
    }

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

        for (FixedElement elem : this.fixed) {
            elem.computeInteractionForce(maxForce);
        }

        // Sum the forces and distribute the average force to all fixed elements
        this.force.setVec(0,0);
        for (FixedElement elem : this.fixed) {
            this.force.addVec(elem.getForce());
        }

        this.force.scaleVec(1.0 / this.fixed.length);

        for (FixedElement elem : this.fixed) {
            elem.getForce().setVec(this.force);
        }
    }

    /**
     * Move elements in the direction of their forces.
     *
     * @param  mobility  the mobility used to convert force into motion
     */
    public void reactToForces(final double mobility) {

        move(this.force.getVecX() * mobility, this.force.getVecY() * mobility);
    }
}
