
/* Copyright 2014 Intel Corporation, All Rights Reserved.

 Licensed under the Apache License, Version 2.0 (the"License");
 you may not use this file except in compliance with the License.
 You may obtain a copy of the License at

  http://www.apache.org/licenses/LICENSE-2.0

 Unless required by applicable law or agreed to in writing,
 software distributed under the License is distributed on an
 "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 KIND, either express or implied. See the License for the
 specific language governing permissions and limitations
 under the License.
 */

// Boid flocking based on http://harry.me/2011/02/17/neat-algorithms---flocking
var Boid = (function() {
  function instance_boid(the_instance) {
    var position = [0, 0],
        velocity = [0, 0],
        gravityCenter = null,
        neighborRadius = 50,
        maxForce = .1,
        maxSpeed = 1,
        separationWeight = 2,
        alignmentWeight = 1,
        cohesionWeight = 1,
        desiredSeparation = 10,
        instance = the_instance,
        pause = false;

    function boid(neighbors) {
      var accel = flock(neighbors);
      d3_ai_boidWrap(position);
      velocity[0] += accel[0];
      velocity[1] += accel[1];
      if (gravityCenter) {
        var g = d3_ai_boidGravity(gravityCenter, positionl, neighborRadius);
        velocity[0] += g[0];
        velocity[1] += g[1];
      }
      d3_ai_boidLimit(velocity, maxSpeed);
      position[0] += velocity[0];
      position[1] += velocity[1];
      return position;
    }

    function flock(neighbors) {
      var separation = [0, 0],
          alignment = [0, 0],
          cohesion = [0, 0],
          separationCount = 0,
          alignmentCount = 0,
          cohesionCount = 0,
          i = -1,
          l = neighbors.length;
      while (++i < l) {
        var n = neighbors[i];
        if (n === this) continue;
        var npos = n.position(),
            d = d3_ai_boidDistance(position, npos);
        if (d > 0) {
          if (d < desiredSeparation * 2) {
            var tmp = d3_ai_boidNormalize(d3_ai_boidSubtract(position.slice(), npos));
            separation[0] += tmp[0];
            separation[1] += tmp[1];
            separationCount++;
          }

          // Custom forces
          neighbor_instance = n.get_instance();

          function increase () {
            var nvel = n.velocity();
            alignment[0] += nvel[0];
            alignment[1] += nvel[1];
            alignmentCount++;
            cohesion[0] += npos[0];
            cohesion[1] += npos[1];
            cohesionCount++;
          }

          function decrease () {
            var tmp = d3_ai_boidNormalize(d3_ai_boidSubtract(position.slice(), npos));
            separation[0] += tmp[0] / (d / 2);
            separation[1] += tmp[1] / (d / 2);
            separationCount++;
          }

          // Same project = greater cohesion
          if (instance.tenant.id === neighbor_instance.tenant.id) {
            increase();
          } else {
            decrease();
          }

          // Same user = greater cohesion
          if (instance.user.id === neighbor_instance.user.id) {
            increase();
          } else {
            decrease();
          }

          // Same flavor = greater cohesion
          if (instance.user.id === neighbor_instance.user.id) {
            increase();
          } else {
            decrease();
          }
        }
      }

      if (separationCount > 0) {
        separation[0] /= separationCount;
        separation[1] /= separationCount;
      }

      if (alignmentCount > 0) {
        alignment[0] /= alignmentCount;
        alignment[1] /= alignmentCount;
      }
      d3_ai_boidLimit(alignment, maxForce);

      if (cohesionCount > 0) {
        cohesion[0] /= cohesionCount;
        cohesion[1] /= cohesionCount;
      } else {
        cohesion = position.slice();
      }
      cohesion = steerTo(cohesion);

      return [
        separation[0] * separationWeight +
         alignment[0] * alignmentWeight +
          cohesion[0] * cohesionWeight,
        separation[1] * separationWeight +
         alignment[1] * alignmentWeight +
          cohesion[1] * cohesionWeight
      ];
    }

    function steerTo(target) {
      var desired = d3_ai_boidSubtract(target, position),
          d = d3_ai_boidMagnitude(desired);

      if (d > 0) {
        d3_ai_boidNormalize(desired);

        // Two options for desired vector magnitude (1 -- based on distance, 2 -- maxspeed)
        var mul = maxSpeed * (d < 100 ? d / 100 : 1);
        desired[0] *= mul;
        desired[1] *= mul;

        // Steering = Desired minus Velocity
        var steer = d3_ai_boidSubtract(desired, velocity);
        d3_ai_boidLimit(steer, maxForce)  // Limit to maximum steering force
      } else {
        steer = [0, 0];
      }
      return steer;
    }

    boid.set_instance = function(x) {
      if (!arguments.length) return instance;
      instance = x;
      return boid;
    }

    boid.get_instance = function() {
      return instance;
    }

    boid.position = function(x) {
      if (!arguments.length) return position;
      position = x;
      return boid;
    }

    boid.velocity = function(x) {
      if (!arguments.length) return velocity;
      velocity = x;
      return boid;
    }

    boid.gravityCenter = function(x) {
      if (!arguments.length) return gravityCenter;
      gravityCenter = x;
      return boid;
    }

    boid.neighborRadius = function(x) {
      if (!arguments.length) return neighborRadius;
      neighborRadius = x;
      return boid;
    }

    boid.maxForce = function(x) {
      if (!arguments.length) return maxForce;
      maxForce = x;
      return boid;
    }

    boid.maxSpeed = function(x) {
      if (!arguments.length) return maxSpeed;
      maxSpeed = x;
      return boid;
    }

    boid.separationWeight = function(x) {
      if (!arguments.length) return separationWeight;
      separationWeight = x;
      return boid;
    }

    boid.alignmentWeight = function(x) {
      if (!arguments.length) return alignmentWeight;
      alignmentWeight = x;
      return boid;
    }

    boid.cohesionWeight = function(x) {
      if (!arguments.length) return cohesionWeight;
      cohesionWeight = x;
      return boid;
    }

    boid.desiredSeparation = function(x) {
      if (!arguments.length) return desiredSeparation;
      desiredSeparation = x;
      return boid;
    }

    boid.pause = function (x) {
      if (!arguments.length) return pause;
      pause = x;
      return boid;
    }

    return boid;
  }

  function d3_ai_boidNormalize(a) {
    var m = d3_ai_boidMagnitude(a);
    if (m > 0) {
      a[0] /= m;
      a[1] /= m;
    }
    return a;
  }

  function d3_ai_boidWrap(position) {
    if (position[0] > w) position[0] = 0;
    else if (position[0] < 0) position[0] = w;
    if (position[1] > h) position[1] = 0;
    else if (position[1] < 0) position[1] = h;
  }

  function d3_ai_boidGravity(center, position, neighborRadius) {
    if (center[0] != null) {
      var m = d3_ai_boidSubtract(center.slice(), position),
          d = d3_ai_boidMagnitude(m) - 10;
      if (d > 0 && d < neighborRadius * 5) {
        d3_ai_boidNormalize(m);
        m[0] /= d;
        m[1] /= d;
        return m;
      }
    }
    return [0, 0];
  }

  function d3_ai_boidDistance(a, b) {
    var dx = a[0] - b[0],
        dy = a[1] - b[1];
    if (dx > w / 2) dx = w - dx;
    if (dy > h / 2) dy = h - dy;
    return Math.sqrt(dx * dx + dy * dy);
  }

  function d3_ai_boidSubtract(a, b) {
    a[0] -= b[0];
    a[1] -= b[1];
    return a;
  }

  function d3_ai_boidMagnitude(v) {
    return Math.sqrt(v[0] * v[0] + v[1] * v[1]);
  }

  function d3_ai_boidLimit(a, max) {
    if (d3_ai_boidMagnitude(a) > max) {
      d3_ai_boidNormalize(a);
      a[0] *= max;
      a[1] *= max;
    }
    return a;
  }

  return instance_boid;
})();
