// Flocklet by Jim White
// Boid code based on SchoolView (Objective-C) for Next by David Lambert.

import java.applet.Applet;
import java.awt.*;
import java.awt.image.*;
import java.lang.Math;

public class Flocklet extends Applet implements Runnable
{
    Rectangle   screenBounds;

    Image       viewImage;
    Graphics    viewGC;

    int     viewWidth;
    int     viewHeight;

    Thread  workThread;

    int     cycleCount;
    boolean wrap_around;
    int     updateRate;

    // Boid flocking control variables.
    // Same as in SchoolView.m.
    int     boidCount;
    int     goalChangeFreq;

    float   distExp;
    float   momentum;
    float   minRadius;
    float   maxVel;
    float   minVel;
    float   accLimit;
    float   avoidFact;
    float   matchFact;
    float   centerFact;
    float   targetFact;

    int     boid_shape;

    static final int   boidPie     = 0;
    static final int   boidSquare  = 1;
    static final int   boidShip    = 2;
    static final int   boidBee     = 3;
    static final int   boidCustom  = 4;

    int     boid_size;
    int     boid_radius;

    int     boid_x_size;
    int     boid_y_size;

    boolean boidChangesColor;
    Color   boidColor = Color.black;

    boolean followMouse;

    long    lastPaint = 0;

    boolean goal_is_mouse;

    int     mouse_x;
    int     mouse_y;

    // If the goal is a boid then boid[0] is the goal.
    boolean goal_is_boid;

    float   goal_x;
    float   goal_y;

    float   boid_x[];
    float   boid_y[];
    float   boid_xv[];
    float   boid_yv[];
    float   boid_xa[];
    float   boid_ya[];

    byte    boid_frame[];

    int    imageAngleCount;
    int    imageFrameCount;

    Image   boidImages[][];

    Image   backgroundImage;
    Image   goalImage;

    public String[][] getParameterInfo() {
	    String[][] info = {
    	    {"count", 	    "int", 		"number in flock"},
    	    {"updaterate", 	"millis", 	"position update rate"},

    	    {"boidsize", 	"pixels", 	"size of boid"},
    	    {"boidshape",   "string",   "pie square"},
    	    {"changecolor", "boolean",  "boids change color"},

    	    {"goalchange", 	"int", 		"goal change frequency"},
    	    {"followmouse", "boolean",  "flock follows the mouse"},
    	    {"chaseboid",   "boolean",  "flock chases a boid"},
    	    {"wraparound",  "boolean",  "boids wrap around the edges"},

    	    {"distExp", 	"float", 	"distance exp"},
    	    {"momentum", 	"float", 	"momentum"},
    	    {"minRadius", 	"float", 	"minimum radius"},
    	    {"maxVel", 	    "float",    "maximum velocity"},
    	    {"minVel", 	    "float", 	"minimum velocity"},
    	    {"accelLimit",	"float", 	"acceleration limit"},
    	    {"avoidance",	"float", 	"avoidance factor"},
    	    {"matching",	"float", 	"matching factor"},
    	    {"centering",	"float",	"centering factor"},
    	    {"targeting",   "float",    "targeting factor"},
    	};

	    return info;
    }

    float getFloatParameter(String name, float defaultValue)
    {
        String param = getParameter(name);
        float  value;

        try {
            value = (param != null)
                ? Float.valueOf(param).floatValue() : defaultValue;
        } catch (NumberFormatException e) {
            value = defaultValue;
        }

        return value;
    }


    public void init()
    {
        {
            String param;

            param = getParameter("count");
            boidCount = (param != null) ? Integer.parseInt(param) : 20;

            param = getParameter("updaterate");
            updateRate = (param != null) ? Integer.parseInt(param) : 75;

            param = getParameter("goalchange");
            goalChangeFreq = (param != null) ? Integer.parseInt(param) : 50;

            param = getParameter("boidsize");
            boid_size = (param != null) ? Integer.parseInt(param) : 8;

    	    param = getParameter("changecolor");
    	    boidChangesColor = (param == null)
    	        ? true : (param.equalsIgnoreCase("yes")
    	                    || param.equalsIgnoreCase("true"));

    	    param = getParameter("followmouse");
    	    followMouse = (param == null)
    	        ? true : (param.equalsIgnoreCase("yes")
    	                    || param.equalsIgnoreCase("true"));

    	    param = getParameter("chaseboid");
    	    goal_is_boid = (param == null)
    	        ? true : (param.equalsIgnoreCase("yes")
    	                    || param.equalsIgnoreCase("true"));

    	    param = getParameter("wraparound");
    	    wrap_around = (param == null)
    	        ? true : (param.equalsIgnoreCase("yes")
    	                    || param.equalsIgnoreCase("true"));

    	    param = getParameter("boidshape");
    	    if ((param != null) && (param.equalsIgnoreCase("square"))) {
    	        boid_shape = boidSquare;
    	    } else if ((param != null) && (param.equalsIgnoreCase("bee"))) {
    	        boid_shape = boidBee;
    	    } else if ((param != null) && (param.equalsIgnoreCase("ship"))) {
    	        boid_shape = boidShip;
    	    } else /* if ((param == null) || (param.equalsIgnoreCase("pie")) */ {
    	        boid_shape = boidPie;

    	        boid_size *= 2;
    	    }
        }

        distExp    = getFloatParameter("distExp", 3.0f);
        momentum   = getFloatParameter("momentum", 0.05f);
        minRadius  = getFloatParameter("minRadius", 60.0f);
        maxVel     = getFloatParameter("maxVel", 18.0f);
        minVel     = getFloatParameter("minVel", 0.0f);
        accLimit   = getFloatParameter("accelLimit", 5.0f);
        avoidFact  = getFloatParameter("avoidance", 10.0f);
        matchFact  = getFloatParameter("matching", 0.7f);
        centerFact = getFloatParameter("centering", 13.0f);
        targetFact = getFloatParameter("targeting", 10.0f);

        screenBounds  = bounds();

        viewImage = createImage(screenBounds.width, screenBounds.height);
        viewGC    = viewImage.getGraphics();

        viewWidth = screenBounds.width /* - boid_size */ ;
        viewHeight = screenBounds.height /* - boid_size */ ;

        boid_x_size = boid_size;
        boid_y_size = boid_size;

        boid_radius = boid_size / 2;

        boid_x = new float[boidCount];
        boid_y = new float[boidCount];
        boid_xv = new float[boidCount];
        boid_yv = new float[boidCount];
        boid_xa = new float[boidCount];
        boid_ya = new float[boidCount];
        boid_frame = new byte[boidCount];

        for (int i = 0; i < boidCount; ++i) {
            boid_x[i] = (float) Math.random() * viewWidth;
            boid_y[i] = (float) Math.random() * viewHeight;
            boid_xv[i] = 2 * ((float) Math.random() - 0.5f) * maxVel;
            boid_yv[i] = 2 * ((float) Math.random() - 0.5f) * maxVel;
            boid_xa[i] = 0;
            boid_ya[i] = 0;
            boid_frame[i] = 0;
        }

        // The first call to computeAccelerations will choose a goal point.
        goal_x = 0;
        goal_y = 0;
        cycleCount = 0;

        goal_is_mouse = false;

        // init_shipImages();
        
        if (boid_shape == boidBee) init_beeImages();
    }

    void init_beeImages()
    {
        imageAngleCount = 16;
        imageFrameCount = 1;

        boidImages = new Image[imageAngleCount][imageFrameCount];

        Image bees = getImage(getCodeBase(), "bees.gif");
        backgroundImage = getImage(getCodeBase(), "sky.gif");
        goalImage = getImage(getCodeBase(), "flower.gif");

        try {
            {
            MediaTracker    tracker = new MediaTracker(this);

            tracker.addImage(bees, 1);
            tracker.waitForID(1);
            }

        boid_x_size = bees.getWidth(this);
        boid_y_size = boid_x_size;

        boid_size   = boid_x_size;
        boid_radius = boid_size / 2;

        MediaTracker    tracker = new MediaTracker(this);

        tracker.addImage(backgroundImage, 2);
        tracker.addImage(goalImage, 2);

        ImageProducer im_source = bees.getSource();

        for (int i = 0; i < imageAngleCount; ++i) {
            for (int j = 0; j < imageFrameCount; ++j) {
                int y_pos = (imageAngleCount - i) % imageAngleCount;

                // y_pos = (y_pos - (imageAngleCount / 4)) % imageAngleCount;

                CropImageFilter im_filter = new CropImageFilter
                    (0, y_pos * boid_size, boid_size, boid_size);

                ImageProducer im_producer = new FilteredImageSource(im_source, im_filter);

                boidImages[i][j] = createImage(im_producer);

                tracker.addImage(boidImages[i][j], 3);
            }
        }

        tracker.waitForAll();

        } catch (InterruptedException e) {
            System.out.println("No bees!");
            return;
        }
    }

    void init_shipImages()
    {
        imageAngleCount = 32;
        imageFrameCount = 10;

        boidImages = new Image[imageAngleCount][imageFrameCount];

        Image ships = getImage(getCodeBase(), "ships.gif");
        // backgroundImage = getImage(getCodeBase(), "stars.gif");
        // goalImage = getImage(getCodeBase(), "flower.gif");

        try {
            {
            MediaTracker    tracker = new MediaTracker(this);

            tracker.addImage(ships, 1);
            // tracker.addImage(backgroundImage, 2);
            // tracker.addImage(goalImage, 2);

            tracker.waitForID(1);
            }

        boid_x_size = ships.getWidth(this) / (imageAngleCount * imageFrameCount);
        boid_y_size = ships.getHeight(this);

        boid_size   = boid_x_size;
        boid_radius = boid_size / 2;

        MediaTracker    tracker = new MediaTracker(this);

        ImageProducer im_source = ships.getSource();

        for (int i = 0; i < imageAngleCount; ++i) {
            for (int j = 0; j < imageFrameCount; ++j) {
                int x_pos = (imageAngleCount - i) % imageAngleCount;

                // x_pos = (x_pos + (imageAngleCount / 4)) % imageAngleCount;

                x_pos *= imageFrameCount;

                x_pos += j;

                x_pos = (x_pos + 1) % (imageAngleCount * imageFrameCount);

                CropImageFilter im_filter = new CropImageFilter(
                    x_pos * boid_x_size, 0, boid_x_size, boid_y_size);

                ImageProducer im_producer = new FilteredImageSource(im_source, im_filter);

                boidImages[i][j] = createImage(im_producer);

                tracker.addImage(boidImages[i][j], 3);
            }
        }

         tracker.waitForAll();

        } catch (InterruptedException e) {
            System.out.println("No bees!");
            return;
        }

        for (int i = 0; i < boidCount; i++)
            boid_frame[i] = (byte) (i % imageFrameCount);
    }

    public boolean mouseEnter(Event evt, int x, int y)
    {
        if (followMouse) {
            mouse_x = x;
            mouse_y = y;
            goal_is_mouse = true;
        }

        return followMouse;
    }

    public boolean mouseMove(Event evt, int x, int y)
    {
        if (followMouse) {
            mouse_x = x;
            mouse_y = y;
            goal_is_mouse = true;
        }

        return followMouse;
    }

    public boolean mouseExit(Event evt, int x, int y)
    {
        goal_is_mouse = false;

        return followMouse;
    }

    public void update(Graphics g)
    {
        paint(g);
    }

    void draw_boid_line(int i)
    {
        double  angle = Math.atan2((double) boid_xv[i], (double) boid_yv[i]);
        float     dx = (float) (boid_radius * Math.cos(angle));
        float     dy = (float) (boid_radius * Math.sin(angle));

        viewGC.drawLine(
            (int) (boid_x[i] + dx), (int) (boid_y[i] + dy)
            , (int) (boid_x[i] - dx), (int) (boid_y[i] - dy));
    }

    void draw_boid_pie(int i)
    {
        int angle = ((int) (Math.atan2((double) boid_xv[i], (double) boid_yv[i])
                * (180 / Math.PI))) + 90;

        viewGC.fillArc(
            ((int) boid_x[i]) - boid_size
            , ((int) boid_y[i]) - boid_size
            , boid_size * 2, boid_size * 2
            , angle - 20, 40);
    }

    void draw_boid_square(int i)
    {
            viewGC.fillRect(((int) boid_x[i]) - boid_radius
                , ((int) boid_y[i]) - boid_radius
                , boid_size, boid_size);
    }

    void draw_boid_rect(int i)
    {
        double xv = ((double) boid_xv[i]) / ((double) maxVel);
        double yv = ((double) boid_yv[i]) / ((double) maxVel);

            viewGC.fillRect(((int) boid_x[i]) - boid_radius
                , ((int) boid_y[i]) - boid_radius
                , boid_size, boid_size);
    }

    void draw_boid_image(int i)
    {
        viewGC.drawImage(
            boidImages[((int) (Math.atan2((double) boid_xv[i]
                        , (double) boid_yv[i])
                    * ((imageAngleCount / 2) / Math.PI)))
                    + ((imageAngleCount / 2))][boid_frame[i]]
                , ((int) boid_x[i]) - boid_radius
                , ((int) boid_y[i]) - boid_radius
                , this);
    }

    void draw_boid_bee(int i)
    {
        viewGC.drawImage(
            boidImages[((int) (-Math.atan2((double) (boid_y[i] - goal_y), (double) (boid_x[i] - goal_x))
                    * (imageAngleCount / (2 * Math.PI)))) + (imageAngleCount / 2)][boid_frame[i]]
                , ((int) boid_x[i]) - boid_radius
                , ((int) boid_y[i]) - boid_radius
                , this);
    }

    public void paint(Graphics g)
    {
        if (backgroundImage != null) {
            viewGC.drawImage(backgroundImage, 0, 0, screenBounds.width, screenBounds.height, this);
        } else {
            viewGC.clearRect(0, 0, screenBounds.width, screenBounds.height);
            viewGC.setColor(boidColor);
        }

        for (int i = (goal_is_boid ? 1 : 0); i < boidCount; ++i) {
            switch (boid_shape) {
               case boidSquare : draw_boid_square(i); break;
               case boidPie    : draw_boid_pie(i); break;
               case boidBee :    draw_boid_bee(i); break;
               case boidShip :
               case boidCustom :
                    draw_boid_image(i);
                    break;
            }
        }

        if (goalImage != null) {
            viewGC.drawImage(goalImage
                , (int) goal_x - (goalImage.getWidth(this) / 2)
                , (int) goal_y - (goalImage.getHeight(this) / 2)
                , this);
        } else {
            viewGC.setColor(Color.red);
            viewGC.fillOval(((int) goal_x) - boid_radius
                , ((int) goal_y) - boid_radius
                , boid_size, boid_size);
        }

        g.drawImage(viewImage, 0, 0, this);

        lastPaint = System.currentTimeMillis();
    }

    public void start()
    {
        workThread = new Thread(this);
        workThread.start();
    }

    public void run()
    {
        try {
            long    updateLimit = 0;
            long    lastUpdate  = 0;

            float   hue = 0.0f;

            while (workThread != null) {

              if (true) {
                if (boidChangesColor) {
                    boidColor = Color.getHSBColor(hue, 1.0f, 1.0f);

                    hue += 1.0f / 256f;
                    if (hue >= 1.0f)
                        hue = 0.0f;
                }

                computeAccelerations();
                oneStep();

                if (lastPaint < lastUpdate) {
                    // Thread.sleep(100);
                }

                lastUpdate = System.currentTimeMillis();

                repaint();

                if (lastUpdate < updateLimit)
                    Thread.sleep(updateLimit - lastUpdate);
                else
                    Thread.yield();

                updateLimit = lastUpdate + updateRate;
              } else {
                computeAccelerations();
                for (int i = 1; i <= imageAngleCount; ++i) {
                    boid_x[i] = i * 20;
                    boid_y[i] = 40;
                    boid_xv[i] = (float) Math.cos((i - 1) * Math.PI * 2 / imageAngleCount);
                    boid_yv[i] = (float) Math.sin((i - 1) * Math.PI * 2 / imageAngleCount);
                }

                repaint();
                Thread.sleep(4000);
              }
            }
        } catch (InterruptedException e) {
        }
    }

    public void stop()
    {
        if (workThread != null) {
            workThread.stop();
        }
    }

    static float norm(float x1, float x2)
    {
        return ((float) Math.sqrt((x1*x1) + (x2*x2)));
    }

    void computeAccelerations()
    {
    	float	cAx;
    	float	cAy;
    	float	aVx;
    	float	aVy;
    	float	dist;
    	float	aMag;
    	float	xDiff;
    	float	yDiff;
    	float	adjDist;
    	float	adjDistSum;

    	float   mid_x = viewWidth / 2;
    	float   mid_y = viewHeight / 2;
    	float   max_x = viewWidth;
    	float   max_y = viewHeight;

    	adjDist = 0;

    	if (goal_is_mouse) {
    	    goal_x = mouse_x;
    	    goal_y = mouse_y;

    	    if (goal_x > viewWidth)
    	        goal_x = viewWidth;
    	    if (goal_y > viewHeight)
    	        goal_y = viewHeight;

    	    if (goal_x < 0)
    	        goal_x = 0;
    	    if (goal_y < 0)
    	        goal_y = 0;
    	} else {
        	if ((cycleCount++ % goalChangeFreq) == 0)	{
        		goal_x = mid_x + (((float) Math.random() - 0.5f) * 0.45f * viewWidth);
        		goal_y = mid_y + (((float) Math.random() - 0.5f) * 0.45f * viewHeight);

                if (goal_is_boid) {
                    boid_x[0] = goal_x;
                    boid_y[0] = goal_y;
                    boid_xv[0] = 0;
                    boid_yv[0] = 0;
                }
        	} else {
                if (goal_is_boid) {
                    goal_x = boid_x[0];
                    goal_y = boid_y[0];
                }
        	}
        }

    	/* other school avoidance */
    	for (int i = 0; i < boidCount; i++)	{
    		adjDistSum = 0;
    		cAx = cAy = aVx = aVy = 0;

    		boid_xa[i] = 0;
    		boid_ya[i] = 0;

    		for (int j = 0; j < boidCount; j++)	{
    			if (i == j)
    			    continue;

    			xDiff = boid_x[i] - boid_x[j];
    			yDiff = boid_y[i] - boid_y[j];

    			if (xDiff > mid_x)
    				xDiff = max_x - xDiff;
    			if (yDiff > mid_y)
    				yDiff = max_y - yDiff;

    			dist = norm(xDiff, yDiff);
    			if (dist > minRadius)
    			    continue;
    			else if (dist <= 0)
    			    dist = 0.5f;

    			adjDist = dist * dist;
    			adjDistSum += (1 / adjDist);
    			xDiff /= adjDist; yDiff /= adjDist;
    			cAx -= xDiff; cAy -= yDiff;
    			boid_xa[i] += xDiff;
    			boid_xa[i] += yDiff;
    			aVx += (boid_xv[j] / adjDist);
    			aVy += (boid_yv[j] / adjDist);
    		}

    		xDiff = goal_x - boid_x[i];
    		yDiff = goal_y - boid_y[i];
    		boid_xa[i] *= avoidFact;
    		boid_ya[i] *= avoidFact;
    		aMag = norm(boid_xa[i], boid_ya[i]);

    		/* velocity matching */
    		if ((adjDistSum != 0.0f) && (aMag < accLimit)) {
    			aVx /= adjDistSum;
    			aVy /= adjDistSum;
    			boid_xa[i] += ((aVx - boid_xv[i]) * matchFact);
    			boid_ya[i] += ((aVy - boid_yv[i]) * matchFact);
    			aMag = norm(boid_xa[i], boid_ya[i]);

    			/* flock centering */
    			if (aMag < accLimit) {
    				boid_xa[i] += cAx * centerFact;
    				boid_ya[i] += cAy * centerFact;
    				aMag = norm(boid_xa[i], boid_ya[i]);

    				/* target attraction */
    				if (aMag < accLimit) {
    					boid_xa[i] += xDiff * targetFact / adjDist;
    					boid_ya[i] += yDiff * targetFact / adjDist;
    				}
    			}
    		}

    		boid_xa[i] += ((float) Math.random() - 0.5f);
    		boid_ya[i] += ((float) Math.random() - 0.5f);
    		aMag = norm(boid_xa[i], boid_ya[i]);
    		if (aMag > accLimit)	{
    			boid_xa[i] *= Math.sqrt(accLimit/aMag);
    			boid_ya[i] *= Math.sqrt(accLimit/aMag);
    		}
    	}
    }

    void oneStep()
    {
	    float avgIndex = 0;

	    for (int i = 0; i < boidCount; ++i)	{
		    /* apply accelerations */
		    boid_xv[i] = boid_xa[i] + ((1 + momentum) * boid_xv[i]);
		    boid_yv[i] = boid_ya[i] + ((1 + momentum) * boid_yv[i]);

    		float vMag = 1.0e-6f + norm(boid_xv[i], boid_yv[i]);

    		if (vMag > maxVel) {
    			boid_xv[i] *= (maxVel / vMag);
    			boid_yv[i] *= (maxVel / vMag);
    		} else if (vMag < minVel) {
    			boid_xv[i] *= (minVel / vMag);
    			boid_yv[i] *= (minVel / vMag);
    		}

    		/* apply movements */
    		boid_x[i] += boid_xv[i];
    		boid_y[i] += boid_yv[i];

    		if (wrap_around) {
        		if (boid_x[i] > viewWidth) {
        		    boid_x[i] -= viewWidth;
        		} else if (boid_x[i] < 0) {
        		    boid_x[i] += viewWidth;
        		}

        		if (boid_y[i] > viewHeight) {
        		    boid_y[i] -= viewHeight;
        		} else if (boid_y[i] < 0) {
        		    boid_y[i] += viewHeight;
        		}
        	}

        	if ((boid_frame[i] += 1) >= imageFrameCount)
        	    boid_frame[i] = 0;
    	}

        if (!wrap_around && goal_is_boid) {
    		if (boid_x[0] > viewWidth) {
    		    boid_x[0] -= viewWidth;
    		} else if (boid_x[0] < 0) {
    		    boid_x[0] += viewWidth;
    		}

    		if (boid_y[0] > viewHeight) {
    		    boid_y[0] -= viewHeight;
    		} else if (boid_y[0] < 0) {
    		    boid_y[0] += viewHeight;
    		}
    	}
    }
}
