function BookingFormDataProvider() {

    this.events = {};

    this.data = {};
    this.queues = [];

    this.timeout = 5000;
    this.maxTries = 2;

    this.onTimeout = function(queueNum, actionNum, opts) {

        // request is taking longer than expected

        // see if the action still exists, it could have finished by another thread in the meantime
        if (this.queues[queueNum]['actions'][actionNum] == null) {
            return;
        }

        // see if we have any tries left
        var num_tries = this.queues[queueNum]['actions'][actionNum]['tries'];
        if (num_tries < this.maxTries) {

            // re-request it
            this.fetch(queueNum, actionNum, opts);

            // trigger timeout
            this.trigger('timeout');

        } else {

            // maximum number of tries is reached, assume service is out of ... service
            this.trigger('connectionfail');

        }
    }

    this.load = function(queueNum, actionNum, action, key, params, callback) {

        if (!(action in this.data)) {
            this.data[action] = {};
        }

        if (!(key in this.data[action])) {
            this.data[action][key] = undefined;
        }

        var obj = this.data[action][key];
        if (obj == undefined) {

            var that = this;

            var opts = {};
            opts.data = params || {};
            opts.dataType = 'json';
            opts.url = '/?module=DJO&action=' + action + '&output=json';
            opts.success = function(result) { that.updateData(action, key, result, callback); };

            this.fetch(queueNum, actionNum, opts);

            return;

        }

        this.updateData(action, key, obj, callback);

    }

    this.fetch = function(queueNum, actionNum, opts) {

        // set proper timeout
        opts.timeout = (this.maxTries - this.queues[queueNum]['actions'][actionNum]['tries']) * this.timeout;

        // increase number of tries
        this.queues[queueNum]['actions'][actionNum]['tries']++;

        // set own timeout instead of XHR timeout (we want to catch both connection timeout and server response timeout
        var that = this;
        this.queues[queueNum]['actions'][actionNum]['timeoutId'] = setTimeout(function() { that.onTimeout(queueNum, actionNum, opts); }, this.timeout);

        $.ajax(opts);
    }

    this.updateData = function(action, key, result, callback) {

        if (result) {
            this.data[action][key] = result;
        }

        if (callback) {
            callback.call(this, this.data[action][key]);
        }
    }

    this.processQueue = function(queue, thisObj, fn) {

        var queueNum = this.queues.length;

        // put queue in an object (you'll see why)
        var actions = {};
        for (var i = 0; i < queue.length; i++) {
            actions[i] = {'name' : queue[i][0], 'params' : queue[i][1], 'tries' : 0};
        }

        this.queues[queueNum] = {'results' : [], 'actions' : actions, 'callback' : fn, 'scope' : thisObj};

        var action;
        var callback;
        for (var a in actions) {

            var actionNum = Number(a);

            action = actions[a];
            callback = this[action['name']];

            if (callback instanceof Function) {
                this.addToQueue(callback, action['params'], queueNum, actionNum);
            }
        }
    }

    this.addToQueue = function(callback, args, queueNum, actionNum) {

        var that = this;

        args.push(function(result) { that.onQueueProgress(queueNum, actionNum, result); });
        args.push(queueNum);
        args.push(actionNum);

        callback.apply(this, args);
    }

    this.onQueueProgress = function(queueNum, actionNum, result) {

        // add result
        this.queues[queueNum]['results'][actionNum] = result;

        // mark this action as done
        this.queues[queueNum]['actions'][actionNum] = null;

        // see if there are more actions in the queue
        for (var a in this.queues[queueNum]['actions']) {
            if (this.queues[queueNum]['actions'][a] != null) {

               // this one still is being processed
               return;
            }
        }

        // all actions processed, call the function
        this.queues[queueNum]['callback'].apply(this.queues[queueNum]['scope'], this.queues[queueNum]['results']);

    }

    this.loadAddressInfo = function(postalcode, house_number, callback, queueNum, actionNum)
    {
        this.load(queueNum, actionNum, 'GetAddressInfo', String(postalcode) + String(house_number), {'postalcode' : postalcode, 'house_number' : house_number}, callback);
    }

    this.loadAvailableArrangements = function(tripId, callback, queueNum, actionNum)
    {
        this.load(queueNum, actionNum, 'GetAvailableArrangements', tripId, {'trip_id' : tripId}, callback);
    }

    this.loadArrangementAllotment = function(arrangementId, callback, queueNum, actionNum)
    {
        this.load(queueNum, actionNum, 'GetArrangementAllotment', arrangementId, {'arrangement_id' : arrangementId}, callback);
    }

    this.loadTripLocations = function(arrangementId, callback, queueNum, actionNum)
    {
        this.load(queueNum, actionNum, 'GetTripLocations', arrangementId, {'arrangement_id' : arrangementId}, callback);
    }

    this.loadArrangementRoomTypes = function(arrangementId, callback, queueNum, actionNum)
    {
        this.load(queueNum, actionNum, 'GetAvailableArrangementRoomTypes', arrangementId, {'arrangement_id' : arrangementId}, callback);
    }

    this.loadRoomTypeDetails = function(arrangementId, callback, queueNum, actionNum)
    {
        this.load(queueNum, actionNum, 'GetArrangementRoomTypes', arrangementId, {'arrangement_id' : arrangementId}, callback);
    }

    this.loadContactFields = function(arrangementId, callback, queueNum, actionNum)
    {
    	this.load(queueNum, actionNum, 'GetContact', arrangementId, {'arrangement_id' : arrangementId}, callback);
    }

    this.loadInsurancePrices = function(arrangementId, callback, queueNum, actionNum)
    {
        this.load(queueNum, actionNum, 'GetDjoserTripInsurancePrices', arrangementId, {'arrangement_id' : arrangementId}, callback);
    }

    this.bind = function(type, fn)
    {
        this.events[type] = fn;
    }

    this.trigger = function(type, args)
    {
        if (!(args instanceof Array)) args = [args];

        for (var a in this.events) {
            if (a === type) {
                this.events[a].apply(this, args);
            }
        }
    }
}
