function BaseConnector(dataQuery, callback) {
    /*
       Talk directly to Facebook to log a user in. 

       Additionaly, provide a method to retrieve data from Facebook
       via SQL (and wire in any necessary FB namespaces). A callback
       should be passed in to act upon the data after it's retrieved.

       Other classes should create one of these (instead of trying 
       to do the direct connecting/querying themselves).
    */

    // ------------
    // Methods 
    // ------------ 
    this.connect = function() {
        // present login dialog
        FB.Connect.requireSession(function() { });
    };

    this.retrieveData = function(query, callback) {
        /* Fetch data from FB, then act upon it (callback). */

        // load up facebook API
        FB_RequireFeatures(["Api"], function() {
            var api = FB.Facebook.apiClient;

            // make sure session is init'd and ready to go
            FB.Facebook.get_sessionState().waitUntilReady(function() {

                // special parser to handle an FB bug with single quotes
                FB.JSON.parse = new SpecialFacebookParser().parse;

                query = query.replace('REPLACE_UID', 
                    api.get_session().uid);

                // fetch the data
                api.fql_query(query, callback);
            });
        });
    };
}


function SpecialFacebookParser() {
    /* This code represents code given to us by Facebook. It fixes an FB
       error that occurrs when a user has single quotes in their display name.
       To make the code more manageable, it's been put it into a Class.

       As you can see, it's super gross.
    */

    // ------------
    // Attributes 
    // ------------ 
    var sfp = this;
    this.sentinelRegex = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g;


    // ------------
    // Methods 
    // ------------ 
    this.parse = function(text, reviver){
        var j;
        sfp.sentinelRegex.lastIndex = 0;

        if (sfp.sentinelRegex.test(text)) {
            text = text.replace(sfp.sentinelRegex, sfp.returnSlice);
        }

        if(/^[\],:{}\s]*$/.test(sfp.replaceText(text))) {
            j = eval('('+text+')');
            return typeof reviver==='function'? sfp.walkObject({'': j}, '') : j;
        }

        throw new SyntaxError('JSON.parse');
    };

    this.returnSlice = function(text) {
        return '\\u' + ('0000' + text.charCodeAt(0).toString(16)).slice(-4);
    };

    this.replaceText = function(text) {
        var tempText = text.replace(/\\\'/g, '\'');
        tempText = tempText.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@');
        tempText = tempText.replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']');
        return tempText.replace(/(?:^|:|,)(?:\s*\[)+/g, '');
    };

    this.walkObject = function(a, c) {
        var b, d, e = a[c];

        if (e && typeof e === 'object') {
            for (b in e) {
                if (Object.hasOwnProperty.call(e, b)) {
                    d = walk(e, b);

                    if (d !== undefined) {
                        e[b] = d;
                    } 
                    else {
                        delete e[b];
                    }
                }
            }
            return reviver.call(a, c, e);
        }
    };
}


var ConnectAPI = function() {
    /* 
       This class deals with:

            - logging a user into Facebook
            - retrieving data (via SQL queries)
                - acting upon the result
            - posting to a user's FB wall
            - sending notifications.

       This class is, however, dumb/slim. It requires the necessary
       inputs to be passed in, and will not generate thim. 
       It is meant as an interface for low level functionality.

       Other classes should instantiate one of these.
     */

    // ------------
    // Attributes 
    // ------------ 
    this.connector = undefined;


    // ------------
    // Methods
    // ------------ 

    this.init = function() {
        this.connector = new BaseConnector();
    };

    this.retrieveFacebookData = function(query, callback) {
        /* Either:
                1. user is already connected - pass
                2. pop a login screen; sign user in
           Then:
                a. Retrieve data from FB (via SQL) and act upon it (callback)
                -- OR --
                b. Call the callback (don't fetch any data)
        */
        if (!query) {
            if (callback) {
                callback();
            }
        } else {
            this.connector.retrieveData(query, callback);
        }
    };

    this.showAllFriends = function (query, callback) {
        /* Show the user's friends by contacting FB (with SQL),
           then acting upon the result (callback). The callback
           will most likely build up HTML then blast it to the screen.
        */
        this.connector.retrieveData(query, callback);
    };

    this.post = function (friendFBID, attachment, actionLinks, callback) {
        /* Post to a friends facebook wall
             - friendFBID:  If null value will post to senders wall
             - attachment:  json object containing wall post data 
             - callback:  function to execute after wall post is complete
        */
        FB.Connect.streamPublish(
                '', //String user_message  
                attachment, 
                actionLinks,  
                friendFBID,  
                'Add a message.', //String user_message_prompt
                callback,  
                false,  
                null);
    };

    this.sendNotification = function send_notification(user, message){
        // You can perform batch operations using the FB.BatchSequencer object.
        // - After creating a BatchSequencer object, it can be passed as the
        // - last argument of each individual API call. When you need to execute
        // - all batched operations, you can call the execute() method 
        // - on the FB.BatchSequencer object.
        var sequencer = new FB.BatchSequencer();

        var pendingNotificationsResult = 
            FB.Facebook.apiClient.notifications_send(user, message, sequencer);

        sequencer.execute(function() {
            //console.debug('notificationsResult from batch execution' + 
            //pendingNotificationsResult.result);
        });
    };

    this.init();

};


var CommonInterface = function(userContainer, friendContainer) {
    /* A base module that holds common functionality for Facebook
       clients (visible website modules). This is more geared towards
       being able to post to a Facebook wall, but it also has methods
       for retrieving data from Facebook, and methods to display one's 
       Facebook friends.
    */


    // ------------
    // Attributes 
    // ------------ 

    var thisRef = this;
    this.userContainer = userContainer;
    this.friendContainer = friendContainer;

    this.cAPI = undefined;
    this.post = undefined;
    this.sendNotification = undefined;

    /* Predefined SQL queries that should be used by 'child' classes (clients)
       to pass to Facebook (via ConnectAPI.retrieveData() )
            -- REPLACE_UID gets replaced with 
                    FB.Facebook.apiClient.get_session().uid
    */
    this.userQuery = 'SELECT name, birthday, pic_square_with_logo, uid ' +
        'FROM user WHERE uid=REPLACE_UID';

    this.friendsQuery = 'SELECT name, birthday, pic_square_with_logo, uid FROM user ' +
        'WHERE uid IN ' +
            '(SELECT uid2 FROM friend WHERE uid1=REPLACE_UID) ' +
        'ORDER BY name';

    this.birthdayQuery = 'SELECT name, birthday, birthday_date, pic_square_with_logo, uid ' +
        'FROM user WHERE birthday_date != "" and uid IN ' +
            '(SELECT uid2 FROM friend WHERE uid1=REPLACE_UID) ' +
        'ORDER BY birthday_date ASC';


    // ------------
    // Methods 
    // ------------ 

    this.init = function() {
        /* Create base module. 
           Assign some of the class's method's to use base.
        */
        this.cAPI = new ConnectAPI();
        this.post = this.cAPI.post;
        this.sendNotification = this.cAPI.sendNotification;
    };

    this.showConnectDialog = function() {
        /* Display the FB Connect Dialog (so user can log in)

           Normally, a client is wired up (during page load) in a way
           that defines what methods to call when a user is logged in/out.
                -- meaning that once a user connects via this dialog,
                   the 'userIsConnected' method will automagically get called.
        */
        this.cAPI.connector.connect();
    };

    this.userIsConnected = function(query, callback) {
        /* Normally, during onload, a page will try to:
            -- connect to FB
            -- retrieve friends
            -- display them

           This method is what's called to attempt the above. If a user
           is not already logged in (during page load), the 
           .userNotConnected method will be called.

           If this method is wired up during page load, and user is NOT
           currently connected, then once a user connects (via the dialog),
           this method will get automagically called.
        */
        thisRef.cAPI.retrieveFacebookData(query, callback);
    };

    this.renderUserInfo = function(user, elemID) {
        /* After a user is successfully signed into FB,
           display his/her profile picture and user name.
        */
        var userHTML = document.getElementById(elemID);

        userHTML.innerHTML = thisRef.userContainer.
            replace(/NAME/g, user[0].name).
            replace(/PICTURE/g, user[0].pic_square_with_logo);
    };

    this.showAllFriends = function(query, callback) {
        /* Retrieve a user's friends from facebook, and display them (via
           the callback).
        */
        thisRef.cAPI.showAllFriends(query, callback);
    };

    this.renderFriends = function (users, numCols, numRows) {
        /* Given a friend's users (as returned by Facebook), build up
           the necessary HTML to display them.
                -- numCols  # of columns to display
                -- numRows  # of rows to display
                                -- if empty/null, show ALL friends (but
                                    still respect numCols)
        */
        var row  = '<li class="agi-fb-thumbrow"><ul>FRIENDS_ROW</ul></li>';
        var friends = '';
        var friendRows = '';
        var numShow;

        // determine overall # of users to display
        if (!numRows) {
            numShow = users.length;
        }
        else {
            numShow = numCols * numRows;
        }

        // loop through and build up HTML for each friend (as a list)
        for(i=0; i<numShow; i++) {
            // exceeded # of friends ?
            if (i >= users.length) {
                break;
            }

            var birthday = users[i].birthday || '';
            var birthday_date = users[i].birthday_date || '';

            var name = users[i].name.replace(/'/g,"").replace(/"/g, "");
            var picSquare = users[i].pic_square_with_logo || 
                imghost + DEFAULTPIC;

            // attach user info
            friends += thisRef.friendContainer.
                replace(/NAME/g, name).
                replace(/UID/g, users[i].uid).
                replace('PICTURE', picSquare).
                replace('BIRTHDAY_FRIENDLY_NO_YEAR', this._removeYear(birthday)).
                replace('BIRTHDAY_SLASHES_NO_YEAR', this._removeYear(birthday_date));

            if ((i+1) % numCols == 0 || i+1 == numShow) {
               friendRows = friendRows + row.replace(/FRIENDS_ROW/g, friends);
               friends = '';
            }
        }
        return friendRows;
    };

    this._removeYear = function(dateString) {
        /* It is oft desired to display a user's birthday, but without the year.
           This method handles dates like '09/13/1975' and 'September 13, 1975'.
        */
        if (!dateString) {
            return '';
        }

        if (dateString.indexOf('/') > -1) {
            var dateParts = dateString.split('/');
            return dateParts[0] + '/' + dateParts[1];
        }
        else {
            return dateString.split(',')[0];
        }

        return '';

    };

    this.insertUsageRecord = function(ajaxPage, formElements) {
        /* Contact a .pd page (on our site) to insert a usage record
           into our DB. return the usage number.
        */
        var ajax = new Requester(ajaxPage, 'POST', false, false);
        var qs = ajax.formToQuery(formElements);

        //XXX: needs some serious error handling
        ajax.sendRequest(qs);
        var usageNumber = ajax.req.responseText.replace(/\\n/g, '');

        return usageNumber;
    };

    this.init();
}
