package photoflockr;

import java.lang.Math;

/**
 * This class incapslates data related to a single picture:
 * ID, size of the displayed image, the photo model etc.
 * It is also used to perform calculations for moving the picture
 * node (Sprite) accross the scene.
 */
public class Boid {
    public var id: Integer;
    public var photoModel: PhotoModel;
    public var width: Number;
    public var height: Number;
    public var radius: Number;
    public var maxforce: Number;    // Maximum steering force
    public var maxspeed: Number;    // Maximum speed
    public var desiredseparation: Number;

    package var tags: String[] = bind photoModel.tags;
    package var imageUrl: String = bind photoModel.thumbUrl;
    package var largeImageUrl: String = bind photoModel.photoUrl;

    package var loc: Vector2D = Vector2D { x: width/2, y: height/2 }
    package var vel: Vector2D;
    var acc: Vector2D;
    var neighborDist: Number = radius*2;

    public var selected: Boolean = false on replace {
        Main.flockr.reorderSprites();
    }

    public function selectYourself() {
        selected = true;
    }

    public function returnToFlocking() {
        selected = false;
    }

    function random(lo: Number, hi: Number): Number {
        var res = lo + Math.random() * (hi - lo);
        return res;
    }

    init {
        acc = Vector2D {};
        vel = Vector2D { x: random(-1, 1), y: random(-1, 1) };
        loc = Vector2D { x: random(0, 200), y: random(0, 200) };
    }

    // Calculate new cordinates and heading of the sprite
    public function run(boids: Boid[], cohere: function(b1: Boid, b2: Boid): Number): Void {
        if (not selected) {
            flock(boids, cohere);
            update();
            borders();
        }
    }

    // Accumulate a new acceleration each time based on three rules
    function flock(boids: Boid[], cohere: function(b1: Boid, b2: Boid): Number): Void {
        var sep: Vector2D = separate(boids);           // Separation
        var ali: Vector2D = align(boids);              // Alignment
        var coh: Vector2D = cohesion(boids, cohere);   // Cohesion
        // Arbitrarily weight these forces
        sep.mult(8.0);
        ali.mult(1.0);
        coh.mult(1.0);
        // Add the force vectors to acceleration
        acc.add(sep);
        acc.add(ali);
        acc.add(coh);
    }

    // Update location
    function update(): Void {
        vel.add(acc);           // Update velocity
        vel.limit(maxspeed);    // Limit speed
        vel.updateHeading2D();  // Update direction
        loc.add(vel);           // Finally update location
        acc.setXY(0, 0);        // Reset accelertion
    }

    // A method that calculates a steering vector towards a target
    // Takes a second argument, if true, it slows down as it approaches the target
    function steer(target: Vector2D, slowdown: Boolean): Vector2D {
        var steer: Vector2D;  // The steering vector
        var desired = target.sub(target, loc);  // A vector pointing from the location to the target
        var d = desired.magnitude(); // Distance from the target is the magnitude of the vector
        // If the distance is greater than 0, calc steering (otherwise return zero vector)
        if (d > 0) {
            // Normalize desired
            desired.normalize();
            // Two options for desired vector magnitude (1 -- based on distance, 2 -- maxspeed)
            if ((slowdown) and (d > 100.0)) {
                desired.mult(maxspeed*(d/100.0)); // This damping is somewhat arbitrary
            } else {
                desired.mult(maxspeed);
            }
            // Steering = Desired minus Velocity
            steer = target.sub(desired, vel);
            steer.limit(maxforce);  // Limit to maximum steering force
        } else {
            steer = Vector2D {};
        }
        return steer;
    }

    // Wraparound
    function borders(): Void {
        if (loc.x < -radius*2) loc.x = width + radius*2;
        if (loc.y < -radius*2) loc.y = height + radius*2;
        if (loc.x > width + radius*2) loc.x = -radius*2;
        if (loc.y > height + radius*2) loc.y = -radius*2;
    }

    // Separation
    // Method checks for nearby boids and steers away
    function separate(boids: Boid[]): Vector2D {
        var sum = Vector2D {};
        var count = 0;
        // For every boid in the system, check if it's too close
        for (other in boids) {
            var d = loc.distance(loc, other.loc);
            // If the distance is greater than 0 and less than an arbitrary amount (0 when you are yourself)
            if ((d > 0) and (d < desiredseparation)) {
                // Calculate vector pointing away from neighbor
                var diff = loc.sub(loc, other.loc);
                diff.normalize();
                diff.div(d);        // Weight by distance
                sum.add(diff);
                count++;            // Keep track of how many
            }
        }
        // Average -- divide by how many
        if (count > 0) {
            sum.div(count);
        }
        return sum;
    }

    // Alignment
    // For every nearby boid in the system, calculate the average velocity
    function align(boids: Boid[]): Vector2D {
        var sum = Vector2D {};
        var count = 0;
        for (other in boids) {
            var d = loc.distance(loc, other.loc);
            if ((d > 0) and (d < neighborDist)) {
                sum.add(other.vel);
                count++;
            }
        }
        if (count > 0) {
            sum.div(count);
            sum.limit(maxforce);
        }
        return sum;
    }

    // Cohesion
    // For the average location (i.e. center) of all nearby boids, calculate steering vector towards that location
    function cohesion(boids: Boid[], cohere: function(b1: Boid, b2: Boid): Number): Vector2D {
        var neighbordist = this.neighborDist*6;
        var sum = Vector2D {};   // Start with empty vector to accumulate all locations
        var count = 0;
        for (other in boids) {
            var c = cohere(this, other);
            var d = loc.distance(loc, other.loc);
            if ((d > 0) and (d < neighbordist*c)) {
                sum.add(other.loc); // Add location
                count++;
            }
        }
        if (count > 0) {
            sum.div(count);
            return steer(sum, false);  // Steer towards the location
        }
        return sum;
    }
}