/*
 * SwarmApplet.java
 *
 * (C) 2005, Alex S.
 */
 
import java.awt.*;
import java.util.Vector;

class MovingObject {
    public double x,y;
    public double direction;
    public double speed;

    public MovingObject(double x,double y,double direction,double speed){
        this.x = x;
        this.y = y;
        this.direction = direction;
        this.speed = speed;
    }

    public void move(int w,int h){
        double sin = Math.sin(direction);
        double cos = Math.cos(direction);
        double nx = x + cos * speed;
        double ny = y + sin * speed;
        if(nx > w-1){
            nx -= w-1;
            //cos = -cos;
            //speed += 0.5*(Math.random()-0.5);
        }
        if(nx < 0){
            nx += w-1;
            //cos = -cos;
            //speed += 0.5*(Math.random()-0.5);
        }
        if(ny > h-1){
            ny -= h-1;
            //sin = -sin;
            //speed += 0.5*(Math.random()-0.5);
        }
        if(ny < 0){
            ny += h-1;
            //sin = -sin;
            //speed += 0.5*(Math.random()-0.5);
        }        
        if(speed < 0.1)
            speed = 0.1;
        if(speed > 10)
            speed = 10;
        //direction = Math.atan2(sin,cos);
        x = nx;
        y = ny;
    }
}


/**
 * BufferedCanvas 
 */
class BufferedCanvas extends Canvas {

    private boolean damage = true;        // you can force a render
    private Image image = null;
    private Graphics buffer = null;
    private Thread t;
    private Rectangle r = new Rectangle(0, 0, 0, 0);

    private final static int NUM_OBJS = 200;
    private final static int GWIDTH = 40;
    private final static int GHEIGHT = 40;
    private final static int RADIUS = 5;

    private final static double EPSILON = 0.001;

    private int width,height;

    private int[] queue = new int[GWIDTH * GHEIGHT];
    private int[] queuelast = new int[GWIDTH * GHEIGHT];
    private int[] next = new int[NUM_OBJS];
    private int[] objx = new int[NUM_OBJS];
    private int[] objy = new int[NUM_OBJS];

    private MovingObject[] objects = new MovingObject[NUM_OBJS];


    // weights that control overall swarm behavior

    // Separation: steer to avoid crowding local flockmates
    private float separation = 0;

    // Alignment: steer towards the average heading of local flockmates
    private float alignment = 0;

    // Cohesion: steer to move toward the average position of local flockmat
    private float cohesion = 0;

    public void setSeparationAlignmentCohesion(float s,float a,float c){
        separation = s;
        alignment = a;
        cohesion = c;
        // normalize
        float len = (float)Math.sqrt(
            separation * separation + alignment * alignment + cohesion * cohesion
        );
        if(len > 0){
            separation /= len;
            alignment /= len;
            cohesion /= len;
        }
        //System.out.println("alignment: "+alignment+"; cohesion: "+cohesion+"; separation: "+separation);
    }

    /**
     * initialize things
     */
    public void init(){
        for(int i=0;i<NUM_OBJS;i++){
            objects[i] = new MovingObject(
                Math.random()*r.width,
                Math.random()*r.height,
                Math.random()*(2 * Math.PI),
                Math.random()*RADIUS/2
            );
        }
    }

    public void update(Graphics g) {
        if (r.width != bounds().width || r.height != bounds().height) {            
            image = createImage(bounds().width, bounds().height);
            buffer = image.getGraphics();
            r = bounds();
            init();
            damage = true;
        }
        render(buffer);
        damage = false;
        if (image != null)
            g.drawImage(image,0,0,this);
    }


    /**
     * function that gets called whenever something needs to be
     * rendered.
     */
    public void render(Graphics g) {        
        width = r.width;
        height = r.height;

        for(int i=0;i<queue.length;i++){
            queue[i] = -1;
        }
        double avgx = 0;
        double avgy = 0;
        for(int i=0;i<NUM_OBJS;i++){
            objects[i].move(width,height);
            
            avgx += objects[i].x;
            avgy += objects[i].y;

            int x = objx[i] = (int)( GWIDTH*(objects[i].x / width) );
            int y = objy[i] = (int)( GHEIGHT*(objects[i].y / height) );            
            int bucket = y * GWIDTH + x;

            // is there anything in this bucket already
            if(queue[bucket]==-1){  //  no
                //the start/end of this bucket is set to index of element.
                queuelast[bucket] = queue[bucket] = i;
            }else{                  // yes
                // the last pointer in this bucket will be this index
                queuelast[bucket] = next[ queuelast[bucket] ] = i;
            }
            // initialize the next pointer for that element.
            next[i]=-1;
        }
        avgx /= NUM_OBJS;
        avgy /= NUM_OBJS;

        double diam = RADIUS + RADIUS;
        double distsquared = diam * diam;
        //double xcenterpull = 0.01;
        //double ycenterpull = 0.02;

        for(int i=0;i<NUM_OBJS;i++){
            double x = objects[i].x;
            double y = objects[i].y;

            /*
            {   // adjust direction and speed.
                double dx = avgx - x;
                double dy = avgy - y;
                double sin = Math.sin(objects[i].direction);
                double cos = Math.cos(objects[i].direction);
                double len = Math.sqrt(dx*dx + dy*dy);
                if(len > 0){
                    dx /= len;
                    dy /= len;
                    cos = (1-xcenterpull)*cos + xcenterpull*dx;
                    sin = (1-ycenterpull)*sin + ycenterpull*dy;
                    sin += 0.03; // downward bias.
                    objects[i].direction = Math.atan2(sin,cos);
                    objects[i].speed = len > RADIUS/2 ? RADIUS/2:len;
                }
            }
            */

            int bx = objx[i];
            int by = objy[i];
            
            int Ux = bx - 1 < 0 ? 0:bx-1;
            int Uy = by - 1 < 0 ? 0:by-1;
            int Lx = bx + 1 > GWIDTH-1 ? GWIDTH-1:bx+1;
            int Ly = by + 1 > GHEIGHT-1 ? GHEIGHT-1:by+1;

            //System.out.println("current bucket: "+bx+","+by+" RANGE: ("+Ux+","+Uy+")->("+Lx+","+Ly+")");

            // direction stats.
            double _avgsin = 0;
            double _avgcos = 0;
            double _avgx = 0;
            double _avgy = 0;
            double _avgspeed = 0;
            int _numobjects = 0;

            for(int cy=Uy;cy<=Ly;cy++){
                for(int cx=Ux;cx<=Lx;cx++){
                    //System.out.println("examining: "+cx+", "+cy);
                    int bucket = cy * GWIDTH + cx;
                    for(int j = queue[bucket];j != -1; j = next[j]){

                        if(i != j){
                            double ox = objects[j].x;
                            double oy = objects[j].y;
                            double dx = ox - x;
                            double dy = oy - y;
                            double dd = dx * dx + dy * dy;
                            
                            //System.out.print("comparing: "+i+" to "+j+" dd:"+dd+" distsqrt: "+distsquared);
                            if(dd < distsquared){
                                double dist = Math.sqrt(dd) - EPSILON;
                                double disp = ((diam - dist)/2)/dist;
                                //System.out.print(" dist: "+dist);

                                objects[i].x -= dx * disp;
                                objects[i].y -= dy * disp;

                                objects[j].x += dx * disp;
                                objects[j].y += dy * disp;


                                //objects[i].direction = Math.atan2(-dy,-dx);
                                //objects[j].direction = Math.atan2(dy,dx);

                                //double speed = objects[i].speed;
                                //objects[i].speed = objects[j].speed;
                                //objects[j].speed = speed;
                            }                            
                        }

                        _avgsin += Math.sin(objects[j].direction);
                        _avgcos += Math.cos(objects[j].direction);
                        _avgspeed += objects[j].speed;
                        _avgx += objects[j].x;
                        _avgy += objects[j].y;
                        _numobjects++;

                    }

                }
            }

            // adjust current object according to averages.
            _avgsin /= _numobjects;
            _avgcos /= _numobjects;
            _avgx /= _numobjects;
            _avgy /= _numobjects;
            _avgspeed /= _numobjects;

            // add some noise into the motion
            _avgsin += 0.2*(Math.random()-0.5);
            _avgcos += 0.2*(Math.random()-0.5);
            objects[i].speed += 0.2*(Math.random()-0.5);

            // find heading into center of local group.
            double _dx = _avgx - objects[i].x ;
            double _dy = _avgy - objects[i].y ;
            // normalize.
            double _ddist = Math.sqrt(_dx * _dx + _dy * _dy);

            // find heading into center of -global- group.
            double _dxall = avgx - objects[i].x;
            double _dyall = avgy - objects[i].y;
            double _ddistall = Math.sqrt(_dxall * _dxall + _dyall * _dyall);
            if(_ddistall == 0)
                _ddistall = EPSILON;
            _dxall /= _ddistall;
            _dyall /= _ddistall;
            if(_ddist > 0){
                _dx /= _ddist;
                _dy /= _ddist;
            }

            

            /*
             * Separation: steer to avoid crowding local flockmates
             * Alignment: steer towards the average heading of local flockmates
             * Cohesion: steer to move toward the average position of local flockmat
             */

            // separation  alignment  cohesion
            float sep = (float)Math.cos(separation * Math.PI);
            objects[i].direction = Math.atan2(
                _avgsin*alignment + sep*_dy*cohesion,
                _avgcos*alignment + sep*_dx*cohesion
            );

            objects[i].speed += 0.05*(_avgspeed - objects[i].speed);
            
        }
        //System.out.println("alignment: "+alignment+"; cohesion: "+cohesion+"; separation: "+separation);


        if (true || damage) {
            g.setColor(Color.white);
            g.fillRect(0, 0, bounds().width, bounds().height);

            /*
            // draw grid. 
            g.setColor(Color.gray);
            for(int i=0;i<GWIDTH;i++){
                int j = (int)(i * ((double)width/(double)GWIDTH));
                g.drawLine(j,0,j,height);
            }
            for(int i=0;i<GHEIGHT;i++){
                int j = (int)(i * ((double)height/(double)GHEIGHT));
                g.drawLine(0,j,width,j);
            }
            */
            
            int[] modelxs = { (int)(RADIUS*1.4), -(int)(RADIUS*0.6), -(int)(RADIUS*0.6) };
            int[] modelys = { 0, (int)(RADIUS*0.6), -(int)(RADIUS*0.6) };

            int[] modeltxs = {0,0,0};
            int[] modeltys = {0,0,0};

            for(int i=0;i<NUM_OBJS;i++){
                int x = (int)objects[i].x;
                int y = (int)objects[i].y;
               
                double direction = objects[i].direction;
                double sin = Math.sin(direction);
                double cos = Math.cos(direction);

                for(int tripoint = 0;tripoint < 3;tripoint++){
                    modeltxs[tripoint] = x + (int)(cos * modelxs[tripoint] + -sin * modelys[tripoint]);
                    modeltys[tripoint] = y + (int)(sin * modelxs[tripoint] + cos * modelys[tripoint]);
                }

                g.setColor(Color.lightGray);
                //g.fillOval(x-RADIUS,y-RADIUS,RADIUS+RADIUS,RADIUS+RADIUS);
                g.fillPolygon(modeltxs,modeltys,3);

                g.setColor(Color.darkGray);
                //g.drawOval(x-RADIUS,y-RADIUS,RADIUS+RADIUS,RADIUS+RADIUS);
                g.drawPolygon(modeltxs,modeltys,3);
                
            }
            
            g.setColor(Color.gray);
            g.drawRect(0,0, bounds().width-1,bounds().height-1);
            g.drawString("separation: "+separation+"; alignment: "+alignment+"; cohesion: "+cohesion,12,12);
        }
    }

    public boolean mouseDown(Event evt, int x, int y){
        return true;
    }
    public boolean mouseUp(Event evt, int x, int y){
        return true;
    }    
    public boolean mouseDrag(Event evt, int x, int y){
        return true;
    }
}


/**
 * very basic swarm applet
 */
public class SwarmApplet extends java.applet.Applet implements Runnable 
{
    Thread t = null;
    BufferedCanvas bc = null;    

    Scrollbar separation = null;
    Scrollbar alignment = null;
    Scrollbar cohesion = null;
    
    public Panel packageLabelComponent(String s,Component c){
        Panel p = new Panel();
        p.setLayout(new BorderLayout());
        p.add("West",new Label(s,Label.RIGHT));
        p.add("Center",c);
        return p;
    }

    public void init(){
        setLayout(new BorderLayout());
        Panel p = new Panel();
        p.setLayout(new BorderLayout());

        // Scrollbar(int orientation, int value, int visible, int minimum, int maximum)  

        /*
         * Separation: steer to avoid crowding local flockmates
         * Alignment: steer towards the average heading of local flockmates
         * Cohesion: steer to move toward the average position of local flockmat
         */

        separation = new Scrollbar(Scrollbar.HORIZONTAL, 6000, 1, 1, 10000);
        alignment = new Scrollbar(Scrollbar.HORIZONTAL, 8000, 1, 1,10000 );
        cohesion = new Scrollbar(Scrollbar.HORIZONTAL, 2000, 1, 1, 10000);

        p.add("North",packageLabelComponent("Separation: ",separation));
        p.add("Center",packageLabelComponent("Alignment: ",alignment));
        p.add("South",packageLabelComponent("Cohesion: ",cohesion));
        
        bc = new BufferedCanvas();
        
        add("Center",bc);
        add("South",p);
        show();
    }

    /**
     * process action
     */
    /*
    public boolean handleEvent(Event evt){
        //System.out.println("action called");
        if(separation != null && alignment != null && cohesion != null){
            bc.setSeparationAlignmentCohesion(
                (float)separation.getValue()/(float)(separation.getMaximum() - separation.getMinimum() ),
                (float)alignment.getValue()/(float)(alignment.getMaximum() - alignment.getMinimum() ),
                (float)cohesion.getValue()/(float)(cohesion.getMaximum() - cohesion.getMinimum() )
            );
        }
        return super.handleEvent(evt);    
    }
    */
    

    public void start() { 
        if (t == null) { 
            t = new Thread(this); 
            t.start(); 
        } 
    }
    public void stop() { 
        if (t != null) { 
            t.stop(); 
            t = null; 
        }
    }
    public void run() { 
        try { 
            while (true) { 
        
                bc.setSeparationAlignmentCohesion(
                    (float)separation.getValue()/(float)(separation.getMaximum() - separation.getMinimum() ),
                    (float)alignment.getValue()/(float)(alignment.getMaximum() - alignment.getMinimum() ),
                    (float)cohesion.getValue()/(float)(cohesion.getMaximum() - cohesion.getMinimum() )
                );

                bc.repaint();
                repaint(); 
                t.sleep(30); 
            } 
        }catch(InterruptedException e){}; 
    }
}


