package com.srbenoit.modeling.cell;

import java.util.ArrayList;
import java.util.List;
import com.srbenoit.geom.Vector2;
import com.srbenoit.modeling.grid.BasedVectorGridMember2;
import com.srbenoit.modeling.grid.GridMember2Int;

/**
 * An element of a membrane, with the corresponding actin filament. Elements are maintained in a
 * doubly linked list.
 *
 * <p>The equilibrium length of the link to the next element is maintained here, and can adjust
 * slowly based on tension. If this value dips below a minimum, the element will be merged with its
 * neighbor, and it it exceeds a maximum, a new element is inserted between this element and its
 * neighbor. This equilibrium length also applies to the separation between the actin filament of
 * this element and that of the next element, although with a different spring constant.
 */
public class MembraneElement extends BasedVectorGridMember2 {

    /** the maximum activation level */
    private static final int MAX_ACTIVATION = 4;

    /** elastic modulus (microgram micron^2 / microsecond^2) */
    private static final double ELASTICMOD = 7e-11;

    /** tension (microgram / microsecond^2) */
    private static final double TENSION = 3e-8;

    /** cytoplasm bulk modulus (microgram / (micron microsecond^2)) */
    private static final double BULKMOD = 1e-4;

    /** the cell to which this membrane belongs */
    private Cell cell;

    /** the next membrane unit (counterclockwise) */
    private MembraneElement flink;

    /** the prior membrane unit (clockwise) */
    private MembraneElement blink;

    /** the actin filaments attached to this element */
    private final List<ActinFilament> actin;

    /** the activation level of the membrane element */
    private double activation;

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

    /** working tuple */
    private final Vector2 e0;

    /** working tuple */
    private final Vector2 ep1;

    /** working tuple */
    private final Vector2 em1;

    /** working tuple */
    private final Vector2 em2;

    /** working tuple */
    private final Vector2 em1p1;

    /** working tuple */
    private final Vector2 term1;

    /** working tuple */
    private final Vector2 term2;

    /** working tuple */
    private final Vector2 term3;

    /**
     * Constructs a new <code>MembraneElement</code>.
     *
     * @param  theCell      the cell to which this membrane belongs
     * @param  xCoord       the X coordinate of the element
     * @param  yCoord       the Y coordinate of the element
     * @param  angle        the direction of the outward-pointing normal vector, used to construct
     *                      the actin filament attached to the membrane element
     * @param  numActinSeg  the number of actin segments to attach to the element
     * @param  actinRad     the radius of each actin segment
     */
    public MembraneElement(final Cell theCell, final double xCoord, final double yCoord,
        final double angle, final int numActinSeg, final double actinRad) {

        this(theCell, xCoord, yCoord, Math.cos(angle), Math.sin(angle), numActinSeg, actinRad);
    }

    /**
     * Constructs a new <code>MembraneElement</code>.
     *
     * @param  theCell      the cell to which this membrane belongs
     * @param  xCoord       the X coordinate of the element
     * @param  yCoord       the Y coordinate of the element
     * @param  normalX      the X component of the outward-pointing normal vector
     * @param  normalY      the Y component of the outward-pointing normal vector
     * @param  numActinSeg  the number of actin segments to attach to the element
     * @param  actinRad     the radius of each actin segment
     */
    public MembraneElement(final Cell theCell, final double xCoord, final double yCoord,
        final double normalX, final double normalY, final int numActinSeg, final double actinRad) {

        super(xCoord, yCoord, normalX, normalY);

        double len;
        double deltaX;
        double deltaY;
        double xPos;
        double yPos;
        ActinFilament prior;
        ActinFilament next;

        this.cell = theCell;
        this.flink = this;
        this.blink = this;
        this.activation = 0;
        this.force = new Vector2();

        this.actin = new ArrayList<ActinFilament>(2);
        len = Simulation.getInstance().getMaxSep() + actinRad;

        if (numActinSeg > 0) {
            xPos = xCoord - len * normalX;
            yPos = yCoord - len * normalY;
            deltaX = -2 * actinRad * normalX;
            deltaY = -2 * actinRad * normalY;

            prior = new ActinFilament(xPos, yPos, len, actinRad, null);
            this.actin.add(prior);

            for (int i = 1; i < numActinSeg; i++) {
                xPos += deltaX;
                yPos += deltaY;

                next = new ActinFilament(xPos, yPos, 2 * actinRad, actinRad, prior);
                prior = next;
            }
        }

        this.e0 = new Vector2();
        this.ep1 = new Vector2();
        this.em1 = new Vector2();
        this.em2 = new Vector2();
        this.em1p1 = new Vector2();
        this.term1 = new Vector2();
        this.term2 = new Vector2();
        this.term3 = new Vector2();
    }

    /**
     * Adds this member in the linked list before a given element.
     *
     * @param  elem  the element before which to add this element
     */
    public void addBefore(final MembraneElement elem) {

        this.setPrior(elem.getPrior());
        this.setNext(elem);
        elem.getPrior().setNext(this);
        elem.setPrior(this);
    }

    /**
     * Adds this member in the linked list after a given element.
     *
     * @param  elem  the element after which to add this element
     */
    public void addAfter(final MembraneElement elem) {

        setNext(elem.getNext());
        setPrior(elem);
        elem.getNext().setPrior(this);
        elem.setNext(this);
    }

    /**
     * Removes this element from the list in which it is installed. NOTE: the Cell object keeps a
     * reference to an element in the membrane list to allow iteration. If you remove that element,
     * that reference must be set to a valid remaining element.
     */
    public void remove() {

        this.getNext().setPrior(getPrior());
        this.getPrior().setNext(getNext());
        setNext(this);
        setPrior(this);
    }

    /**
     * Gets the next item in the linked list.
     *
     * @return  the next item
     */
    public MembraneElement getNext() {

        return this.flink;
    }

    /**
     * Gets the prior item in the linked list.
     *
     * @return  the prior item
     */
    public MembraneElement getPrior() {

        return this.blink;
    }

    /**
     * Sets the next item in the linked list.
     *
     * @param  next  the next item
     */
    public void setNext(final MembraneElement next) {

        this.flink = next;
    }

    /**
     * Sets the prior item in the linked list.
     *
     * @param  prior  the prior item
     */
    public void setPrior(final MembraneElement prior) {

        this.blink = prior;
    }

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

        return this.force;
    }

    /**
     * Adds an actin filament associated with this element.
     *
     * @param  filament  the actin filament
     */
    public void addActin(final ActinFilament filament) {

        this.actin.add(filament);
    }

    /**
     * Removes an actin filament associated with this element.
     *
     * @param  filament  the actin filament
     */
    public void removeActin(final ActinFilament filament) {

        this.actin.remove(filament);
    }

    /**
     * Replaces an actin filament in the list with a new filament.
     *
     * @param  index     the index of the filament to replace
     * @param  filament  the new actin filament
     */
    public void replaceActin(final int index, final ActinFilament filament) {

        this.actin.set(index, filament);
    }

    /**
     * Gets the number of actin filaments associated with this element.
     *
     * @return  the number of actin filaments
     */
    public int getNumActin() {

        return this.actin.size();
    }

    /**
     * Gets an actin filament associated with this element.
     *
     * @param   index  the index of the actin filament to get
     * @return  the actin filament
     */
    public ActinFilament getActin(final int index) {

        return this.actin.get(index);
    }

    /**
     * Process activation by a signal.
     *
     * @param  actLevel  the activation level carried by the signal
     */
    public void activate(final double actLevel) {

        this.activation += actLevel;

        if (this.activation > MAX_ACTIVATION) {
            this.activation = MAX_ACTIVATION;
        } else if (this.activation < -MAX_ACTIVATION) {
            this.activation = -MAX_ACTIVATION;
        }
    }

    /**
     * Gets the current activation level of the element.
     *
     * @return  the current activation level
     */
    public double getActivation() {

        return this.activation;
    }

    /**
     * Adjusts the current activation level;
     *
     * @param  delta  the amount by which to adjust the current activation level
     */
    public void adjustActivation(final double delta) {

        this.activation += delta;
    }

    /**
     * Computes the force on the element.
     */
    public void computeInnerForce() {

        double eqVolume;
        double volume;
        MembraneElement prior1;
        MembraneElement prior2;
        MembraneElement next1;
        MembraneElement next2;
        double normE0;
        double normEm1;
        double dot;
        double scale;
        double dist;
        double delta;

        eqVolume = this.cell.getEquilibriumVolume();
        volume = this.cell.getCurrentVolume();

        // Get the surrounding elements in the membrane
        prior1 = this.getPrior();
        prior2 = prior1.getPrior();
        next1 = this.getNext();
        next2 = next1.getNext();

        // Build relative vectors (cache these?)
        this.e0.vectorBetween(this, next1);
        this.ep1.vectorBetween(next1, next2);
        this.em1.vectorBetween(prior1, this);
        this.em2.vectorBetween(prior2, prior1);
        this.em1p1.vectorBetween(prior1, next1);
        normE0 = this.e0.length();
        normEm1 = this.em1.length();
        this.e0.normalize();
        this.ep1.normalize();
        this.em1.normalize();
        this.em2.normalize();

        // Pressure force
        scale = BULKMOD * this.cell.getThickness() * (eqVolume - volume) / (2 * eqVolume);
        this.force.addVec(scale * this.em1p1.getVecY(), -scale * this.em1p1.getVecX());

        // Tension force
        this.force.addVec(-TENSION * this.cell.getThickness()
            * ((this.em1.getVecX() * normEm1) - (this.e0.getVecX() * normE0)),
            -TENSION * this.cell.getThickness()
            * ((this.em1.getVecY() * normEm1) - (this.e0.getVecY() * normE0)));

        // Curvature force
        if ((normE0 > 0) && (normEm1 > 0)) {

            dot = this.e0.dot(this.ep1);
            this.term1.setVec(this.e0);
            this.term1.scaleVec(dot);
            this.term1.subVec(this.ep1);
            scale = 1 / (normE0 * (1 + dot) * (1 + dot));
            this.term1.scaleVec(scale);

            dot = this.em2.dot(this.em1);
            this.term2.setVec(this.em1);
            this.term2.scaleVec(-dot);
            this.term2.addVec(this.em2);
            scale = 1 / (normEm1 * (1 + dot) * (1 + dot));
            this.term2.scaleVec(scale);

            dot = this.em1.dot(this.e0);
            this.term3.setVec(this.e0);
            this.term3.scaleVec(normE0);
            this.term3.addVecScaled(-normEm1, this.em1);
            this.term3.addVecScaled(dot * normEm1, this.e0);
            this.term3.addVecScaled(-dot * normE0, this.em1);
            scale = 1 / (normEm1 * normE0 * (1 + dot) * (1 + dot));
            this.term3.scaleVec(scale);

            this.force.addVec((this.term1.getVecX() + this.term2.getVecX() + this.term3.getVecX())
                * 8 * ELASTICMOD,
                (this.term1.getVecY() + this.term2.getVecY() + this.term3.getVecY()) * 8
                * ELASTICMOD);
        }

        // Connection to actin filament, if any
        for (ActinFilament fil : this.actin) {

            dist = fil.dist(this);
            delta = dist - fil.getLength();
            this.term1.vectorBetween(this, fil);
            this.term1.normalize();
            this.term1.scaleVec(delta * Simulation.FILAMENT_SPRING);
            this.force.addVec(this.term1);
            fil.getForce().subVec(this.term1); // Don't compute again
        }
    }

    /**
     * Computes the force on the element due to interactions.
     *
     * @param  maxForce  the maximum force from non-interaction sources
     */
    public void computeInteractionForce(final double maxForce) {

        MembraneElement prior1;
        MembraneElement next1;
        double range;
        int count;
        GridMember2Int nbr;
        double dist;
        double scale;

        // Get the surrounding elements in the membrane
        prior1 = this.getPrior();
        next1 = this.getNext();

        // Interaction force with membrane or fixed elements
        count = getNumNeighbors();

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

            if (nbr instanceof MembraneElement) {

                if ((nbr != next1) && (nbr != prior1)) {
                    range = Simulation.getInstance().getMaxSep();
                    dist = nbr.dist(this);

                    if (dist < range) {
                        scale = Simulation.SS_FORCE.forceTimesEqDist(dist / range) / range;

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

                        this.term1.vectorBetween(nbr, this);
                        this.term1.normalize();
                        this.term1.scaleVec(scale);
                        this.force.addVec(term1);
                    }
                }
            } else if (nbr instanceof FixedElement) {
                range = 1.5 * Simulation.getInstance().getMaxSep();
                dist = nbr.dist(this);

                if (dist < range) {
                    scale = Simulation.SS_FORCE.forceTimesEqDist(dist / range) / range;

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

                    this.term1.vectorBetween(nbr, this);
                    this.term1.normalize();
                    this.term1.scaleVec(scale);
                    this.force.addVec(term1);
                }
            } else if (nbr instanceof ActinFilament) {
                dist = nbr.dist(this);
                range = Simulation.getInstance().getMaxSep() + nbr.getRadius();

                if (dist < range) {
                    scale = Simulation.SS_FORCE.forceTimesEqDist(dist / range) / range;

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

                    this.term1.vectorBetween(nbr, this);
                    this.term1.normalize();
                    this.term1.scaleVec(scale);
                    this.force.addVec(term1);
                }
            }
        }
    }

    /**
     * Computes the normal vector at the element based on its neighbors.
     */
    public void recomputeNormal() {

        MembraneElement prior1;
        MembraneElement next1;

        prior1 = this.getPrior();
        next1 = this.getNext();

        this.em1p1.vectorBetween(prior1, next1);
        this.em1p1.normalize();
        this.setVec(this.em1p1.getVecY(), -this.em1p1.getVecX());
    }
}
