package com.srbenoit.modeling.cell;

import java.util.Random;
import com.srbenoit.geom.Point2;
import com.srbenoit.geom.Point2Int;
import com.srbenoit.geom.Vector2;
import com.srbenoit.log.LoggedObject;
import com.srbenoit.modeling.grid.Grid2D;
import com.srbenoit.modeling.grid.GridMember2Int;

/**
 * A cell, consisting of a membrane and corresponding actin filaments.
 */
public class Cell extends LoggedObject {

    /** the number of membrane elements currently in the cell */
    private int numMembrane;

    /** an entry to the linked list of membrane elements */
    private MembraneElement membrane;

    /** the total amount of monomer in the cell */
    private final double monomerPool;

    /** the maximum radius of an actin segment */
    private final double maxActinRad;

    /** the length of a step in the actin polymerization process */
    private final double actinStep;

    /** the separation of actin from the membrane */
    private final double separation;

    /** the cell thickness, for volume computation */
    private final double thickness;

    /** the current cell volume */
    private double currVolume;

    /** the equilibrium cell volume */
    private double eqVolume;

    /** random number generator */
    private final Random rnd;

    /** a working vector */
    private final Vector2 vec;

    /** a working point */
    private final Point2 test;

    /**
     * Constructs a new <code>Cell</code>.
     *
     * @param  center          the center about which to build the cell
     * @param  radius          the cell radius
     * @param  numElements     the number of elements from which to build the membrane
     * @param  numActinSeg     the number of actin segments attached to each membrane element
     * @param  actinThickness  the thickness of the initial actin cortex
     * @param  avgSep          the average separation between elements
     * @param  maxMove         the maximum per-step movement of an element
     */
    public Cell(final Point2Int center, final double radius, final int numElements,
        final int numActinSeg, final double actinThickness, final double avgSep,
        final double maxMove) {

        double angle;
        double xPos;
        double yPos;
        MembraneElement elem;

        this.maxActinRad = actinThickness / (2.5 * numActinSeg);
        this.actinStep = maxMove / 2;
        this.rnd = new Random();
        this.numMembrane = numElements;
        this.separation = Simulation.getInstance().getMaxSep();

        // Empirical: 100 elements = sep/15, 200 = sep/1.5, 400 = sep / 0.15
        // Formula: denom = 66016350 N ^ -3.322
        this.thickness = avgSep / (66016350 * Math.pow(numElements, -3.322));

        // Create the first element in the list
        this.membrane = new MembraneElement(this, center.getPosX() + radius, center.getPosY(), 0,
                numActinSeg, this.maxActinRad);

        for (int i = 1; i < numElements; i++) {
            angle = 2 * Math.PI * i / numElements;
            xPos = center.getPosX() + (radius * Math.cos(angle));
            yPos = center.getPosY() + (radius * Math.sin(angle));

            elem = new MembraneElement(this, xPos, yPos, angle, numActinSeg, this.maxActinRad);
            elem.addBefore(this.membrane);
        }

        this.monomerPool = numElements * actinThickness;
        computeVolume();
        this.eqVolume = this.currVolume;

        this.vec = new Vector2();
        this.test = new Point2();
    }

    /**
     * Gets the head membrane element in the linked list.
     *
     * @return  the membrane element
     */
    public MembraneElement getMembrane() {

        return this.membrane;
    }

    /**
     * Adds all the objects that make up the cell to a grid, and stores the grid so new objects
     * created as the cell evolves can be automatically added to the grid, and objects deleted can
     * be removed.
     *
     * @param  grid  the grid
     */
    public void addToGrid(final Grid2D grid) {

        MembraneElement elem;
        ActinFilament actin;

        elem = this.membrane;

        for (;;) {
            elem.installInGrid(grid);

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

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

            elem = elem.getNext();

            if (elem == this.membrane) {
                break;
            }
        }
    }

    /**
     * Gets the equilibrium cell volume.
     *
     * @return  the equilibrium cell volume
     */
    public double getEquilibriumVolume() {

        return this.eqVolume;
    }

    /**
     * Gets the current cell volume.
     *
     * @return  the cell volume
     */
    public double getCurrentVolume() {

        return this.currVolume;
    }

    /**
     * Gets the thickness of the cell.
     *
     * @return  the cell thickness
     */
    public double getThickness() {

        return this.thickness;
    }

    /**
     * Computes the interior volume of the membrane.
     */
    public final void computeVolume() {

        MembraneElement current;
        MembraneElement next;
        double vol;

        vol = 0;
        current = this.membrane;
        next = current.getNext();

        do {
            vol += (current.getPosX() * next.getPosY()) - (current.getPosY() * next.getPosX());

            current = next;
            next = current.getNext();

        } while (current != this.membrane);

        this.currVolume = this.thickness * vol * 0.5;
    }

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

        MembraneElement elem;
        double act;
        double delta;
        double total;

        // Walk the membrane, extending filaments where activation is positive, and shortening
        // where it is negative, keeping track of total actin in use.

        total = 0;
        elem = this.membrane;

        do {
            act = elem.getActivation();

            if (act != 0) {

                if (act > 0) {
                    delta = polymerize(elem, act); // returns a positive value
                } else {
                    delta = depolymerize(elem, act); // returns a negative value
                }

                elem.adjustActivation(-delta);
                total += delta;
            }

            elem = elem.getNext();
        } while (elem != this.membrane);

        // If there was a net change, choose random membrane elements and have them polymerize
        // or depolymerize to make up the difference
        useUpMonomer(-total);
    }

    /**
     * Given an amount of monomer level to use up, either polymerizes (of the amount is positive),
     * or depolymerizes (if negative) until the amount is used up.
     *
     * @param  amout  the amount of monomer to use up
     */
    private void useUpMonomer(final double amout) {

        MembraneElement elem;
        double remaining;
        int steps;

        elem = this.membrane;
        remaining = amout;

        while (remaining > 0) {
            steps = this.rnd.nextInt(this.numMembrane);

            for (int i = 0; i < steps; i++) {
                elem = elem.getNext();
            }

            remaining -= polymerize(elem, remaining); // returns a positive value
        }

        while (remaining < 0) {
            steps = this.rnd.nextInt(this.numMembrane);

            for (int i = 0; i < steps; i++) {
                elem = elem.getNext();
            }

            remaining -= depolymerize(elem, remaining); // returns a negative value
        }
    }

    /**
     * Polymerizes the actin below a membrane element.
     *
     * @param   elem       the membrane element at which to polymerize
     * @param   available  the available monomer to use (positive)
     * @return  the amount of monomer actually used (positive)
     */
    private double polymerize(final MembraneElement elem, final double available) {

        ActinFilament actin;
        ActinFilament newActin;
        double xPos;
        double yPos;
        double used;
        double angle;
        double dist;
        int count;
        GridMember2Int nbr;
        boolean good;

        xPos = elem.getPosX();
        yPos = elem.getPosY();

        if (elem.getNumActin() == 0) {

            // There is no actin at the membrane position, so we make a new very small one,
            // positioned on the inside of the membrane at a point that allows it (if such a
            // point exists)
            if (available >= (this.actinStep / 3)) {
                used = this.actinStep / 3;
            } else {
                used = available;
            }

            // Identify an open point at which to insert a new filament.  The best-case is the
            // direction opposite the outward-pointing normal, which is:
            angle = Math.atan2(-elem.getVecY(), -elem.getVecX());
            dist = Simulation.getInstance().getMaxSep();
            count = elem.getNumNeighbors();
            good = false;

            for (double trial = 0; trial < (Math.PI / 2); trial += 0.01) {
                test.setPos(xPos + (dist * Math.cos(angle + trial)),
                    yPos + (dist * Math.sin(angle + trial)));

                good = true;

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

                    if (nbr.dist(test) < nbr.getRadius()) {
                        good = false;

                        break;
                    }
                }

                if (good) {
                    actin = new ActinFilament(test.getPosX(), test.getPosY(),
                            this.separation + used, used, null);
                    actin.installInGrid(elem.getGrid());
                    elem.addActin(actin);

                    break;
                }

                test.setPos(xPos + (dist * Math.cos(angle - trial)),
                    yPos + (dist * Math.sin(angle - trial)));

                good = true;

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

                    if (nbr.dist(test) < nbr.getRadius()) {
                        good = false;

                        break;
                    }
                }

                if (good) {
                    actin = new ActinFilament(test.getPosX(), test.getPosY(),
                            this.separation + used, used, null);
                    actin.installInGrid(elem.getGrid());
                    elem.addActin(actin);

                    break;
                }
            }

            if (!good) {
                // Could not place actin without overlapping something
                used = 0;
            }

        } else {
            actin = elem.getActin(0); // We only polymerize the first actin on a node

            if (actin.getRadius() >= this.maxActinRad) {

                // Need to spawn a new filament from the end of the existing one
                if (available >= (this.actinStep / 3)) {
                    used = this.actinStep / 3;
                } else {
                    used = available;
                }

                vec.vectorBetween(elem, actin);
                vec.normalize();
                vec.scaleVec(this.separation + used);
                newActin = new ActinFilament(xPos + vec.getVecX(), yPos + vec.getVecY(),
                        this.separation + used, used, null);
                newActin.installInGrid(elem.getGrid());
                actin.setUpstream(newActin);
                actin.setLength(actin.getRadius() + used);
                newActin.setDownstream(actin);
                elem.replaceActin(0, newActin);
            } else {

                // Grow the actin filament that's there one step
                if (available >= this.actinStep) {
                    used = this.actinStep;
                } else {
                    used = available;
                }

                actin.adjustLength(used);

                if (actin.getDownstream() != null) {
                    actin.getDownstream().setLength(actin.getDownstream().getLength() + used);
                }
            }
        }

        return used;
    }

    /**
     * Depolymerizes the actin below a membrane element.
     *
     * @param   elem       the membrane element at which to depolymerize
     * @param   available  the available monomer to use (negative)
     * @return  the amount of monomer actually used (negative)
     */
    private double depolymerize(final MembraneElement elem, final double available) {

        int num;
        ActinFilament head;
        ActinFilament actin;
        double used;

        // Find the most downstream filament attached to this membrane element

        num = elem.getNumActin();

        if (num == 0) {
            used = 0;
        } else {
            head = elem.getActin(num - 1); // We only depolymerize the last actin on a node

            actin = head;

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

            if (available < -this.actinStep) {
                used = -this.actinStep;
            } else {
                used = available;
            }

            // Shorten the actin by one step, delete it if it vanishes
            if ((actin.getRadius() + used) > 0) {
                actin.adjustLength(used);
            } else {
                used = -actin.getRadius();
                actin.removeFromGrid();

                if (actin == head) {
                    elem.removeActin(actin);
                } else {
                    actin.getUpstream().setDownstream(null);
                }
            }
        }

        return used;
    }

    /**
     * Restructures the membrane by adding elements where two elements are too far apart or
     * deleting elements where two are too close together.
     *
     * @param  minSep  the minimum separation between elements
     * @param  maxSep  the maximum separation between elements
     */
    public void restructureMembrane(final double minSep, final double maxSep) {

        MembraneElement start;
        MembraneElement elem;
        MembraneElement next;
        double dist;

        elem = this.membrane;
        start = elem;

        do {
            next = elem.getNext();

            dist = elem.dist(next);

            if (dist > maxSep) {
                subdivideEdge(elem, next);
                start = elem;
            } else if (dist < minSep) {
//                mergeElements(elem, next);
//                start = elem;
            }

            elem = elem.getNext();
        } while (elem != start);
    }

    /**
     * Subdivides the edge between two membrane elements, adding a new element at the midpoint.
     *
     * @param  elem  the first element
     * @param  next  the next element
     */
    private void subdivideEdge(final MembraneElement elem, final MembraneElement next) {

        MembraneElement newElem;

        // Split the edge in half at the midpoint, adding a new element, whose normal is
        // the average of the normals at the endpoints.
        this.vec.setVec(elem);
        this.vec.addVec(next);
        this.vec.normalize();

        newElem = new MembraneElement(this, (elem.getPosX() + next.getPosX()) / 2,
                (elem.getPosY() + next.getPosY()) / 2, this.vec.getVecX(), this.vec.getVecY(), 0,
                0);
        newElem.installInGrid(elem.getGrid());
        newElem.addAfter(elem);

        this.numMembrane++;
    }

    /**
     * Merges two membrane elements that are too close together.
     *
     * @param   elem  the first element
     * @param   next  the next element
     * @return  the amount of monomer released by the merged element (positive)
     */
    private void mergeElements(final MembraneElement elem, final MembraneElement next) {

        ActinFilament actin;

        // Move any actin filaments under 'next' to 'elem'
        while (next.getNumActin() > 0) {
            actin = next.getActin(0);
            next.removeActin(actin);
            elem.addActin(actin);
        }

        // Move 'elem' to the midpoint
        vec.vectorBetween(elem, next);
        vec.scaleVec(0.5);
        elem.move(vec);

        // Remove 'next' from the membrane.  If 'next' happens to be the 'membrane' member
        // variable, we make 'elem' the new membrane head.
        if (this.membrane == next) {
            this.membrane = elem;
        }

        next.remove();
        next.removeFromGrid();

        this.numMembrane--;
    }

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

        MembraneElement elem;
        int count;
        ActinFilament actin;

        computeVolume();

        // Zero out all forces first
        elem = this.membrane;

        do {
            elem.getForce().setVec(0, 0);

            count = elem.getNumActin();

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

                while (actin != null) {
                    actin.getForce().setVec(0, 0);
                    actin = actin.getDownstream();
                }
            }

            elem = elem.getNext();
        } while (elem != this.membrane);

        // Now compute all forces
        elem = this.membrane;

        do {
            elem.computeInnerForce();

            count = elem.getNumActin();

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

                while (actin != null) {
                    actin.computeInnerForce();
                    actin = actin.getDownstream();
                }
            }

            elem = elem.getNext();
        } while (elem != this.membrane);
    }

    /**
     * Computes the maximum force on any element in the cell.
     *
     * @return  the maximum force
     */
    public double maxForce() {

        MembraneElement elem;
        int count;
        ActinFilament actin;
        double force;
        double maxForce;

        // Determine the largest force in the system
        maxForce = 0;
        elem = this.membrane;

        do {
            force = elem.getForce().length();

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

            count = elem.getNumActin();

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

                while (actin != null) {
                    force = actin.getForce().length();

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

                    actin = actin.getDownstream();
                }
            }

            elem = elem.getNext();
        } while (elem != this.membrane);

        return maxForce;
    }

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

        MembraneElement elem;
        int count;
        ActinFilament actin;

        elem = this.membrane;

        do {
            elem.computeInteractionForce(maxForce);

            count = elem.getNumActin();

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

                while (actin != null) {
                    actin.computeInteractionForce(maxForce);
                    actin = actin.getDownstream();
                }
            }

            elem = elem.getNext();
        } while (elem != this.membrane);
    }

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

        MembraneElement elem;
        int count;
        ActinFilament actin;
        Vector2 force;

        elem = this.membrane;

        do {
            force = elem.getForce();
            elem.move(force.getVecX() * mobility, force.getVecY() * mobility);

            count = elem.getNumActin();

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

                while (actin != null) {
                    force = actin.getForce();
                    actin.move(force.getVecX() * mobility, force.getVecY() * mobility);
                    actin = actin.getDownstream();
                }
            }

            elem = elem.getNext();
        } while (elem != this.membrane);
    }

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

        MembraneElement elem;

        elem = this.membrane;

        do {
            elem.recomputeNormal();
            elem = elem.getNext();
        } while (elem != this.membrane);
    }
}
