Source: EntityManager.js

module.exports = EntityManager;

var Entity     = require('./Entity.js');
var ObjectPool = require('./ObjectPool.js');
var getName    = require('typedef').getName;

/**
 * Manage, create, and destroy entities. Can use methods to mutate entities
 * (tags, components) directly or via the facade on the Entity.
 * @constructor
 */
function EntityManager()
{
  this._tags = {};

  /**
   * @type {Array.<Entity>}
   */
  this._entities = [];

  /**
   * @type {Array.<Group>}
   */
  this._groups = {};

  this._entityPool = new ObjectPool(Entity);

  this._componentPools = {};
}

/**
 * @constructor
 * @param {Array.<Function>} Components
 * @param {Array<Entity>} entities
 */
function Group(Components, entities)
{
  this.Components = Components || [];
  this.entities = entities || [];
}

/**
 * Get a new entity.
 * @return {Entity}
 */
EntityManager.prototype.createEntity = function()
{
  var entity = this._entityPool.aquire();

  this._entities.push(entity);
  entity._manager = this;
  return entity;
};

/**
 * @param {String} tag
 */
EntityManager.prototype.removeEntitiesByTag = function(tag)
{
  var entities = this._tags[tag];

  if (!entities) return;

  for (var x = entities.length - 1; x >= 0; x--) {
    var entity = entities[x];
    entity.remove();
  }
};

/**
 * @param {Entity} entity
 */
EntityManager.prototype.removeEntity = function(entity)
{
  var index = this._entities.indexOf(entity);

  if (!~index)
    throw new Error('Tried to remove entity not in list');

  this._entities.splice(index, 1);

  // Remove entity from any of the component group indexes
  for (var groupName in this._groups) {
    var group = this._groups[groupName];

    // Skip if it doesnt belog, otherwise remove it from the group's list
    if (!entity.hasAllComponents(group.Components))
      continue;
    var loc = group.entities.indexOf(entity);
    if (~loc)
      group.entities.splice(loc, 1);
  }

  // Remove entity from any tag groups
  entity._tags = null;
  for (var tag in this._tags) {
    var entities = this._tags[tag];

    var n = entities.indexOf(entity);
    if (~n) {
      entities.splice(n, 1);
    }
  }

  entity._manager = null;

  // Recycle all components and delete the refs in the entities
  for (var i = 0; i < entity._Components.length; i++) {
    var T = entity._Components[i];

    var cName = componentPropertyName(T);
    var component = entity[cName];
    this._componentPools[cName].release(component);
    delete entity[cName];
  }
  entity._Components.length = 0;

  this._entityPool.release(entity);
};

/**
 * @param {Entity} entity
 * @param {String} tag
 */
EntityManager.prototype.entityAddTag = function(entity, tag)
{
  var entities = this._tags[tag];

  if (!entities)
    entities = this._tags[tag] = [];

  // Don't add if already there
  if (~entities.indexOf(entity)) return;

  entities.push(entity);
  entity._tags.push(tag);
};

/**
 * @param {Entity} entity
 * @param {String} tag
 */
EntityManager.prototype.entityRemoveTag = function(entity, tag)
{
  var entities = this._tags[tag];
  if (!entities) return;

  var index = entities.indexOf(entity);
  if (!~index) return;

  entities.splice(index, 1);
  entity._tags.splice(entity._tags.indexOf(tag), 1);
};

/**
 * @param {Entity} entity
 * @param {Function} Component
 */
EntityManager.prototype.entityAddComponent = function(entity, Component)
{
  if (~entity._Components.indexOf(Component)) return;

  entity._Components.push(Component);

  var cName = componentPropertyName(Component);

  var cPool = this._componentPools[cName];
  if (!cPool) {
    cPool = this._componentPools[cName] = new ObjectPool(Component);
  }
  var component = cPool.aquire();

  entity[cName] = component;

  // Check each indexed group to see if we need to add this entity to the list
  for (var groupName in this._groups) {
    var group = this._groups[groupName];

    if (!~group.Components.indexOf(Component))
      continue;
    if (!entity.hasAllComponents(group.Components))
      continue;
    if (~group.entities.indexOf(entity))
      continue;

    group.entities.push(entity);
  }
};

/**
 * @param {Entity} entity
 * @param {Function} Component
 */
EntityManager.prototype.entityRemoveComponent = function(entity, Component)
{
  var index = entity._Components.indexOf(Component);
  if (!~index) return;

  // Check each indexed group to see if we need to remove it
  for (var groupName in this._groups) {
    var group = this._groups[groupName];

    if (!~group.Components.indexOf(Component))
      continue;
    if (!entity.hasAllComponents(group.Components))
      continue;

    var loc = group.entities.indexOf(entity);
    if (~loc) {
      group.entities.splice(loc, 1);
    }
  }

  var propName = componentPropertyName(Component);
  entity._Components.splice(index, 1);
  var component = entity[propName];
  delete entity[propName];
  this._componentPools[propName].release(component);
};

/**
 * @param {Array.<Function>} Components
 * @return {Array.<Entity>}
 */
EntityManager.prototype.queryComponents = function(Components)
{
  var group = this._groups[groupKey(Components)];

  if (!group) {
    group = this._indexGroup(Components);
  }

  return group.entities;
};

/**
 * @param {String} tag
 * @return {Array.<Entity>}
 */
EntityManager.prototype.queryTag = function(tag)
{
  var entities = this._tags[tag];

  if (entities === undefined)
    entities = this._tags[tag] = [];

  return entities;
};

/**
 * @return {Number} Total number of entities.
 */
EntityManager.prototype.count = function()
{
  return this._entities.length;
};

/**
 * Create an index of entities with a set of components.
 * @param {Array.<Function>} Components
 * @private
 */
EntityManager.prototype._indexGroup = function(Components)
{
  var key = groupKey(Components);

  if (this._groups[key]) return;

  var group = this._groups[key] = new Group(Components);

  for (var n = 0; n < this._entities.length; n++) {
    var entity = this._entities[n];
    if (entity.hasAllComponents(Components)) {
      group.entities.push(entity);
    }
  }

  return group;
};

/**
 * Get information about the object pools of the entities and the various
 * components.
 * @return {Object}
 */
EntityManager.prototype.poolStats = function()
{
  var stats = {};
  var e = this._entityPool;
  stats.entity = {
    used: this._entityPool.totalUsed(),
    size: this._entityPool.count
  };

  for (var cName in this._componentPools) {
    var pool = this._componentPools[cName];
    stats[cName] = {
      used: pool.totalUsed(),
      size: pool.count
    };
  }

  return stats;
};

/**
 * @param {Function} Component
 * @return {String}
 * @private
 */
function componentPropertyName(Component)
{
  var name = getName(Component);
  return name.charAt(0).toLowerCase() + name.slice(1);
}

/**
 * @param {Array.<Function>} Components
 * @return {String}
 * @private
 */
function groupKey(Components)
{
  var names = [];
  for (var n = 0; n < Components.length; n++) {
    var T = Components[n];
    names.push(getName(T));
  }

  return names
    .map(function(x) { return x.toLowerCase(); })
    .sort()
    .join('-');
}