import java.util.*;

import net.phys2d.math.*;
import net.phys2d.raw.*;
import net.phys2d.raw.shapes.*;
import net.phys2d.raw.strategies.*;

public World world;

ArrayList shape;

final float DISTANCE_SQUARED_THRESHOLD = 25.0f;
final float ANGLE_THRESHOLD = radians(3);

// True if we should render normals
private boolean normals = false;
// True if we should render contact points
private boolean contacts = false;
// True if the simulation should be reset
private boolean needsReset = false;


private final int ACTION_NONE = 0;
private final int ACTION_DRAWING = 1;
private final int ACTION_DELETING = 2;

private int action = ACTION_NONE;

private final int POLY_CLOCKWISE = -1;
private final int POLY_COUNTERCLOCKWISE = 1;
private final int POLY_INCOMPUTABLE = 0;

PFont font;

void setup()
{
    // Setup window
    size(400, 400);
    frameRate(200);
    
    font = loadFont("d.vlw");
    textFont(font, 12);
    
    initialiseWorld();
}

void initialiseWorld()
{
    // Initialise physics
    world = new World(new Vector2f(0.0f, 20.0f), 20, new QuadSpaceStrategy(20, 5));
    
    // Add ground
    Line line1 = new Line(0, height - 100, 50, height - 50);
    Body ground1 = new StaticBody(line1);
    
    Line line2 = new Line(width - 50, height - 50, width, height - 100);
    Body ground2 = new StaticBody(line2);
    
    Line line3 = new Line(50, height - 50, width - 50, height - 50);
    Body ground3 = new StaticBody(line3);
    
    world.add(ground1);
    world.add(ground2);
    world.add(ground3);
}

void draw()
{
    background(255);

    noFill();
    stroke(200);
    rect(0, 0, width - 1, height - 1);

    fill(0);
    noStroke();
    text("FPS: " + frameRate, 5, 20);

    if (needsReset)
    {
        BodyList bodies = world.getBodies();
        
        for (int i = bodies.size() - 1; i >= 0; i--)
        {
            Body body = bodies.get(i);
            
            if (!body.isStatic())
            {
                world.remove(body);
            }
        }
        
        shape = null;
        
        needsReset = false;
    }

    world.step();
    
    if (shape != null)
    {    
        drawShape(shape);
    }
    
    BodyList bodies = world.getBodies();

    for (int i = 0; i < bodies.size(); i++)
    {
        Body body = bodies.get(i);

        drawBody(body);
    }
    
    if (contacts || normals)
    {
        ArbiterList arbs = world.getArbiters();
		
        for (int i = 0; i < arbs.size(); i++)
        {
            Arbiter arb = arbs.get(i);

            Contact[] contacts = arb.getContacts();
            int numContacts = arb.getNumContacts();

            for (int j = 0; j < numContacts; j++)
            {
                drawContact(contacts[j]);
            }
        }
    }
}

void drawShape(ArrayList shape)
{
    ellipseMode(CENTER);
    
    if (shape.size() == 0)
    {
        return;
    }
    else
    {
        noStroke();
        fill(100);
        Vector2f p = (Vector2f)shape.get(0);
        ellipse(p.x, p.y, 2, 2);
        
        if (shape.size() > 1)
        {
            for (int i = 1; i < shape.size(); i++)
            {
                Vector2f p1 = (Vector2f)shape.get(i - 1);
                Vector2f p2 = (Vector2f)shape.get(i);
                
                noStroke();
                fill(100);
                ellipse(p2.x, p2.y, 2, 2);
                
                stroke(150);
                line(p1.x, p1.y, p2.x, p2.y);
            }
        }
    }
}

void drawBody(Body body)
{
    if (body.getShape() instanceof Box)
    {
        drawBoxBody(body, (Box) body.getShape());
    }
    else if (body.getShape() instanceof Circle)
    {
        drawCircleBody(body, (Circle) body.getShape());
    }
    else if (body.getShape() instanceof Line)
    {
        drawLineBody(body, (Line) body.getShape());
    }
    else if (body.getShape() instanceof net.phys2d.raw.shapes.Polygon)
    {
        drawPolygonBody(body, (net.phys2d.raw.shapes.Polygon) body.getShape());
    }
}

void drawPolygonBody(Body body, net.phys2d.raw.shapes.Polygon poly)
{

    ROVector2f[] verts = poly.getVertices(body.getPosition(), body.getRotation());
    
    stroke(50);
    fill(200);

    beginShape();

    for (int i = 0; i < verts.length; i++)
    {
        vertex(verts[i].getX(), verts[i].getY());
    }
    
    endShape(CLOSE);
}

void drawLineBody(Body body, Line l)
{
    stroke(50);
    noFill();

    Vector2f[] verts = l.getVertices(body.getPosition(), body.getRotation());
    line(
    (int) verts[0].getX(),
    (int) verts[0].getY(), 
    (int) verts[1].getX(),
    (int) verts[1].getY());
}

void drawCircleBody(Body body, Circle circle)
{
    stroke(50);
    noFill();
    
    float x = body.getPosition().getX();
    float y = body.getPosition().getY();
    float r = circle.getRadius();
    float rot = body.getRotation();
    float xo = (float) (cos(rot) * r);
    float yo = (float) (sin(rot) * r);

    ellipse(x, y, r * 2, r * 2);
    line((int) x, (int) y, (int) (x+xo), (int) (y+yo));
}

void drawBoxBody(Body body, Box b)
{
    Vector2f[] pts = b.getPoints(body.getPosition(), body.getRotation());

    Vector2f v1 = pts[0];
    Vector2f v2 = pts[1];
    Vector2f v3 = pts[2];
    Vector2f v4 = pts[3];

    stroke(50);
    noFill();
    line((int) v1.x, (int) v1.y, (int) v2.x, (int) v2.y);
    line((int) v2.x, (int) v2.y, (int) v3.x, (int) v3.y);
    line((int) v3.x, (int) v3.y, (int) v4.x, (int) v4.y);
    line((int) v4.x, (int) v4.y, (int) v1.x, (int) v1.y);
}

void drawContact(Contact contact)
{
    int x = (int) contact.getPosition().getX();
    int y = (int) contact.getPosition().getY();
    if (contacts)
    {
        noStroke();
        fill(0, 0, 255);
        ellipse(x, y, 6, 6);
    }

    if (normals)
    {
        int dx = (int) (contact.getNormal().getX() * 10);
        int dy = (int) (contact.getNormal().getY() * 10);
        stroke(150);
        noFill();
        line(x, y, x + dx, y + dy);
    }
}


void mousePressed()
{
    if (mouseButton == LEFT)
    {
        action = ACTION_DRAWING;
        shape = new ArrayList();
        Vector2f p = new Vector2f(mouseX, mouseY);
        shape.add(p);
    }
    
    if (mouseButton == RIGHT)
    {
        action = ACTION_DELETING;
        
        boolean found = false;
        Vector2f mouseVector = new Vector2f(mouseX, mouseY);
        
        BodyList bodies = world.getBodies();
        
        for (int i = 0; i < bodies.size() && !found; i++)
        {
            Body body = bodies.get(i);
            net.phys2d.raw.shapes.Shape bodyShape = body.getShape();
            if (bodyShape instanceof net.phys2d.raw.shapes.Polygon)
            {
                net.phys2d.raw.shapes.Polygon poly = (net.phys2d.raw.shapes.Polygon)bodyShape;
                if (poly.contains(mouseVector))
                {
                    world.remove(body);
                    found = true;
                }
            }
        }
    }
}

void mouseDragged()
{
    if (mouseButton == LEFT)
    {
        int pointCount = shape.size();
        boolean addThisPoint = true;
        // p1 is the most recent point - the one caused by the drag event
        // p2 is the previously stored point
        // p3, if it exists, is the point before the previously stored point.
        Vector2f p1 = new Vector2f(mouseX, mouseY);
        Vector2f p2 = (Vector2f)shape.get(pointCount - 1);
    
        float distanceSquared = p1.distanceSquared(p2);
    
        if (distanceSquared < DISTANCE_SQUARED_THRESHOLD)
        {
            addThisPoint = false;
        }
    
        if (pointCount > 1)
        {
            Vector2f p3 = (Vector2f)shape.get(pointCount - 2);
        
            float angle = vectorAngle(p1, p2, p3);
            
            if (angle < ANGLE_THRESHOLD ||
                (angle < PI + ANGLE_THRESHOLD && angle > PI - ANGLE_THRESHOLD))
            {
                addThisPoint = false;
            }
        }
    
        if (addThisPoint)
        {
            shape.add(p1);
        }
    }
}

void mouseReleased()
{
    if (action == ACTION_DRAWING)
    {
        if (shape != null && shape.size() > 2)
        {
            
            Vector2f[] vertexArray;
            
            Vector2f[] arrayType = new Vector2f[1];

            vertexArray = (Vector2f[])shape.toArray(arrayType);

            int polyOrientation = orientation(shape);

            if (polyOrientation == POLY_CLOCKWISE)
            {
                int j = vertexArray.length - 1;
                for (int i = 0; i < j; i++, j--)
                {
                    Vector2f temp = vertexArray[i];
                    vertexArray[i] = vertexArray[j];
                    vertexArray[j] = temp;
                }
            }
        
            net.phys2d.raw.shapes.Polygon poly = new net.phys2d.raw.shapes.Polygon(vertexArray);
            Vector2f centroid = poly.getCentroid();
        
            for (int i = 0; i < vertexArray.length; i++)
            {
                vertexArray[i].sub(centroid);
            }
        
            poly = new net.phys2d.raw.shapes.Polygon(vertexArray);
        
            Body polyBody = new Body(poly, poly.getArea());
            polyBody.setPosition(centroid.x, centroid.y);
            polyBody.setRotation(0.0f);
            polyBody.setForce(0, 10.0f);
            world.add(polyBody);
        }
        shape = null;
    }
    
    action = ACTION_NONE;
}

void keyPressed()
{
    switch (key)
    {
        
        case ' ':
            Vector2f[] circleVerts = new Vector2f[30];
            float[] radius = {20,10};
            for(int i = 0; i < 30; i++)
            {
                float angle = (float) (3 * 4 * i * PI / 180);
                circleVerts[i] = new Vector2f(
                    (float) (Math.cos(angle) * radius[i % 2]), 
                    (float) (Math.sin(angle) * radius[i % 2]));
            }
            net.phys2d.raw.shapes.Polygon circlePolygon = new net.phys2d.raw.shapes.Polygon(circleVerts);
            Body newBody = new Body(circlePolygon, 4);
		
            newBody.setPosition(250, 150);
            newBody.setRotation((float) (random(1.0) * 2 * PI));
            newBody.setForce(0, 10.0f);
            world.add(newBody);
            break;

        case 'r':
            needsReset = true;
            break;
            
        case 'c':
            contacts = !contacts;
            break;
            
        case 'n':
            normals = !normals;
            break;
    }
}


float vectorAngle(Vector2f p1, Vector2f p2, Vector2f p3)
{
    
    Vector2f a = MathUtil.sub(p2, p1);
    Vector2f b = MathUtil.sub(p2, p3);
    
    float dotProduct = a.dot(b);
    float crossProduct = MathUtil.cross(a, b);
    
    float angle = atan2(crossProduct, dotProduct);
    return abs(angle);
}

public Vector2f[] convexHull(ArrayList points)
{
    // Find the lowest point
    Vector2f base = (Vector2f)points.get(0);
    
    for (int i = 1; i < points.size(); i++)
    {
        Vector2f temp = (Vector2f)points.get(i);
        
        if (temp.y < base.y || (temp.y == base.y && temp.x < base.x))
        {
            base = (Vector2f)points.get(i);
        }
    }
    
    // Sort the point array
    Vector2f[] sortedPoints;
    Vector2f[] arrayType = new Vector2f[1];
    
    sortedPoints = (Vector2f[])points.toArray(arrayType);
    VectorAngleComparator vac = new VectorAngleComparator(base);
    Arrays.sort(sortedPoints, vac);
    
    // Calculate the convex hull
    Stack stack = new Stack();
    
    stack.push(sortedPoints[0]);
    stack.push(sortedPoints[1]);
    
    for (int i = 3; i < sortedPoints.length; i++)
    {
        while (stack.size() >= 2 &&
                crossProduct((Vector2f)stack.get(stack.size() - 2), (Vector2f)stack.peek(), sortedPoints[i]) <= 0)
        {
            stack.pop();
        }
        
        stack.push(sortedPoints[i]);
    }
    
    //stack.push(base);
    
    /*
    Vector2f[] hull = new Vector2f[stack.size()];
    
    for (int i = 0; i < hull.length; i++)
    {
        hull[i] = (Vector2f)stack.get(i);//(Vector2f)stack.pop();
    }
    
    return hull;
    */
    return (Vector2f[])stack.toArray(arrayType);
}

public float crossProduct(Vector2f p1, Vector2f p2, Vector2f p3)
{
    return (p2.x - p1.x) * (p3.y - p1.y) - (p3.x - p1.x) * (p2.y - p1.y);
}

class VectorAngleComparator implements Comparator
{
    
    private Vector2f base;
    
    VectorAngleComparator(Vector2f base)
    {
        this.base = base;
    }
    
    public int compare(Object p1, Object p2)
    {
        if (p1 instanceof Vector2f && p2 instanceof Vector2f)
        {
            Vector2f a = MathUtil.sub((Vector2f)p1, base);
            Vector2f b = MathUtil.sub((Vector2f)p2, base);
            
            float aAngle = atan2(a.y, a.x);
            float bAngle = atan2(b.y, b.x);
            
            float angleDiff = aAngle - bAngle;
            
            if (angleDiff < 0)
            {
                return -1;
            }
            
            if (angleDiff > 0)
            {
                return 1;
            }
        }
        
        return 0;
    }
    
    public boolean equals(Object obj)
    {
        if (obj instanceof VectorAngleComparator)
        {
            VectorAngleComparator vac = (VectorAngleComparator)obj;
            
            return (vac.base.x == this.base.x) && (vac.base.y == this.base.y);
        }
        
        return false;
    }
}

public int orientation(ArrayList poly)
{
    float area;
    int i, pointCount = poly.size();

    Vector2f p1, p2;
    
    p1 = (Vector2f)poly.get(pointCount - 1);
    p2 = (Vector2f)poly.get(0);
    
    area = p1.x * p2.y - p2.x * p1.y;

    for (i = 0; i < pointCount - 1; i++)
    {
        p1 = (Vector2f)poly.get(i);
        p2 = (Vector2f)poly.get(i + 1);
        area += p1.x * p2.y - p2.x * p1.y;
    }
    
    if (area >= 0.0)
    {
        return POLY_COUNTERCLOCKWISE;
    }
    
    return POLY_CLOCKWISE;
}
