;(function(undefined) {
  'use strict';

  var __instances = {};

  // Deal with resize:
  window.addEventListener('resize', function() {
    for (var key in __instances) {
      if (__instances.hasOwnProperty(key)) {
        var instance = __instances[key];
        instance.refresh();
      }
    }
  });

  /**
   * This is the sigma instances constructor. One instance of sigma represent
   * one graph. It is possible to represent this grapĥ with several renderers
   * at the same time. By default, the default renderer (WebGL + Canvas
   * polyfill) will be used as the only renderer, with the container specified
   * in the configuration.
   *
   * @param  {?*}    conf The configuration of the instance. There are a lot of
   *                      different recognized forms to instantiate sigma, check
   *                      example files, documentation in this file and unit
   *                      tests to know more.
   * @return {sigma}      The fresh new sigma instance.
   *
   * Instanciating sigma:
   * ********************
   * If no parameter is given to the constructor, the instance will be created
   * without any renderer or camera. It will just instantiate the graph, and
   * other modules will have to be instantiated through the public methods,
   * like "addRenderer" etc:
   *
   *  > s0 = new sigma();
   *  > s0.addRenderer({
   *  >   type: 'canvas',
   *  >   container: 'my-container-id'
   *  > });
   *
   * In most of the cases, sigma will simply be used with the default renderer.
   * Then, since the only required parameter is the DOM container, there are
   * some simpler way to call the constructor. The four following calls do the
   * exact same things:
   *
   *  > s1 = new sigma('my-container-id');
   *  > s2 = new sigma(document.getElementById('my-container-id'));
   *  > s3 = new sigma({
   *  >   container: document.getElementById('my-container-id')
   *  > });
   *  > s4 = new sigma({
   *  >   renderers: [{
   *  >     container: document.getElementById('my-container-id')
   *  >   }]
   *  > });
   *
   * Recognized parameters:
   * **********************
   * Here is the exhaustive list of every accepted parameters, when calling the
   * constructor with to top level configuration object (fourth case in the
   * previous examples):
   *
   *   {?string} id        The id of the instance. It will be generated
   *                       automatically if not specified.
   *   {?array}  renderers An array containing objects describing renderers.
   *   {?object} graph     An object containing an array of nodes and an array
   *                       of edges, to avoid having to add them by hand later.
   *   {?object} settings  An object containing instance specific settings that
   *                       will override the default ones defined in the object
   *                       sigma.settings.
   */
  var sigma = function(conf) {
    // Local variables:
    // ****************
    var i,
        l,
        a,
        c,
        o,
        id;

    sigma.classes.dispatcher.extend(this);

    // Private attributes:
    // *******************
    var _conf = conf || {};

    // Little shortcut:
    // ****************
    // The configuration is supposed to have a list of the configuration
    // objects for each renderer.
    //  - If there are no configuration at all, then nothing is done.
    //  - If there are no renderer list, the given configuration object will be
    //    considered as describing the first and only renderer.
    //  - If there are no renderer list nor "container" object, it will be
    //    considered as the container itself (a DOM element).
    //  - If the argument passed to sigma() is a string, it will be considered
    //    as the ID of the DOM container.
    if (
      typeof _conf === 'string' ||
      _conf instanceof HTMLElement
    )
      _conf = {
        renderers: [_conf]
      };
    else if (Object.prototype.toString.call(_conf) === '[object Array]')
      _conf = {
        renderers: _conf
      };

    // Also check "renderer" and "container" keys:
    o = _conf.renderers || _conf.renderer || _conf.container;
    if (!_conf.renderers || _conf.renderers.length === 0)
      if (
        typeof o === 'string' ||
        o instanceof HTMLElement ||
        (typeof o === 'object' && 'container' in o)
      )
        _conf.renderers = [o];

    // Recense the instance:
    if (_conf.id) {
      if (__instances[_conf.id])
        throw 'sigma: Instance "' + _conf.id + '" already exists.';
      Object.defineProperty(this, 'id', {
        value: _conf.id
      });
    } else {
      id = 0;
      while (__instances[id])
        id++;
      Object.defineProperty(this, 'id', {
        value: '' + id
      });
    }
    __instances[this.id] = this;

    // Initialize settings function:
    this.settings = new sigma.classes.configurable(
      sigma.settings,
      _conf.settings || {}
    );

    // Initialize locked attributes:
    Object.defineProperty(this, 'graph', {
      value: new sigma.classes.graph(this.settings),
      configurable: true
    });
    Object.defineProperty(this, 'middlewares', {
      value: [],
      configurable: true
    });
    Object.defineProperty(this, 'cameras', {
      value: {},
      configurable: true
    });
    Object.defineProperty(this, 'renderers', {
      value: {},
      configurable: true
    });
    Object.defineProperty(this, 'renderersPerCamera', {
      value: {},
      configurable: true
    });
    Object.defineProperty(this, 'cameraFrames', {
      value: {},
      configurable: true
    });
    Object.defineProperty(this, 'camera', {
      get: function() {
        return this.cameras[0];
      }
    });
    Object.defineProperty(this, 'events', {
      value: [
        'click',
        'rightClick',
        'clickStage',
        'doubleClickStage',
        'rightClickStage',
        'clickNode',
        'clickNodes',
        'doubleClickNode',
        'doubleClickNodes',
        'rightClickNode',
        'rightClickNodes',
        'hovers',
        'downNode',
        'downNodes',
        'upNode',
        'upNodes'
      ],
      configurable: true
    });

    // Add a custom handler, to redispatch events from renderers:
    this._handler = (function(e) {
      var k,
          data = {};

      for (k in e.data)
        data[k] = e.data[k];

      data.renderer = e.target;
      this.dispatchEvent(e.type, data);
    }).bind(this);

    // Initialize renderers:
    a = _conf.renderers || [];
    for (i = 0, l = a.length; i < l; i++)
      this.addRenderer(a[i]);

    // Initialize middlewares:
    a = _conf.middlewares || [];
    for (i = 0, l = a.length; i < l; i++)
      this.middlewares.push(
        typeof a[i] === 'string' ?
          sigma.middlewares[a[i]] :
          a[i]
      );

    // Check if there is already a graph to fill in:
    if (typeof _conf.graph === 'object' && _conf.graph) {
      this.graph.read(_conf.graph);

      // If a graph is given to the to the instance, the "refresh" method is
      // directly called:
      this.refresh();
    }

  };




  /**
   * This methods will instantiate and reference a new camera. If no id is
   * specified, then an automatic id will be generated.
   *
   * @param  {?string}              id Eventually the camera id.
   * @return {sigma.classes.camera}    The fresh new camera instance.
   */
  sigma.prototype.addCamera = function(id) {
    var self = this,
        camera;

    if (!arguments.length) {
      id = 0;
      while (this.cameras['' + id])
        id++;
      id = '' + id;
    }

    if (this.cameras[id])
      throw 'sigma.addCamera: The camera "' + id + '" already exists.';

    camera = new sigma.classes.camera(id, this.graph, this.settings);
    this.cameras[id] = camera;

    // Add a quadtree to the camera:
    camera.quadtree = new sigma.classes.quad();

    // Add an edgequadtree to the camera:
    if (sigma.classes.edgequad !== undefined) {
      camera.edgequadtree = new sigma.classes.edgequad();
    }

    camera.bind('coordinatesUpdated', function(e) {
      self.dispatchEvent('coordinatesUpdated');
      self.renderCamera(camera);
    });

    this.renderersPerCamera[id] = [];

    return camera;
  };

  /**
   * This method kills a camera, and every renderer attached to it.
   *
   * @param  {string|camera} v The camera to kill or its ID.
   * @return {sigma}           Returns the instance.
   */
  sigma.prototype.killCamera = function(v) {
    v = typeof v === 'string' ? this.cameras[v] : v;

    if (!v)
      throw 'sigma.killCamera: The camera is undefined.';

    var i,
        l,
        a = this.renderersPerCamera[v.id];

    for (l = a.length, i = l - 1; i >= 0; i--)
      this.killRenderer(a[i]);

    delete this.renderersPerCamera[v.id];
    delete this.cameraFrames[v.id];
    delete this.cameras[v.id];

    if (v.kill)
      v.kill();

    return this;
  };

  /**
   * This methods will instantiate and reference a new renderer. The "type"
   * argument can be the constructor or its name in the "sigma.renderers"
   * package. If no type is specified, then "sigma.renderers.def" will be used.
   * If no id is specified, then an automatic id will be generated.
   *
   * @param  {?object}  options Eventually some options to give to the renderer
   *                            constructor.
   * @return {renderer}         The fresh new renderer instance.
   *
   * Recognized parameters:
   * **********************
   * Here is the exhaustive list of every accepted parameters in the "options"
   * object:
   *
   *   {?string}            id     Eventually the renderer id.
   *   {?(function|string)} type   Eventually the renderer constructor or its
   *                               name in the "sigma.renderers" package.
   *   {?(camera|string)}   camera Eventually the renderer camera or its
   *                               id.
   */
  sigma.prototype.addRenderer = function(options) {
    var id,
        fn,
        camera,
        renderer,
        o = options || {};

    // Polymorphism:
    if (typeof o === 'string')
      o = {
        container: document.getElementById(o)
      };
    else if (o instanceof HTMLElement)
      o = {
        container: o
      };

    // If the container still is a string, we get it by id
    if (typeof o.container === 'string')
      o.container = document.getElementById(o.container);

    // Reference the new renderer:
    if (!('id' in o)) {
      id = 0;
      while (this.renderers['' + id])
        id++;
      id = '' + id;
    } else
      id = o.id;

    if (this.renderers[id])
      throw 'sigma.addRenderer: The renderer "' + id + '" already exists.';

    // Find the good constructor:
    fn = typeof o.type === 'function' ? o.type : sigma.renderers[o.type];
    fn = fn || sigma.renderers.def;

    // Find the good camera:
    camera = 'camera' in o ?
      (
        o.camera instanceof sigma.classes.camera ?
          o.camera :
          this.cameras[o.camera] || this.addCamera(o.camera)
      ) :
      this.addCamera();

    if (this.cameras[camera.id] !== camera)
      throw 'sigma.addRenderer: The camera is not properly referenced.';

    // Instantiate:
    renderer = new fn(this.graph, camera, this.settings, o);
    this.renderers[id] = renderer;
    Object.defineProperty(renderer, 'id', {
      value: id
    });

    // Bind events:
    if (renderer.bind)
      renderer.bind(
        [
          'click',
          'rightClick',
          'clickStage',
          'doubleClickStage',
          'rightClickStage',
          'clickNode',
          'clickNodes',
          'clickEdge',
          'clickEdges',
          'doubleClickNode',
          'doubleClickNodes',
          'doubleClickEdge',
          'doubleClickEdges',
          'rightClickNode',
          'rightClickNodes',
          'rightClickEdge',
          'rightClickEdges',
          'hovers',
          'downNode',
          'downNodes',
          'downEdge',
          'downEdges',
          'upNode',
          'upNodes',
          'upEdge',
          'upEdges'
        ],
        this._handler
      );

    // Reference the renderer by its camera:
    this.renderersPerCamera[camera.id].push(renderer);

    return renderer;
  };

  /**
   * This method kills a renderer.
   *
   * @param  {string|renderer} v The renderer to kill or its ID.
   * @return {sigma}             Returns the instance.
   */
  sigma.prototype.killRenderer = function(v) {
    v = typeof v === 'string' ? this.renderers[v] : v;

    if (!v)
      throw 'sigma.killRenderer: The renderer is undefined.';

    var a = this.renderersPerCamera[v.camera.id],
        i = a.indexOf(v);

    if (i >= 0)
      a.splice(i, 1);

    if (v.kill)
      v.kill();

    delete this.renderers[v.id];

    return this;
  };




  /**
   * This method calls the "render" method of each renderer, with the same
   * arguments than the "render" method, but will also check if the renderer
   * has a "process" method, and call it if it exists.
   *
   * It is useful for quadtrees or WebGL processing, for instance.
   *
   * @param  {?object}  options Eventually some options to give to the refresh
   *                            method.
   * @return {sigma}            Returns the instance itself.
   *
   * Recognized parameters:
   * **********************
   * Here is the exhaustive list of every accepted parameters in the "options"
   * object:
   *
   *   {?boolean} skipIndexation A flag specifying wether or not the refresh
   *                             function should reindex the graph in the
   *                             quadtrees or not (default: false).
   */
  sigma.prototype.refresh = function(options) {
    var i,
        l,
        k,
        a,
        c,
        bounds,
        prefix = 0;

    options = options || {};

    // Call each middleware:
    a = this.middlewares || [];
    for (i = 0, l = a.length; i < l; i++)
      a[i].call(
        this,
        (i === 0) ? '' : 'tmp' + prefix + ':',
        (i === l - 1) ? 'ready:' : ('tmp' + (++prefix) + ':')
      );

    // Then, for each camera, call the "rescale" middleware, unless the
    // settings specify not to:
    for (k in this.cameras) {
      c = this.cameras[k];
      if (
        c.settings('autoRescale') &&
        this.renderersPerCamera[c.id] &&
        this.renderersPerCamera[c.id].length
      )
        sigma.middlewares.rescale.call(
          this,
          a.length ? 'ready:' : '',
          c.readPrefix,
          {
            width: this.renderersPerCamera[c.id][0].width,
            height: this.renderersPerCamera[c.id][0].height
          }
        );
      else
        sigma.middlewares.copy.call(
          this,
          a.length ? 'ready:' : '',
          c.readPrefix
        );

      if (!options.skipIndexation) {
        // Find graph boundaries:
        bounds = sigma.utils.getBoundaries(
          this.graph,
          c.readPrefix
        );

        // Refresh quadtree:
        c.quadtree.index(this.graph, {
          prefix: c.readPrefix,
          maxLevel: c.settings('nodeQuadtreeMaxLevel'),
          bounds: {
            x: bounds.minX,
            y: bounds.minY,
            width: bounds.maxX - bounds.minX,
            height: bounds.maxY - bounds.minY
          }
        });

        // Refresh edgequadtree:
        if (
          c.edgequadtree !== undefined &&
          c.settings('drawEdges') &&
          (c.settings('enableEdgeHovering') ||
            c.settings('edgesClippingWithNodes'))
        ) {
          c.edgequadtree.index(this.graph, {
            prefix: c.readPrefix,
            maxLevel: c.settings('edgeQuadtreeMaxLevel'),
            bounds: {
              x: bounds.minX,
              y: bounds.minY,
              width: bounds.maxX - bounds.minX,
              height: bounds.maxY - bounds.minY
            }
          });
        }
      }
    }

    // Call each renderer:
    a = Object.keys(this.renderers);
    for (i = 0, l = a.length; i < l; i++)
      if (this.renderers[a[i]].process) {
        if (this.settings('skipErrors'))
          try {
            this.renderers[a[i]].process();
          } catch (e) {
            console.log(
              'Warning: The renderer "' + a[i] + '" crashed on ".process()"'
            );
          }
        else
          this.renderers[a[i]].process();
      }

    this.render();

    return this;
  };

  /**
   * This method calls the "render" method of each renderer.
   *
   * @return {sigma} Returns the instance itself.
   */
  sigma.prototype.render = function() {
    var l,
        i,
        prefix = 0;

    // Call each renderer:
    for (i in this.renderers)
      if (this.settings('skipErrors'))
        try {
          this.renderers[i].render();
        } catch (e) {
          if (this.settings('verbose'))
            console.log(
              'Warning: The renderer "' + this.renderers[i] +
              '" crashed on ".render()"'
            );
        }
      else
        this.renderers[i].render();

    return this;
  };

  /**
   * This method calls the "render" method of each renderer that is bound to
   * the specified camera. The number of effective renderings is limitated to
   * one per frame, unless you are using the "force" flag.
   *
   * @param  {sigma.classes.camera} camera The camera to render.
   * @param  {?boolean}             force  If true, will render the camera
   *                                       directly.
   * @return {sigma}                       Returns the instance itself.
   */
  sigma.prototype.renderCamera = function(camera, force) {
    var i,
        l,
        a,
        render,
        self = this;

    render = function() {
      a = self.renderersPerCamera[camera.id];
      for (i = 0, l = a.length; i < l; i++) {
        if (self.settings('skipErrors'))
          try {
            a[i].render();
          } catch (e) {
            if (self.settings('verbose'))
              console.log(
                'Warning: The renderer "' +
                  a[i].id +
                  '" crashed on ".render()"'
              );
          }
        else
          a[i].render();
      }
    };

    if (force) {
      render();
    } else {
      self.cameraFrames[camera.id] = render;
      requestAnimationFrame(function() {
        self.cameraFrames[camera.id]();
      });
    }
    return this;
  };

  /**
   * This method calls the "kill" method of each module and destroys any
   * reference from the instance.
   */
  sigma.prototype.kill = function() {
    var k;

    // Dispatching event
    this.dispatchEvent('kill');

    // Kill graph:
    this.graph.kill();

    // Kill middlewares:
    delete this.middlewares;

    // Kill each renderer:
    for (k in this.renderers)
      this.killRenderer(this.renderers[k]);

    // Kill each camera:
    for (k in this.cameras)
      this.killCamera(this.cameras[k]);

    delete this.renderers;
    delete this.cameras;

    // Kill everything else:
    for (k in this)
      if (this.hasOwnProperty(k))
        delete this[k];

    delete __instances[this.id];
  };




  /**
   * Returns a clone of the instances object or a specific running instance.
   *
   * @param  {?string} id Eventually an instance ID.
   * @return {object}     The related instance or a clone of the instances
   *                      object.
   */
  sigma.instances = function(id) {
    return arguments.length ?
      __instances[id] :
      sigma.utils.extend({}, __instances);
  };



  /**
   * The current version of sigma:
   */
  sigma.version = '1.4.0';


  /**
   * Disable ES6 features if true:
   */
  sigma.forceES5 = false;


  /**
   * EXPORT:
   * *******
   */
  if (typeof this.sigma !== 'undefined')
    throw 'An object called sigma is already in the global scope.';

  this.sigma = sigma;

}).call(this);
