var voxioConnector = (function() {
  var clients = [],
      clientTimeout = 15000, // 15 seconds
      clientTimeoutId,
      clientState = { DISCONNECTED: "disconnected", CONNECTED: "connected", CONNECTING: "connecting" },
      currentClientIndex = -1,
      active = false;

  function handleError(e) {
    console.log(e);
  }

  // ensures that supplied function is executed only once
  // (could be moved to some utility module)
  function once(fn) {
    var done = false;

    return function () {
      if (done) {
        return undefined;
      }
      done = true;
      return fn.apply(this, arguments);
    };
  }

  // utility object for working with time intervals
  var TimeIntervals = function(maxLength) {
    // just make sure that an instance is created even if caller
    // invoked this function without 'new' or 'Object.create'
    if(!(this instanceof TimeIntervals)) {
      return new TimeIntervals(maxLength);
    }

    var intervals = [];

    return {
      add: function(dateStart, dateEnd) {
        // only keep in memory last 'maxLength' intervals
        if(intervals.length >= maxLength) {
          intervals = intervals.slice(1);
        }

        intervals[intervals.length] = {
          begin: dateStart,
          end: dateEnd
        };
      },

      lastInterval: function() {
        if(intervals.length > 0) {
          return intervals[intervals.length - 1];
        }
        return null;
      },

      size: function() {
          return intervals.length;
      },

      toString: function() {
        if(intervals.length < 1) {
          return "[]";
        }

        var intervalString = [], begin, end, i;
        for(i = 0;i < intervals.length;i++) {
          begin = (intervals[i].begin === null) ? "null" : intervals[i].begin.toTimeString();
          end = (intervals[i].end === null) ? "unfinished" : intervals[i].end.toTimeString();
          intervalString[i] = begin + " - " + end;
        }
        return "[" + intervalString + "]";
      }
    };
  }; //END TimeIntervals

  // repeatedly checks if computer has waken up from sleep
  var watcher = (function () {
    var delta = 3000, // 3 seconds between runs
        lastTime = 0,
        active = false,
        timeoutId,
        suspendData = new TimeIntervals(10),
        suspendCallbacks = [];

    function loop() {
      var currentTime = (new Date()).getTime(),
          afterSuspend = (currentTime > (lastTime + 2 * delta)),
          i;

      if (afterSuspend) {
        suspendData.add(new Date(lastTime), new Date(currentTime));
        for(i = 0;i < suspendCallbacks.length;i++) {
          suspendCallbacks[i](suspendData.lastInterval());
        }
      }

      lastTime = currentTime;
      if (active) {
        timeoutId = setTimeout(loop, delta);
      }
    }

    return {
      init: once(function() {
        active = true;
        lastTime = (new Date()).getTime();
        timeoutId = setTimeout(loop, delta);
      }),

      stop: function() {
        // cancel timeout
        clearTimeout(timeoutId);
        // also, disable active flag, in case we are 
        // in the middle of loop function execution
        active = false;
      },

      lastSuspend: function() {
        return suspendData.lastInterval();
      },

      registerCallback: function(callback) {
        suspendCallbacks[suspendCallbacks.size] = callback;
      },

      toString: function() {
        var msg = (suspendData.size() < 1) ? 
          "There are no known suspend/wakeup cycles" : "Known suspends: " + suspendData.toString();

        return msg;
      }
    };
  }()); // END watcher

  // each client will have one Scheduler
  var Scheduler = function(index) {
    if(!(this instanceof Scheduler)) {
      return new Scheduler();
    }
    var timetable,
        active = false,
        timeoutId,
        runFn;

    function action() {
      if(active) {
        runFn.call(this, index);
      }
    }

    function nextTimeout() {
      var timeout = timetable[0];
      if(timetable.length > 1) {
        timetable = timetable.slice(1);
      }

      return timeout * 1000;
    }

    return {
      init: function(timeouts, fn) {
        if(timeouts === undefined || timeouts === null || timeouts.lentgh < 1) {
          timetable = [3, 3, 5, 5, 10]; // default timeouts in seconds
        } else {
          timetable = timeouts.slice(0);
        }
        runFn = fn;
        active = true;
      },

      scheduleNext: function() {
        if(active) {
          timeoutId = setTimeout(action, nextTimeout());
        }
      },

      cancel: function() {
        clearTimeout(timeoutId);
        active = false;
      }
    };
  }; //END Scheduler

  // object template which has to be used and extended by external clients
  var Client = function (name, connectFn) {
    // just make sure, that an instance is created even if caller
    // invoked this function without 'new' or 'Object.create'
    if(!(this instanceof Client)) {
      return new Client(name, connectFn);
    }

    var callbacks = {};

    function event(ctx, callback) {
      if(callbacks.hasOwnProperty(callback)) {
        callbacks[callback].apply(ctx, null);
      }
    }

    return {
      id: name,

      init: once(function() {
        voxioConnector.register(this);
      }),

      stop: function() {
        var prop;
        for(prop in callbacks) {
          if(callbacks.hasOwnProperty(prop)) {
            delete callbacks[prop];
          }
        }
      },

      //manager registers callbacks via this method
      listener: function(name, fn) { callbacks[name] = fn; },

      //concrete clients signal that an event has happened
      onConnected: function() { event(this, 'connected'); },
      onDisconnected: function() { event(this, 'disconnected'); },
      onError: function() { event(this, 'error'); }
    };
  }; // END Client

  function disconnectedInternal(index, endDate) {
    var c = clients[index];
    if(c.state !== clientState.DISCONNECTED) {
      if(c.state === clientState.CONNECTED) {
        c.history.lastInterval().end = endDate;
      }
      c.state = clientState.DISCONNECTED;
      c.scheduler.scheduleNext();
    }
  }

  function errorInternal(index) {
    if(clients[index].state === clientState.CONNECTED) {
      disconnectedInternal(index, new Date());
    }
    clients[index].state = clientState.DISCONNECTED; 
  }

  function connectInternal(index) {
    // first, set up a timeout to handle a client that is stuck
    clientTimeoutId = setTimeout(function() {
      if(clients[index].state === clientState.CONNECTING) {
        // oops, client wasn't able to connect in time
        try {
          clients[index].client.disconnect();
        } catch(e) {
          handleError(e);
          disconnectedInternal(index, new Date());
        }
      }
    }, clientTimeout);

    clients[index].state = clientState.CONNECTING;
    try {
      clients[index].client.connect();
    } catch(e) {
      handleError(e);
      errorInternal(index);
    }
  }

  function reconnectInternal(index) {
    clearTimeout(clientTimeoutId);
    clients[i].scheduler.cancel();
    clients[i].scheduler.init(null, connectInternal);
    connectInternal(i);
  }

  function nextInChain() {
    currentClientIndex = currentClientIndex + 1;
    if(currentClientIndex < clients.length) {
      connectInternal(currentClientIndex);
    }
  }

  return {
    init: once(function(chained) {
      active = true;
      watcher.init();
      if(clients.length > 0) {
        watcher.registerCallback(function(suspendInterval) {
          var i;
          for(i = 0;i < clients.length;i++) {
            disconnectedInternal(i, suspendInterval.begin);
            reconnectInternal(i);
          }
        });
        if(chained) {
          // the next client is initialized only after the previous is connected
          nextInChain();
        } else {
          var i;
          // initialize all clients immediately
          for(i = 0;i < clients.length;i++) {
            connectInternal(i);
          }
        }
      }
    }),

    register: function(client) {
      // test if obj conforms to prescribed structure
      if(client.connect === undefined || client.disconnect === undefined) {
        throw new TypeError("Object is missing at least one of the required methods [connect, disconnect]");
      }
      var obj = {},
          index = clients.length;

      obj.client = client;
      obj.state = clientState.DISCONNECTED;
      obj.history = new TimeIntervals(10);
      obj.scheduler = new Scheduler(index);
      obj.scheduler.init(null, connectInternal);
      obj.client.listener('connected',  function() {
        if(obj.state === clientState.CONNECTED) {
          // do nothing
          return;
        }
        clearTimeout(clientTimeoutId);
        obj.state = clientState.CONNECTED;
        obj.scheduler.cancel();
        obj.scheduler.init(null, connectInternal);
        obj.history.add(new Date(), null);
        if(currentClientIndex > -1) {
          nextInChain();
        }
      });
      obj.client.listener('disconnected', function() {
        disconnectedInternal(index, new Date());
      });
      obj.client.listener('error', function() {
        errorInternal(index);
      });
      clients[index] = obj;
      // check, if we're already initialized
      if(active === true) {
        connectInternal(index);
      }
    },

    createClient: function(id) {
      return new Client(id);
    },

    forceReconnect: function(name) {
      var i;

      for(i = 0;i < clients.length;i++) {
        if(clients[i].client.id === name) {
          if(clients[i].state !== clientState.CONNECTED) {
            try {
              clients[i].client.disconnect();
            } catch(e) {
              handleError(e);
              disconnectedInternal(i, new Date());
            }           
          }
          reconnectInternal(i);
        }
      }
    },

    stop: function() {
      var i;

      watcher.stop();
      active = false;
      for(i = 0;i < clients.length;i++) {
        clients[i].scheduler.cancel();
        clients[i].client.stop();
        try {
          clients[i].client.disconnect();
        } catch(e) {
          handleError(e);
        }
        disconnectedInternal(i, new Date());
      }
    },

    toString: function() {
      var i, msg;

      msg = "Connections manager at " + new Date().toTimeString() + ":\n";
      msg += "\t* " + watcher.toString();
      for(i = 0;i < clients.length;i++) {
        msg += "\n\t* " + clients[i].client.id + "\n" +
              "\t\tcurrent status: " + clients[i].state + "\n" +
              "\t\thistory: " + clients[i].history.toString();
      }
      return msg;
    },

    pageLoaded: function() {
      // make clients independent of each other by passing 'false' flag
      this.init(false); 
    }
  };
}());