/**
 * conrad.js is a tiny JavaScript jobs scheduler,
 *
 * Version: 0.1.0
 * Sources: http://github.com/jacomyal/conrad.js
 * Doc:     http://github.com/jacomyal/conrad.js#readme
 *
 * License:
 * --------
 * Copyright © 2013 Alexis Jacomy, Sciences-Po médialab
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to
 * deal in the Software without restriction, including without limitation the
 * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
 * sell copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * The Software is provided "as is", without warranty of any kind, express or
 * implied, including but not limited to the warranties of merchantability,
 * fitness for a particular purpose and noninfringement. In no event shall the
 * authors or copyright holders be liable for any claim, damages or other
 * liability, whether in an action of contract, tort or otherwise, arising
 * from, out of or in connection with the software or the use or other dealings
 * in the Software.
 */
(function(global) {
  'use strict';

  // Check that conrad.js has not been loaded yet:
  if (global.conrad)
    throw new Error('conrad already exists');


  /**
   * PRIVATE VARIABLES:
   * ******************
   */

  /**
   * A flag indicating whether conrad is running or not.
   *
   * @type {Number}
   */
  var _lastFrameTime;

  /**
   * A flag indicating whether conrad is running or not.
   *
   * @type {Boolean}
   */
  var _isRunning = false;

  /**
   * The hash of registered jobs. Each job must at least have a unique ID
   * under the key "id" and a function under the key "job". This hash
   * contains each running job and each waiting job.
   *
   * @type {Object}
   */
  var _jobs = {};

  /**
   * The hash of currently running jobs.
   *
   * @type {Object}
   */
  var _runningJobs = {};

  /**
   * The array of currently running jobs, sorted by priority.
   *
   * @type {Array}
   */
  var _sortedByPriorityJobs = [];

  /**
   * The array of currently waiting jobs.
   *
   * @type {Object}
   */
  var _waitingJobs = {};

  /**
   * The array of finished jobs. They are stored in an array, since two jobs
   * with the same "id" can happen at two different times.
   *
   * @type {Array}
   */
  var _doneJobs = [];

  /**
   * A dirty flag to keep conrad from starting: Indeed, when addJob() is called
   * with several jobs, conrad must be started only at the end. This flag keeps
   * me from duplicating the code that effectively adds a job.
   *
   * @type {Boolean}
   */
  var _noStart = false;

  /**
   * An hash containing some global settings about how conrad.js should
   * behave.
   *
   * @type {Object}
   */
  var _parameters = {
    frameDuration: 20,
    history: true
  };

  /**
   * This object contains every handlers bound to conrad events. It does not
   * requirea any DOM implementation, since the events are all JavaScript.
   *
   * @type {Object}
   */
  var _handlers = Object.create(null);


  /**
   * PRIVATE FUNCTIONS:
   * ******************
   */

  /**
   * Will execute the handler everytime that the indicated event (or the
   * indicated events) will be triggered.
   *
   * @param  {string|array|object} events  The name of the event (or the events
   *                                       separated by spaces).
   * @param  {function(Object)}    handler The handler to bind.
   * @return {Object}                      Returns conrad.
   */
  function _bind(events, handler) {
    var i,
        i_end,
        event,
        eArray;

    if (!arguments.length)
      return;
    else if (
      arguments.length === 1 &&
      Object(arguments[0]) === arguments[0]
    )
      for (events in arguments[0])
        _bind(events, arguments[0][events]);
    else if (arguments.length > 1) {
      eArray =
        Array.isArray(events) ?
          events :
          events.split(/ /);

      for (i = 0, i_end = eArray.length; i !== i_end; i += 1) {
        event = eArray[i];

        if (!_handlers[event])
          _handlers[event] = [];

        // Using an object instead of directly the handler will make possible
        // later to add flags
        _handlers[event].push({
          handler: handler
        });
      }
    }
  }

  /**
   * Removes the handler from a specified event (or specified events).
   *
   * @param  {?string}           events  The name of the event (or the events
   *                                     separated by spaces). If undefined,
   *                                     then all handlers are removed.
   * @param  {?function(Object)} handler The handler to unbind. If undefined,
   *                                     each handler bound to the event or the
   *                                     events will be removed.
   * @return {Object}            Returns conrad.
   */
  function _unbind(events, handler) {
    var i,
        i_end,
        j,
        j_end,
        a,
        event,
        eArray = Array.isArray(events) ?
                   events :
                   events.split(/ /);

    if (!arguments.length)
      _handlers = Object.create(null);
    else if (handler) {
      for (i = 0, i_end = eArray.length; i !== i_end; i += 1) {
        event = eArray[i];
        if (_handlers[event]) {
          a = [];
          for (j = 0, j_end = _handlers[event].length; j !== j_end; j += 1)
            if (_handlers[event][j].handler !== handler)
              a.push(_handlers[event][j]);

          _handlers[event] = a;
        }

        if (_handlers[event] && _handlers[event].length === 0)
          delete _handlers[event];
      }
    } else
      for (i = 0, i_end = eArray.length; i !== i_end; i += 1)
        delete _handlers[eArray[i]];
  }

  /**
   * Executes each handler bound to the event.
   *
   * @param  {string}  events The name of the event (or the events separated
   *                          by spaces).
   * @param  {?Object} data   The content of the event (optional).
   * @return {Object}         Returns conrad.
   */
  function _dispatch(events, data) {
    var i,
        j,
        i_end,
        j_end,
        event,
        eventName,
        eArray = Array.isArray(events) ?
                   events :
                   events.split(/ /);

    data = data === undefined ? {} : data;

    for (i = 0, i_end = eArray.length; i !== i_end; i += 1) {
      eventName = eArray[i];

      if (_handlers[eventName]) {
        event = {
          type: eventName,
          data: data || {}
        };

        for (j = 0, j_end = _handlers[eventName].length; j !== j_end; j += 1)
          try {
            _handlers[eventName][j].handler(event);
          } catch (e) {}
      }
    }
  }

  /**
   * Executes the most prioritary job once, and deals with filling the stats
   * (done, time, averageTime, currentTime, etc...).
   *
   * @return {?Object} Returns the job object if it has to be killed, null else.
   */
  function _executeFirstJob() {
    var i,
        l,
        test,
        kill,
        pushed = false,
        time = __dateNow(),
        job = _sortedByPriorityJobs.shift();

    // Execute the job and look at the result:
    test = job.job();

    // Deal with stats:
    time = __dateNow() - time;
    job.done++;
    job.time += time;
    job.currentTime += time;
    job.weightTime = job.currentTime / (job.weight || 1);
    job.averageTime = job.time / job.done;

    // Check if the job has to be killed:
    kill = job.count ? (job.count <= job.done) : !test;

    // Reset priorities:
    if (!kill) {
      for (i = 0, l = _sortedByPriorityJobs.length; i < l; i++)
        if (_sortedByPriorityJobs[i].weightTime > job.weightTime) {
          _sortedByPriorityJobs.splice(i, 0, job);
          pushed = true;
          break;
        }

      if (!pushed)
        _sortedByPriorityJobs.push(job);
    }

    return kill ? job : null;
  }

  /**
   * Activates a job, by adding it to the _runningJobs object and the
   * _sortedByPriorityJobs array. It also initializes its currentTime value.
   *
   * @param  {Object} job The job to activate.
   */
  function _activateJob(job) {
    var l = _sortedByPriorityJobs.length;

    // Add the job to the running jobs:
    _runningJobs[job.id] = job;
    job.status = 'running';

    // Add the job to the priorities:
    if (l) {
      job.weightTime = _sortedByPriorityJobs[l - 1].weightTime;
      job.currentTime = job.weightTime * (job.weight || 1);
    }

    // Initialize the job and dispatch:
    job.startTime = __dateNow();
    _dispatch('jobStarted', __clone(job));

    _sortedByPriorityJobs.push(job);
  }

  /**
   * The main loop of conrad.js:
   *  . It executes job such that they all occupate the same processing time.
   *  . It stops jobs that do not need to be executed anymore.
   *  . It triggers callbacks when it is relevant.
   *  . It starts waiting jobs when they need to be started.
   *  . It injects frames to keep a constant frapes per second ratio.
   *  . It stops itself when there are no more jobs to execute.
   */
  function _loop() {
    var k,
        o,
        l,
        job,
        time,
        deadJob;

    // Deal with the newly added jobs (the _jobs object):
    for (k in _jobs) {
      job = _jobs[k];

      if (job.after)
        _waitingJobs[k] = job;
      else
        _activateJob(job);

      delete _jobs[k];
    }

    // Set the _isRunning flag to false if there are no running job:
    _isRunning = !!_sortedByPriorityJobs.length;

    // Deal with the running jobs (the _runningJobs object):
    while (
      _sortedByPriorityJobs.length &&
      __dateNow() - _lastFrameTime < _parameters.frameDuration
    ) {
      deadJob = _executeFirstJob();

      // Deal with the case where the job has ended:
      if (deadJob) {
        _killJob(deadJob.id);

        // Check for waiting jobs:
        for (k in _waitingJobs)
          if (_waitingJobs[k].after === deadJob.id) {
            _activateJob(_waitingJobs[k]);
            delete _waitingJobs[k];
          }
      }
    }

    // Check if conrad still has jobs to deal with, and kill it if not:
    if (_isRunning) {
      // Update the _lastFrameTime:
      _lastFrameTime = __dateNow();

      _dispatch('enterFrame');
      setTimeout(_loop, 0);
    } else
      _dispatch('stop');
  }

  /**
   * Adds one or more jobs, and starts the loop if no job was running before. A
   * job is at least a unique string "id" and a function, and there are some
   * parameters that you can specify for each job to modify the way conrad will
   * execute it. If a job is added with the "id" of another job that is waiting
   * or still running, an error will be thrown.
   *
   * When a job is added, it is referenced in the _jobs object, by its id.
   * Then, if it has to be executed right now, it will be also referenced in
   * the _runningJobs object. If it has to wait, then it will be added into the
   * _waitingJobs object, until it can start.
   *
   * Keep reading this documentation to see how to call this method.
   *
   * @return {Object} Returns conrad.
   *
   * Adding one job:
   * ***************
   * Basically, a job is defined by its string id and a function (the job). It
   * is also possible to add some parameters:
   *
   *  > conrad.addJob('myJobId', myJobFunction);
   *  > conrad.addJob('myJobId', {
   *  >   job: myJobFunction,
   *  >   someParameter: someValue
   *  > });
   *  > conrad.addJob({
   *  >   id: 'myJobId',
   *  >   job: myJobFunction,
   *  >   someParameter: someValue
   *  > });
   *
   * Adding several jobs:
   * ********************
   * When adding several jobs at the same time, it is possible to specify
   * parameters for each one individually or for all:
   *
   *  > conrad.addJob([
   *  >   {
   *  >     id: 'myJobId1',
   *  >     job: myJobFunction1,
   *  >     someParameter1: someValue1
   *  >   },
   *  >   {
   *  >     id: 'myJobId2',
   *  >     job: myJobFunction2,
   *  >     someParameter2: someValue2
   *  >   }
   *  > ], {
   *  >   someCommonParameter: someCommonValue
   *  > });
   *  > conrad.addJob({
   *  >   myJobId1: {,
   *  >     job: myJobFunction1,
   *  >     someParameter1: someValue1
   *  >   },
   *  >   myJobId2: {,
   *  >     job: myJobFunction2,
   *  >     someParameter2: someValue2
   *  >   }
   *  > }, {
   *  >   someCommonParameter: someCommonValue
   *  > });
   *  > conrad.addJob({
   *  >   myJobId1: myJobFunction1,
   *  >   myJobId2: myJobFunction2
   *  > }, {
   *  >   someCommonParameter: someCommonValue
   *  > });
   *
   *  Recognized parameters:
   *  **********************
   *  Here is the exhaustive list of every accepted parameters:
   *
   *    {?Function} end      A callback to execute when the job is ended. It is
   *                         not executed if the job is killed instead of ended
   *                         "naturally".
   *    {?Integer}  count    The number of time the job has to be executed.
   *    {?Number}   weight   If specified, the job will be executed as it was
   *                         added "weight" times.
   *    {?String}   after    The id of another job (eventually not added yet).
   *                         If specified, this job will start only when the
   *                         specified "after" job is ended.
   */
  function _addJob(v1, v2) {
    var i,
        l,
        o;

    // Array of jobs:
    if (Array.isArray(v1)) {
      // Keep conrad to start until the last job is added:
      _noStart = true;

      for (i = 0, l = v1.length; i < l; i++)
        _addJob(v1[i].id, __extend(v1[i], v2));

      _noStart = false;
      if (!_isRunning) {
        // Update the _lastFrameTime:
        _lastFrameTime = __dateNow();

        _dispatch('start');
        _loop();
      }
    } else if (typeof v1 === 'object') {
      // One job (object):
      if (typeof v1.id === 'string')
        _addJob(v1.id, v1);

      // Hash of jobs:
      else {
        // Keep conrad to start until the last job is added:
        _noStart = true;

        for (i in v1)
          if (typeof v1[i] === 'function')
            _addJob(i, __extend({
              job: v1[i]
            }, v2));
          else
            _addJob(i, __extend(v1[i], v2));

        _noStart = false;
        if (!_isRunning) {
          // Update the _lastFrameTime:
          _lastFrameTime = __dateNow();

          _dispatch('start');
          _loop();
        }
      }

    // One job (string, *):
    } else if (typeof v1 === 'string') {
      if (_hasJob(v1))
        throw new Error(
          '[conrad.addJob] Job with id "' + v1 + '" already exists.'
        );

      // One job (string, function):
      if (typeof v2 === 'function') {
        o = {
          id: v1,
          done: 0,
          time: 0,
          status: 'waiting',
          currentTime: 0,
          averageTime: 0,
          weightTime: 0,
          job: v2
        };

      // One job (string, object):
      } else if (typeof v2 === 'object') {
        o = __extend(
          {
            id: v1,
            done: 0,
            time: 0,
            status: 'waiting',
            currentTime: 0,
            averageTime: 0,
            weightTime: 0
          },
          v2
        );

      // If none of those cases, throw an error:
      } else
        throw new Error('[conrad.addJob] Wrong arguments.');

      // Effectively add the job:
      _jobs[v1] = o;
      _dispatch('jobAdded', __clone(o));

      // Check if the loop has to be started:
      if (!_isRunning && !_noStart) {
        // Update the _lastFrameTime:
        _lastFrameTime = __dateNow();

        _dispatch('start');
        _loop();
      }

    // If none of those cases, throw an error:
    } else
      throw new Error('[conrad.addJob] Wrong arguments.');

    return this;
  }

  /**
   * Kills one or more jobs, indicated by their ids. It is only possible to
   * kill running jobs or waiting jobs. If you try to kill a job that does not
   * exist or that is already killed, a warning will be thrown.
   *
   * @param  {Array|String} v1 A string job id or an array of job ids.
   * @return {Object}       Returns conrad.
   */
  function _killJob(v1) {
    var i,
        l,
        k,
        a,
        job,
        found = false;

    // Array of job ids:
    if (Array.isArray(v1))
      for (i = 0, l = v1.length; i < l; i++)
        _killJob(v1[i]);

    // One job's id:
    else if (typeof v1 === 'string') {
      a = [_runningJobs, _waitingJobs, _jobs];

      // Remove the job from the hashes:
      for (i = 0, l = a.length; i < l; i++)
        if (v1 in a[i]) {
          job = a[i][v1];

          if (_parameters.history) {
            job.status = 'done';
            _doneJobs.push(job);
          }

          _dispatch('jobEnded', __clone(job));
          delete a[i][v1];

          if (typeof job.end === 'function')
            job.end();

          found = true;
        }

      // Remove the priorities array:
      a = _sortedByPriorityJobs;
      for (i = 0, l = a.length; i < l; i++)
        if (a[i].id === v1) {
          a.splice(i, 1);
          break;
        }

      if (!found)
        throw new Error('[conrad.killJob] Job "' + v1 + '" not found.');

    // If none of those cases, throw an error:
    } else
      throw new Error('[conrad.killJob] Wrong arguments.');

    return this;
  }

  /**
   * Kills every running, waiting, and just added jobs.
   *
   * @return {Object} Returns conrad.
   */
  function _killAll() {
    var k,
        jobs = __extend(_jobs, _runningJobs, _waitingJobs);

    // Take every jobs and push them into the _doneJobs object:
    if (_parameters.history)
      for (k in jobs) {
        jobs[k].status = 'done';
        _doneJobs.push(jobs[k]);

        if (typeof jobs[k].end === 'function')
          jobs[k].end();
      }

    // Reinitialize the different jobs lists:
    _jobs = {};
    _waitingJobs = {};
    _runningJobs = {};
    _sortedByPriorityJobs = [];

    // In case some jobs are added right after the kill:
    _isRunning = false;

    return this;
  }

  /**
   * Returns true if a job with the specified id is currently running or
   * waiting, and false else.
   *
   * @param  {String}  id The id of the job.
   * @return {?Object} Returns the job object if it exists.
   */
  function _hasJob(id) {
    var job = _jobs[id] || _runningJobs[id] || _waitingJobs[id];
    return job ? __extend(job) : null;
  }

  /**
   * This method will set the setting specified by "v1" to the value specified
   * by "v2" if both are given, and else return the current value of the
   * settings "v1".
   *
   * @param  {String}   v1 The name of the property.
   * @param  {?*}       v2 Eventually, a value to set to the specified
   *                       property.
   * @return {Object|*} Returns the specified settings value if "v2" is not
   *                    given, and conrad else.
   */
  function _settings(v1, v2) {
    var o;

    if (typeof a1 === 'string' && arguments.length === 1)
      return _parameters[a1];
    else {
      o = (typeof a1 === 'object' && arguments.length === 1) ?
        a1 || {} :
        {};
      if (typeof a1 === 'string')
        o[a1] = a2;

      for (var k in o)
        if (o[k] !== undefined)
          _parameters[k] = o[k];
        else
          delete _parameters[k];

      return this;
    }
  }

  /**
   * Returns true if conrad is currently running, and false else.
   *
   * @return {Boolean} Returns _isRunning.
   */
  function _getIsRunning() {
    return _isRunning;
  }

  /**
   * Unreference every job that is stored in the _doneJobs object. It will
   * not be possible anymore to get stats about these jobs, but it will release
   * the memory.
   *
   * @return {Object} Returns conrad.
   */
  function _clearHistory() {
    _doneJobs = [];
    return this;
  }

  /**
   * Returns a snapshot of every data about jobs that wait to be started, are
   * currently running or are done.
   *
   * It is possible to get only running, waiting or done jobs by giving
   * "running", "waiting" or "done" as fist argument.
   *
   * It is also possible to get every job with a specified id by giving it as
   * first argument. Also, using a RegExp instead of an id will return every
   * jobs whose ids match the RegExp. And these two last use cases work as well
   * by giving before "running", "waiting" or "done".
   *
   * @return {Array} The array of the matching jobs.
   *
   * Some call examples:
   * *******************
   *  > conrad.getStats('running')
   *  > conrad.getStats('waiting')
   *  > conrad.getStats('done')
   *  > conrad.getStats('myJob')
   *  > conrad.getStats(/test/)
   *  > conrad.getStats('running', 'myRunningJob')
   *  > conrad.getStats('running', /test/)
   */
  function _getStats(v1, v2) {
    var a,
        k,
        i,
        l,
        stats,
        pattern,
        isPatternString;

    if (!arguments.length) {
      stats = [];

      for (k in _jobs)
        stats.push(_jobs[k]);

      for (k in _waitingJobs)
        stats.push(_waitingJobs[k]);

      for (k in _runningJobs)
        stats.push(_runningJobs[k]);

      stats = stats.concat(_doneJobs);
    }

    if (typeof v1 === 'string')
      switch (v1) {
        case 'waiting':
          stats = __objectValues(_waitingJobs);
          break;
        case 'running':
          stats = __objectValues(_runningJobs);
          break;
        case 'done':
          stats = _doneJobs;
          break;
        default:
          pattern = v1;
      }

    if (v1 instanceof RegExp)
      pattern = v1;

    if (!pattern && (typeof v2 === 'string' || v2 instanceof RegExp))
      pattern = v2;

    // Filter jobs if a pattern is given:
    if (pattern) {
      isPatternString = typeof pattern === 'string';

      if (stats instanceof Array) {
        a = stats;
      } else if (typeof stats === 'object') {
        a = [];

        for (k in stats)
          a = a.concat(stats[k]);
      } else {
        a = [];

        for (k in _jobs)
          a.push(_jobs[k]);

        for (k in _waitingJobs)
          a.push(_waitingJobs[k]);

        for (k in _runningJobs)
          a.push(_runningJobs[k]);

        a = a.concat(_doneJobs);
      }

      stats = [];
      for (i = 0, l = a.length; i < l; i++)
        if (isPatternString ? a[i].id === pattern : a[i].id.match(pattern))
          stats.push(a[i]);
    }

    return __clone(stats);
  }


  /**
   * TOOLS FUNCTIONS:
   * ****************
   */

  /**
   * This function takes any number of objects as arguments, copies from each
   * of these objects each pair key/value into a new object, and finally
   * returns this object.
   *
   * The arguments are parsed from the last one to the first one, such that
   * when two objects have keys in common, the "earliest" object wins.
   *
   * Example:
   * ********
   *  > var o1 = {
   *  >       a: 1,
   *  >       b: 2,
   *  >       c: '3'
   *  >     },
   *  >     o2 = {
   *  >       c: '4',
   *  >       d: [ 5 ]
   *  >     };
   *  > __extend(o1, o2);
   *  > // Returns: {
   *  > //   a: 1,
   *  > //   b: 2,
   *  > //   c: '3',
   *  > //   d: [ 5 ]
   *  > // };
   *
   * @param  {Object+} Any number of objects.
   * @return {Object}  The merged object.
   */
  function __extend() {
    var i,
        k,
        res = {},
        l = arguments.length;

    for (i = l - 1; i >= 0; i--)
      for (k in arguments[i])
        res[k] = arguments[i][k];

    return res;
  }

  /**
   * This function simply clones an object. This object must contain only
   * objects, arrays and immutable values. Since it is not public, it does not
   * deal with cyclic references, DOM elements and instantiated objects - so
   * use it carefully.
   *
   * @param  {Object} The object to clone.
   * @return {Object} The clone.
   */
  function __clone(item) {
    var result, i, k, l;

    if (!item)
      return item;

    if (Array.isArray(item)) {
      result = [];
      for (i = 0, l = item.length; i < l; i++)
        result.push(__clone(item[i]));
    } else if (typeof item === 'object') {
      result = {};
      for (i in item)
        result[i] = __clone(item[i]);
    } else
      result = item;

    return result;
  }

  /**
   * Returns an array containing the values of an object.
   *
   * @param  {Object} The object.
   * @return {Array}  The array of values.
   */
  function __objectValues(o) {
    var k,
        a = [];

    for (k in o)
      a.push(o[k]);

    return a;
  }

  /**
   * A short "Date.now()" polyfill.
   *
   * @return {Number} The current time (in ms).
   */
  function __dateNow() {
    return Date.now ? Date.now() : new Date().getTime();
  }

  /**
   * Polyfill for the Array.isArray function:
   */
  if (!Array.isArray)
    Array.isArray = function(v) {
      return Object.prototype.toString.call(v) === '[object Array]';
    };


  /**
   * EXPORT PUBLIC API:
   * ******************
   */
  var conrad = {
    hasJob: _hasJob,
    addJob: _addJob,
    killJob: _killJob,
    killAll: _killAll,
    settings: _settings,
    getStats: _getStats,
    isRunning: _getIsRunning,
    clearHistory: _clearHistory,

    // Events management:
    bind: _bind,
    unbind: _unbind,

    // Version:
    version: '0.1.0'
  };

  if (typeof exports !== 'undefined') {
    if (typeof module !== 'undefined' && module.exports)
      exports = module.exports = conrad;
    exports.conrad = conrad;
  }
  global.conrad = conrad;
})(this);
