"use strict";

class Boids {
    _boids = [];
    _speedLimit;
    _speedLimitSqr;
    _accelerationLimit;
    _attractors;
    _separationDistanceSqr;
    _alignmentDistanceSqr;
    _cohesionDistanceSqr;
    _separationForce;
    _alignmentForce;
    _cohesionForce;


    constructor (options) {
        options = options || new BoidOptions();
        this._speedLimit = options.speedLimit;
        this._speedLimitSqr = options.speedLimit * options.speedLimit;
        this._accelerationLimit = options.accelerationLimit;
        this._accelerationLimitSqr = options.accelerationLimit * options.accelerationLimit;
        this._attractors = options.attractors;
        this._separationDistanceSqr = options.separationDistance * options.separationDistance;
        this._alignmentDistanceSqr = options.alignmentDistance * options.alignmentDistance;
        this._cohesionDistanceSqr = options.cohesionDistance * options.cohesionDistance;
        this._separationForce = options.separationForce;
        this._alignmentForce = options.alignmentForce;
        this._cohesionForce = options.cohesionForce;

        for (var i = 0; i < options.amount; i++) {
            const boid = new Boid();
            boid.x = (options.maxX - options.minX) * Math.random() + options.minX;
            boid.y = (options.maxY - options.minY) * Math.random() + options.minY; 
            this._boids[i] = boid;
        }
    }

    update() {
        this._updateForces();
        this._updateSpeedAndPosition();
    }

    each(handler) {
        this._boids.forEach(handler);
    }

    _updateForces() {
        for (const boid of this._boids)
        {
            for (const attractor of this._attractors) {
                const deltaX = boid.x - attractor.x;
                const deltaY = boid.y - attractor.y;
                const distSqr = deltaX * deltaX + deltaY * deltaY;
                if (distSqr < attractor.radius * attractor.radius) {
                    const length = this._hypot(deltaX, deltaY);
                    boid.speedX -= attractor.force * deltaX / length;
                    boid.speedY -= attractor.force * deltaY / length;
                }
            }

            let sepForceX = 0;
            let sepForceY = 0;
            let cohForceX = 0;
            let cohForceY = 0;
            let alnForceX = 0;
            let alnForceY = 0;
            for (const otherBoid of this._boids) {
                if (otherBoid === boid)
                    continue;
                const deltaX = boid.x - otherBoid.x;
                const deltaY = boid.y - otherBoid.y;
                const distSqr = deltaX * deltaX + deltaY * deltaY;
                if (distSqr < this._separationDistanceSqr) {
                    sepForceX += deltaX;
                    sepForceY += deltaY;
                } else {
                    if (distSqr < this._cohesionDistanceSqr) {
                        cohForceX += deltaX;
                        cohForceY += deltaY;
                    }
                    if (distSqr < this._alignmentDistanceSqr) {
                        alnForceX += boid.speedX;
                        alnForceY += boid.speedY;
                    }
                }
            }

            const sepLength = this._hypot(sepForceX, sepForceY);
            if (sepLength > 0) {
                boid.accX += this._separationForce * sepForceX / sepLength;
                boid.accY += this._separationForce * sepForceY / sepLength;
            }

            const cohLength = this._hypot(cohForceX, cohForceY);
            if (cohLength > 0) {
                boid.accX -= this._cohesionForce * cohForceX / cohLength;
                boid.accY -= this._cohesionForce * cohForceY / cohLength;
            }

            const alnLength = this._hypot(alnForceX, alnForceY);
            if (alnLength > 0) {
                boid.accX -= this._alignmentForce * alnForceX / alnLength;
                boid.accY -= this._alignmentForce * alnForceY / alnLength;
            }
        }
    }

    _updateSpeedAndPosition() {
        for (const boid of this._boids)
        {
            if (this._accelerationLimitSqr) {
                const distSqr = boid.accX * boid.accX + boid.accY * boid.accY;
                if (distSqr > this._accelerationLimitSqr) {
                    const ratio = this._accelerationLimit / this._hypot(boid.accX, boid.accY);
                    boid.accX *= ratio;
                    boid.accY *= ratio;
                }
            }

            boid.speedX += boid.accX;
            boid.speedY += boid.accY;

            if (this._speedLimitSqr)
            {
                const distSqr = boid.speedX * boid.speedX + boid.speedY * boid.speedY;
                if (distSqr > this._speedLimitSqr) {
                    const ratio = this._speedLimit / this._hypot(boid.speedX, boid.speedY)
                    boid.speedX *= ratio;
                    boid.speedY *= ratio;
                }
            }

            boid.x += boid.speedX;
            boid.y += boid.speedY;
        }
    }

    _hypot(a, b) {
        return Math.sqrt(a*a+b*b);
    }
}

class BoidOptions {
    minX = 0;
    minY = 0;
    maxX = 240;
    maxY = 136;
    amount = 50;
    speedLimit = 1;
    accelerationLimit = 1;
    separationDistance = 5;
    alignmentDistance = 2;
    cohesionDistance = 0;
    separationForce = 0; // 0.5;
    cohesionForce = 0; // 0.1;
    alignmentForce = 0.0; // 0.25;
    attractors = [];
}

class Boid {
    x = 0;
    y = 0;
    speedX = 0;
    speedY = 0;
    accX = 0;
    accY = 0;
}

class BoidAttractor {
    x = 0;
    y = 0;
    radius = 20;
    force = 1;      // Negative force will repel boids
}