/**
 * cloudtalk.js is a JavaScript API for the cloudtalk social communications API
 *
 * <p> CloudTalk.API let's you access the CloudTalk HTTP api </p>
 * <p> CloudTalk.utils are a collection of application support functions for building apps </p>
 * <p> CloudTalk.CTPlayer is a wrapper around the jquery.jplayer.js  </p>
 *
 * @name CloudTalk
 * @namespace CloudTalk
 *
 * @exports CloudTalk.API
 * @exports CloudTalk.utils
 * @exports CloudTalk.CTPlay
 *
 * @dependencies
 * @imports jquery
 * @imports jquery.tools.js
 * @imports jquery.outerHTML.js
 * @imports jquery.jplayer.js
 * @imports jquery.timago.js
 * @imports jquery.disable.text.select.js
 * @imports swfobject.js
 * @imports web_socket.js
 *
 * @author jottos@cloudtalk.com
 */

window.CloudTalk = window.CloudTalk || {};

NS_CLOUDTALK = "CloudTalk";
NS_CT_API = "CloudTalk";
NS_CT_UTILS = "CloudTalk";
NS_CT_CTPLAYER = "CloudTalk";
NS_CT_VOICES = "CloudTalk";

var replyInFocusMessage = "Enter your message here";


// TODO - jos finish up the root setting, its needed for a lot of the TODO's below
CLOUDTALK_ROOT = window.CLOUDTALK_ROOT || (function(){
    var scripts = document.getElementsByTagName("script");
    var result = window.location.protocol || "http:";
    var re = /(\w+:).*cloudtalk\.(?:min\.)js/;
    for (var i = 0; i < scripts.length; i++) {
        var match = scripts[i].src.match(re);
        if (match) {
            result = match[1] || result;
            break;
        }
    }
    return result;
})() + "//cloudtalk.me/v2/html/ct/"; // the expected place

/// base functions in the CloudTalk namespace
///
(function(nameSpace) {
    /** info log entry
     * @name log
     * @param o {object} thing to send to log
     * @exports log as CloudTalk.log
     */
    var log;
    if (typeof(console) === "object" &&
        (typeof(console.log) == "function" || typeof(console.log) == "object")) {
        log = function(o) {
            console.log(o);
        }
    } else {
        log = function(o) { }
    }

    /** error log entry
     * @name error
     * @param o {object} thing to send to log
     * @exports error as CloudTalk.error
     */
    var error;
    if (typeof(console) === "object" &&
        (typeof(console.error) == "function" || typeof(console.error) == "object")) {
        error = function(o) {
            console.error(o) ;
        }
    } else {
        error = log;
    }


    /** Exception class.
     * @name Exception
     * @exports Exception as CloudTalk.Exception
     * @constructor
     * @param message {string} exception message
     * @param type {string} exception type
     */
    function Exception(message, type) {
        if (!(this instanceof Exception))
            return new Exception(message);
        this.message = message;
        this.type = type;
    }

    // make sure toString is working
    Exception.prototype.toString = function() {
        var eType = (this.type) ? this.type+": " : "CloudTalk.Exception: ";
        return eTpyp + this.message;
    }

    window.CloudTalk = window.CloudTalk || {};
    window.CloudTalk.log = log;
    window.CloudTalk.error = error;
    window.CloudTalk.Exception = Exception;
})(window);

/// setup the CloudTalk API
///
/// API TODO
///  1. add error handler params to handle non 200 return values
/// 
(function (nameSpace) {
    var _cloudTalkProtocolVersion = '4.1';

    function API(token, userID) {
        if (!(this instanceof API))
            return new API(token);

        this.__apiRoot = "/v2/resources";
        this.__apiCalls = {
            // TODO: design review the names
            login:"/account/login",
            twitterLogout:"/account/setTwitterUser",
            createUser:"/partners/user/create",
            getMyInbox:"/conversation/getMyInbox",
            getPrivateMessages:"/conversation/getMessages",
            getPublicMessages:"/conversation/getPublicMessages",
            getPublicConversation:"/conversation/getPublicConversation",
            getPosts:"/conversation/getPublicConversations",
            getUserProfile:"/profile/getUserProfile",
            setUserProfile:"/profile/setUserProfile",
            getFollows:"/relation/getRelations",
            getGroups:"/group/getUserGroups",
            joinGroup:"/group/joinRequest",
            uploadImage:"/profile/uploadImage"
        };
        this.authToken = token;
        this.userID = userID;
    }

    API.prototype = {
        /**
         * create a fresh api request
         */
        __createApiCall:function (apiCall, handler) {
            if (apiCall in this.__apiCalls) {
                var req = new XMLHttpRequest();
                var apiURL = this.__apiRoot + this.__apiCalls[apiCall];
                req.open('POST', apiURL, true);
                req.setRequestHeader('userTicket', this.authToken);
                req.setRequestHeader('Content-Type', 'application/json');
                req.setRequestHeader('Cloudtalk-Protocol', _cloudTalkProtocolVersion);
                req.onreadystatechange = handler;
                return req;
            }
            else {
                return null;
            }
        },

        /**
         * createUser - attempt to create user with provided credentials, if successful
         * replace the current authToken, userID pair with new values
         *
         *   NOTE: caution needs to be taken to wait for new
         *   credentials to arrive from server, manage this in your
         *   callback routine
         *
         *  @param args {object} with the following required fields
         *    userName {string} alphaNum cloudtalk login name identifier
         *    password {string}
         *    email {string} valid email address for account
         *    anchors  [string,..] list of application anchor paths
         *
         *  @param handler {functionin} HTTPXmlRequest callback. Handles the response from server
         *  @param error {functionin} callback. Handles error from server and error generated by user code
         *    so there are two signatures to this handler the caller needs to check for see code
         *  @return {object} with authToken and user display name & a registration validationID
         *
         *  EG. {
         *        "userTicket":"UA1a1cdc82-86f7-49fd-b1f8-73c91550be92",
         *        "displayName":"John User",
         *        "userID":"US1a1cdc82-86f7-49fd-b1f8-73c91550be92",
         *       }
         *
         */
        createUser:function (args, handler, errorHandler) {
            var self = this;
            var req = this.__createApiCall(
                "createUser",
                function () {
                    if (req.readyState === 4) {
                        if (req.status === 200) {
                            try {
                                var createUserReturn = $.parseJSON(req.responseText);
                                self.authToken = createUserReturn.userTicket;
                                self.userID = createUserReturn.userID;
                                handler(createUserReturn);
                            } catch (error) {
                                if (errorHandler)
                                    errorHandler(error);
                                CloudTalk.error("createUser: failed with: ");
                                CloudTalk.error(error);
                            }
                        } else {
                            var createUserErrorReturn = $.parseJSON(req.responseText);
                            if (errorHandler) {
                                errorHandler(req.status, createUserErrorReturn);
                            }
                            CloudTalk.error($.format("createUser: status=%s, error=%s",
                                [req.status, createUserErrorReturn.error]));
                        }
                    }
                });
            if (args)
                req.send(JSON.stringify(args));
            else
                CloudTalk.error("createUser: no args supplied");
        },


        /**
         * login - attempt a login with credentials provided, if it
         *   succeeds, then replace the current (authToken , userID)
         *   credentials with the new ones.
         *
         *   NOTE: caution needs to be taken to wait for new
         *   credentials to arrive from server, manage this in your
         *   callback routine
         *
         *  @param args {object} with the following required fields
         *    userName {string} alphaNum cloudtalk login name identifier
         *    password {string}
         *    anchors  [string,..] list of application anchor paths
         *
         *  @param handler {functionin} HTTPXmlRequest callback. Handles the response from server
         *  @param error {functionin} callback. Handles error from server and error generated by user code
         *    so there are two signatures to this handler the caller needs to check for see code
         *
         *  @return {object} with authToken and user display name
         *
         *  EG. {
         *        "userTicket":"UA1a1cdc82-86f7-49fd-b1f8-73c91550be92",
         *        "displayName":"John User"
         *       }
         *
         * NOTE: this API call is being used to design and test error handlers
         */
        login:function (args, handler, errorHandler) {
            var self = this;
            var req = this.__createApiCall(
                "login",
                function () {
                    if (req.readyState == 4) {
                        if (req.status == 200) {
                            try {
                                var loginReturn = $.parseJSON(req.responseText);
                                self.authToken = loginReturn.userTicket;
//TODO: login does not return the userID, we'll need to make a get profile call for it				
                                self.userID = "??";
                                handler(loginReturn);
                            } catch (error) {
                                if (errorHandler)
                                    errorHandler(error);
                                CloudTalk.error("login: failed with: ");
                                CloudTalk.error(error);
                            }
                        }
                        else {
                            if (errorHandler) {
                                var loginErrorReturn = $.parseJSON(req.responseText);
                                errorHandler(req.status, loginErrorReturn);
                            }
                            CloudTalk.error("login: failed with status: " + req.status);
                        }
                    }
                });
            if (args)
                req.send(JSON.stringify(args));
            else
                CloudTalk.error("login: no args supplied");
        },


        /**
         *  twitterLogout - remove the twitterAccount association with the current user
         *
         *  @param handler {function} optional HTTPXmlRequest callback. Handles the response from server
         *  @param error {functionin} optional callback. Handles error from server and error generated by user code
         *    so there are two signatures to this handler the caller needs to check for see code
         *  @return {object} containing:
         *    on success:
         *      {
         *       "userID":"US....-88956546be5f",
         *       "status":"twitter credentials cleared"
         *       }
         *
         * NOTE: remove handler does not fail unless there was no association. As such the handlers is optional
         */
        twitterLogout:function (handler, errorHandler) {
            var self = this;
            var req = this.__createApiCall(
                "twitterLogout",
                function () {
                    if (req.readyState == 4) {
                        if (req.status == 200) {
                            try {
                                if (handler)
                                    handler($.parseJSON(req.responseText));
                            } catch (error) {
                                if (errorHandler)
                                    errorHandler(error);
                                CloudTalk.error("twitterLogout: failed with: ");
                                CloudTalk.error(error);
                            }
                        }
                        else {
                            if (errorHandler) {
                                var twitterLogoutReturn = $.parseJSON(req.responseText);
                                errorHandler(req.status, twitterLogoutReturn);
                            }
                            CloudTalk.error("twitterLogout: failed with status: " + req.status);
                        }
                    }
                });
            req.send(JSON.stringify({remove:"true"}));
        },

        /**
         *  getUserProfile - get a user profile. default profile is that of the currently logged in user
         *
         *  @param args {object} optional dictionary with a user identifier which may be one of the specified
         *    identifier types below.
         *
         *    Pass either of the following optional parameters. Optional if the requested user is the
         *    logged in user, you can pass {} and getUserProfile defaults to the current logged in user
         *    Otherwise valid user identifiers are:
         *    "userID" : "internal userID"
         *    "userName" : "unique user name"
         *    "emailAddress" : "one of the user's registered email addresses"
         *    "mobile" : " 10 digit format phone#"
         *  @param handler {functionin} HTTPXmlRequest callback. Handles the response from server
         *  @return {object} compiled from returned profile in JSON
         */
        getUserProfile:function (args, handler) {
            var req = this.__createApiCall(
                "getUserProfile",
                function () {
                    if (req.readyState == 4) {
                        if (req.status == 200) {
                            try {
                                handler($.parseJSON(req.responseText));
                            } catch (error) {
                                CloudTalk.error("getUserProfile: failed with: ");
                                CloudTalk.error(error);
                            }
                        }
                        else {
                            CloudTalk.error("getUserProfile: failed with status: " + req.status);
                        }
                    }
                });
            if (args === undefined)
                args = {};
            req.send(JSON.stringify(args));
        },


        /**
         *  setUserProfile - get a user profile. default profile is that of the currently logged in user
         *
         *  @param args {object} optional dictionary with http args to service HTTP call. supported options
         *    user_name - if given then profile for that user given
         *   {
         *      "displayName" : "The user's preferred name for display on their profile",
         *      "firstName" : "the user's first name",
         *      "lastName" : "the user's last name",
         *      "password" : "the user's password - will not be changed if empty",
         *      "emailAddress" : "List  of User's email address, e.g. ["john@doe.com", "foo@bar.com"]",
         *      "mobile" : [
         *         {
         *            "number" : "User's main mobile phone number",
         *            "mno" : "Mobile Network Operator"
         *         },...],
         *      "sex" : "User sex "male", "female"",
         *      "birthdayDate": "date of birth [yyyy-MM-dd], ex: 1970-11-23",
         *      "city" : "City name",
         *      "state" : "State name",
         *      "postalCode" : "postal code - default to US zipcode: 5 digits",
         *      "country" : { 
         *         "country" : "Country name - default: United States",
         *         "ISOcode" : "ISO 2 letter code for country - default: us"
         *      },
         *      "descriptionText": "Some text (1024 chars max)",
         *      "tags": "a JSON Array of words [alphanum and space and -_.@# accepted]"
         *          if passed all current tags will be removed and will be replaced by this list -
         *          to do specific changes use addTags and removeTags calls,
         *      "userID": "the userID of the user to change - only pass this if you have admin rights
         *          on the user's tenancy"
         *   }
         *
         *  NOTES:
         *  User to set values for defaults to current logged in user, you may specify another userID if
         *    you are logged in as a tenancy admin
         *
         *  All params are optional. Only items specified will be set, for 'password' a password must
         *    be specified or no change is made
         *
         *  MOBILE format: List of objects that have a mobile # and 'mno' (mobile network operator name)
         *          [{"4159338904", "txt.att.com"}, {"16502814440", "txt.att.com"}]
         *
         *  Returns:
         *  HTTP 204 -- No content on success
         *  HTTP 400 -- { "error" : "Invalid data" } on error
         *  HTTP 400 -- { "error" : "You are not authorized to change this user's profile" }
         *    when trying to change another user's profile without being an admin of the user's tenancy
         *    or a system user
         *
         *  @param handler {functionin} HTTPXmlRequest callback. Handles the response from server
         *  @param errorHandler {functionin} HTTPXmlRequest callback. Handles error response
         *  @return {object} compiled from returned profile in JSON
         */
        setUserProfile:function (args, handler, errorHandler) {
            var req = this.__createApiCall(
                "setUserProfile",
                function () {
                    if (req.readyState == 4) {
                        if (req.status == 204) {
                            try {
                                handler($.parseJSON(req.responseText));
                            } catch (error) {
                                if (errorHandler)
                                    errorHandler(error);
                                CloudTalk.error("setUserProfile: failed with: ");
                                CloudTalk.error(error);
                            }
                        }
                        else {
                            if (errorHandler) {
                                var setProfileErrorReturn = $.parseJSON(req.responseText);
                                errorHandler(req.status, setProfileErrorReturn);
                            }
                            CloudTalk.error($.format("setUserProfile: failed with status: %s, message: %s",
                                [req.status, req.responseText]));
                        }
                    }
                });
            if (args === undefined)
                args = {};
            req.send(JSON.stringify(args));
        },

        /**
         * TODO: jos, either fix this to do something or remove it. this code as is cannot work for parts
         *  that come off of disk (eg, audio or pics)
         *
         * uploadUserAvatar
         *
         */
        uploadUserAvatar:function (data, handler) {
            // Create te ajax request
            var req = this.__createApiCall(
                "uploadImage",
                function () {
                    if (req.readyState == 4) {
                        if (req.status == 200) {
                            try {
                                handler($.parseJSON(req.responseText));
                            } catch (error) {
                                CloudTalk.error("getInbox: failed with");
                                CloudTalk.error(error);
                            }
                        } else {
                            CloudTalk.error("getInbox: failed with status: " + req.status);
                        }
                    }
                }
            );

            // Parse the data to the multipart
            var boundary = Math.random().toString().substr(2);
            xml.setRequestHeader("content-type",
                "multipart/form-data; charset=utf-8; boundary=" + boundary);
            for (var key in data) {
                multipart += "--" + boundary
                    + "\r\nContent-Disposition: form-data; name=" + key
                    + "\r\nContent-type: application/octet-stream"
                    + "\r\n\r\n" + data[key] + "\r\n";
            }
            multipart += "--" + boundary + "--\r\n";

            req.send(multipart);
        },

        /**
         *  getInbox - get the user's inbox
         *
         *  @param handler {function} HTTPXmlRequest callback. Handles the response from server
         *  @param args {object} optional dictionary with http args to service HTTP call. supported options:
         *    startingDate {string} 'now' or string date in the format of 'yyyy-MM-dd'T'HH:mm:ssZ'
         *       from which to start the search
         *    searchDirection {string} 'forward' or 'backward'
         *    conversationCount {positive integer or string} 'all' - how many items to retrieve
         *    messageCount {positive integer or string} - positive number, string 'all', or 'none'
         *    unheardMessageCount {boolean} defaults to false, uses more resources to get counts
         *
         *  @return {object} feed cursor {
         *                                 'conversations' : [ convos...],
         *                                 'conversationTotal' : someNum,
         *                                 'nextStartingDate' : dateFromWhichToStartNextSearch
         *                                }
         *
         *  in JSON format note that the nextStartingDate is dependent on the direction of search
         *
         */
        getInbox:function (args, handler) {
            var req = this.__createApiCall(
                "getMyInbox",
                function () {
                    if (req.readyState == 4) {
                        if (req.status == 200) {
                            try {
                                handler($.parseJSON(req.responseText));
                            } catch (error) {
                                CloudTalk.error("getInbox: failed with");
                                CloudTalk.error(error);
                            }
                        }
                        else {
                            CloudTalk.error("getInbox: failed with status: " + req.status);
                        }
                    }
                });
            if (args === undefined)
                args = {};
            req.send(JSON.stringify(args));
        },

        /**
         *  getMessages - get messages of a single conversation
         *
         *  @param handler {function} HTTPXmlRequest callback. Handles the response from server
         *  @param args {object} dictionary with http args to service HTTP call. supported options:
         *    conversationID {string} required ID of the conversation
         *    startingDate {string} 'now' or string date in the format of 'yyyy-MM-dd'T'HH:mm:ssZ'
         *       from which to start the search
         *    searchDirection {string} 'forward' or 'backward'
         *    messageCount {positive integer or string} - positive number or  string 'all'
         *
         *  EG. {
         *        conversationID : _currentConversationID,
         *        startingDate : "now",
         *        searchDirection : "backward",
         *        messageCount : 10
         *      }
         *
         *  @return feed cursor {
         *                        'messages' : [ message, ...],
         *                        'messageTotal' : someNum,
         *                        'nextStartingDate' : dateFromWhichToStartNextSearch
         *                       }
         *
         *  in JSON format note that the nextStartingDate is dependent on the direction of search
         *
         */
        getMessages:function (args, handler) {
            var convoID = args.conversationID;
            if (convoID === undefined) {
                throw new CloudTalk.Exception("getMessages requires conversationID", "CloudTalkAPI");
            }
            // private convos start with PRC, public with PUC
            apiCall = (/^PRC/.test(convoID)) ? "getPrivateMessages" : "getPublicMessages";
            var req = this.__createApiCall(
                apiCall,
                function () {
                    if (req.readyState == 4) {
                        if (req.status == 200) {
                            try {
                                handler($.parseJSON(req.responseText));
                            } catch (error) {
                                CloudTalk.error("getMessages: failed with:");
                                CloudTalk.error(error);
                            }
                        }
                        else {
                            CloudTalk.error("getMessages: failed with status:" + req.status);
                        }
                    }
                });
            if (args === undefined)
                args = {};
            req.send(JSON.stringify(args));
        },


        /**
         *  getPublicConversation - get a single top level public conversation object
         *
         *  @param args {object} dictionary with http args to service HTTP call. supported options:
         *    conversationID {string} required ID of the conversation
         *    addGroupDetails {optional boolean} add additional details about the groups message is posted to
         *  @param handler {function} HTTPXmlRequest callback. Handles the response from server
         *
         *  EG. {
         *        conversationID : _currentConversationID,
         *        addGroupDetails : "true",
         *      }
         *
         *  @return object with a conversation object hanging off of "conversation" field e.g.
         * {
         *   "conversation": {
         *     "contactImageHash": "424A125933AB433D25B4DB5F4895955C",
         *     "contactImageURL": "http://integration.cloudtalk.me/....-a629-8c1b7b91cfb7/IMAGE-noname_profile.jpg",
         *     "conversationID": "PUCd865bd63-a88d-4d02-9fcc-f54f2a5ddc81",
         *     "creationDate": "2011-10-11T04:33:28.032",
         *     "displayName": "Lester del Rey",
         *     "firstMessageMediaContent": [
         *       {
         *       "caption": "Hey yo!! Testing twitter out ",
         *       "file": "http://integration.cloudtalk.me/v2....45ee-8df8-239fb24e31c8---public_auth",
         *       "type": "text"
         *       } ],
         *      "firstMessageText": "Hey yo!! Testing twitter out ",
         *      "firstMessageURL": "http://integration.cloudtalk...-8df8-239fb24e31c8---public_auth",
         *      "isClosed": "false",
         *      "likeCount": "0",
         *      "localTime": "Mon Oct 10 21:33:28 PDT 2011",
         *      "messageCount": "22",
         *      "noticeImageHash": "4FC1BCE04CB0DB5CF0A41F2EDEFBAC3A",
         *      "noticeImageURL": "http://integration.cloudtalk.me...-8c1b7b91cfb7/IMAGE-noname_notice.jpg",
         *      "notify": "false",
         *      "profileImageHash": "BE596E3416CDCF4F171E8F54326AD03C",
         *      "profileImageURL": "http://integration.cloudtalk...-8c1b7b91cfb7/IMAGE-noname_profile.jpg",
         *      "streamID": "GR2286d37c-d753-4477-a629-8c1b7b91cfb7",
         *      "subject": "Hey yo!! Testing twitter out ",
         *      "userID": "US2286d37c-d753-4477-a629-8c1b7b91cfb7",
         *      "userName": "jottos"
         *    }
         * }
         *
         */
        getPublicConversation:function (args, handler) {
            if (args.conversationID === undefined) {
                throw new CloudTalk.Exception("getPublicMessage requires conversationID", "CloudTalkAPI");
            }
            // private convos start with PRC, public with PUC
            var req = this.__createApiCall(
                "getPublicConversation",
                function () {
                    if (req.readyState == 4) {
                        if (req.status == 200) {
                            try {
                                handler($.parseJSON(req.responseText));
                            } catch (error) {
                                CloudTalk.error("getPublicConversation: failed with:");
                                CloudTalk.error(error);
                            }
                        }
                        else {
                            CloudTalk.error("getPublicConversation: failed with status:" + req.status);
                        }
                    }
                });
            if (args === undefined)
                args = {};
            req.send(JSON.stringify(args));
        },

        /**
         *  getFollows - get list of follows for the logged in user
         *
         *  @param args {object} - currently there are no accepted args, any nill value is acceptable
         *  @param handler {function} HTTPXmlRequest callback. Handles the response from server
         *
         *  @return array of userInfos e.g.
         *    [
         *      {
         *        displayName: "John Linney",
         *        profileImageHash: "28CAB25BE77BB198526BADB9633F804C",
         *        profileImageURL: "http://../IMAGE-noname_profile.jpg",
         *        streamID: "GRR7d841a50-901f-4843-bc49-e7b93c79a2c4",
         *        userID: "US48e935d4-ca67-4a83-96bc-80f78ccffd6e",
         *        userName: "jflinney"
         *      },
         *      ...
         *    ]
         *  in JSON format note that the nextStartingDate is dependent on the direction of search
         *
         */
        getFollows:function (args, handler) {
            var req = this.__createApiCall(
                "getFollows",
                function () {
                    if (req.readyState == 4) {
                        if (req.status == 200) {
                            try {
                                var rawData = $.parseJSON(req.responseText);
                                var followedUsers = [];
                                // quick transform to our internal needs
                                $.each(rawData, function (idx, rawUser) {
                                    followedUsers.push(
                                        { displayName:rawUser.displayName,
                                            profileImageHash:rawUser.profileImageHash,
                                            profileImageURL:rawUser.profileImageURL,
                                            streamID:rawUser.streamID,
                                            userID:rawUser.userID,
                                            userName:rawUser.userName
                                        })
                                });
                                handler(followedUsers);
                            } catch (error) {
                                CloudTalk.error("getFollows: failed with");
                                CloudTalk.error(error);
                            }
                        }
                        else {
                            CloudTalk.error("getFollows: failed with status: " + req.status);
                        }
                    }
                });
            // for now, we will set the args appropriately, if there are any options we'll add later
            args = { 'relationshipType':['follow'] }
            req.send(JSON.stringify(args));
        },


        /**
         *  getGroups - get list of groups the logged in user or specified user is a member of
         *  NOTE: we're doing agressive filtering to remove any groups the curernt user is owner of
         *
         *  @param args {object} - currently {} or { userID : "US....ID STRING"} are the only accepted forms
         *      {} will return the groups current logged in user, specifying a userID will get the group
         *      memberships of that user this will change at some point to deal with the filtering
         *  @param handler {function} HTTPXmlRequest callback. Handles the response from server
         *
         *  @return array of group membership info e.g.
         *    [
         *      {
         *        groupID:"GRR2579a2de-25a4-496e-96b3-005728ac0a66",
         *        groupRole:"contributor",
         *        displayName:"joe's garage",
         *        groupName:"joes_garage",
         *        groupMembersCount:"704",
         *        descriptionText:"my feed rocks",
         *        profileImageURL:"http:...-noname_profile.jpg",
         *        profileImageHash:"23BED576A24132643BDEE2C6D88CC06F",
         *        createdOn:"2011-02-02T20:37:49.504",
         *        astUpdatedOn:"2011-12-06T04:14:33.043",
         *        groupAccessType:"public",
         *        groupOwner:{groupOwnerUserID:"US5f60c4e2-4823-45f1-b06f-c173d1ceac17",
         *                  userName:"joe",
         *                  displayName:"joe the garage owner",
         *                  profileImageURL:"http:.../IMAGE-noname_profile.jpg",
         *                  profileImageHash:"721711680D58047E90DC837A2EDA713C",
         *                  sex:"notSpecified"}
         *        },
         *      ...
         *    ]
         *  in JSON format note that the nextStartingDate is dependent on the direction of search
         *
         */
        getGroups:function (args, handler) {
            // TODO: let's put arg checking functions on todo list...
            if ($.isEmptyObject(args))
                args = {}; // just to be safe
            else if (typeof(args) === "object" && "userID" in args)
                ; // we're ok
            else
                throw new CloudTalk.Exception("getGroups requires {} or { userID : 'US...xxx'} forms", "CloudTalkAPI");

            var req = this.__createApiCall(
                "getGroups",
                function () {
                    if (req.readyState == 4) {
                        if (req.status == 200) {
                            try {
                                var rawData = $.parseJSON(req.responseText);
                                var groups = [];
                                // quick transform to our internal needs
                                $.each(rawData.userGroups, function (idx, rawGroup) {
                                    groups.push(
                                        {
                                            "groupID":rawGroup.groupID,
                                            "groupRole":rawGroup.groupRole,
                                            "displayName":rawGroup.displayName,
                                            "groupName":rawGroup.groupName,
                                            "groupMembersCount":rawGroup.groupMembersCount,
                                            "descriptionText":rawGroup.descriptionText,
                                            "profileImageURL":rawGroup.profileImageURL,
                                            "profileImageHash":rawGroup.profileImageHash,
                                            "createdOn":rawGroup.createdOn,
                                            "lastUpdatedOn":rawGroup.lastUpdatedOn,
                                            "groupAccessType":rawGroup.groupAccessType,
                                            "groupOwner":{groupOwnerUserID:rawGroup.groupOwner,
                                                userName:rawGroup.groupOwner.userName,
                                                displayName:rawGroup.groupOwner.displayName,
                                                profileImageURL:rawGroup.groupOwner.profileImageURL,
                                                profileImageHash:rawGroup.groupOwner.profileImageHash,
                                                sex:rawGroup.groupOwner.sex
                                            }
                                        })
                                });
                                handler(groups);
                            } catch (error) {
                                CloudTalk.error("getGroups: failed with");
                                CloudTalk.error(error);
                            }
                        }
                        else {
                            CloudTalk.error("getGroups: failed with status: " + req.status);
                        }
                    }
                });
            req.send(JSON.stringify(args));
        },

        /**
         *  joinGroup - request membership of the currently logged in user to the group specified
         *
         *  @param args {object} - specifies the group to join and optional control params
         *
         * {
         *    "groupID" : required unique group ID of requested group
         *    "groupRole" : the user's role to join the group as [viewer/follower/contributor/moderator/admin]"
         *       (default: group's default join role)
         *    "source": the source application [alphanum and -_. and space accepted] - 32 chars Max" (default: pana.ma)
         * }
         *
         *  @return HTTP 200 -- on success - With the following JSON Object
         *     {
         *        "groupID" : "unique group identifier",
         *        "userID" : "the userID of the currentUser"
         *     }
         *
         *          HTTP 400 -- { "error" : "[specific error message]" } on error
         *
         *
         */
        joinGroup:function (args, handler, errorHandler) {
            if ($.isEmptyObject(args))
                throw new CloudTalk.Exception("joinGroup requires { groupID: 'GR...ID' }", "CloudTalkAPI");
            else if (typeof(args) === "object" && "groupID" in args)
                ; // we're ok
            // TODO - finish additional arg checking

            var req = this.__createApiCall(
                "joinGroup",
                function () {
                    if (req.readyState == 4) {
                        if (req.status == 200) {
                            try {
                                handler($.parseJSON(req.responseText));
                            } catch (error) {
                                CloudTalk.error("joinGroup: failed with");
                                CloudTalk.error(error);
                            }
                        }
                        else {
                            if (errorHandler) {
                                errorHandler(req.status, $.parseJSON(req.responseText));
                            }

                            CloudTalk.error("joinGroup: failed with status: " + req.status);
                        }
                    }
                });
            req.send(JSON.stringify(args));
        },

        /**
         *  getPosts - request the posts for a particular user's group
         *
         *  @param args {object} - specifies the user group to get posts for and the maximum count
         *
         * {
         *    "streamID" : groupID of user's stream
         *    "conversationCount" : max # to return
         * }
         *
         *  @return HTTP 200 -- on success - With the following JSON Object
         *     {
         *       ... tbd
         *     }
         *
         *          HTTP 400 -- { "error" : "[specific error message]" } on error
         *
         *
         */
        getPosts:function (args, handler) {
            // TODO: let's put arg checking functions on todo list...
            if ($.isEmptyObject(args))
                throw new CloudTalk.Exception("getPosts requires {} or { userID : 'US...xxx'} forms", "CloudTalkAPI");
            else if (typeof(args) === "object" && "streamID" in args) {
                // we're ok, just reformat args for the actual call
                args.groups = [args.streamID];
                delete args.streamID;
            }
            else {
                throw new CloudTalk.Exception("getPosts requires missing streamID", "CloudTalkAPI");
            }

            var req = this.__createApiCall(
                "getPosts",
                function () {
                    if (req.readyState == 4) {
                        if (req.status == 200) {
                            try {
                                handler($.parseJSON(req.responseText));
                            } catch (error) {
                                CloudTalk.error("getPosts: failed with");
                                CloudTalk.error(error);
                            }
                        }
                        else {
                            CloudTalk.error("getPosts: failed with status: " + req.status);
                        }
                    }
                });
            req.send(JSON.stringify(args));
        }
    };

    nameSpace.API = API;
    nameSpace.APIProtocolVersion = _cloudTalkProtocolVersion;
})(window[window.NS_CT_API] || window);

/// CloudTalk API utilities; these function operate on the parameter and return
/// payloads of the API
/// 
(function (nameSpace) {
    var utils;
    utils = {

        /**
         *  get and trim the list of participants, the trim length is
         *  dependent on the width of the item cells
         *
         *  @param convo {object} conversation object
         *
         *  TODO: need to parameterize the trim length in some way
         *   so we don't need the magic #s here, this might include
         *   some CSS tweeks
         */
        getParticipants:function (convo) {
            var participants = "";
            if ("participants" in convo) {
                var numParticipants = convo.participants.length;
                $.each(convo.participants,
                    function (idx, participant) {
                        participants += participant.participantDisplayName;
                        if (participants.length > 34) {
                            participants = participants.substring(0, 32) + "...";
                            return false; //breaks loop
                        }
                        else if ((idx + 1) < numParticipants) {
                            participants += ", ";
                        }
                    });
            }

            return participants;
        },

        /**
         *  @param convoOrMessage {object} conversation or message object
         */
        getUserAvatar:function (convoOrMessage) {
            if ("conversationCategory" in convoOrMessage)
                return utils.fixResourceURL(convoOrMessage.profileImageURL);
            else
                return utils.fixResourceURL(convoOrMessage.sender.profileImageURL);
        },

        getParticipantAvatar:function (convo) {
            return utils.fixResourceURL(convo.profileImageURL);
        },

        /**
         *  find the sender of the last message in the convo,
         *  if it is the current user (logged in user) then return
         *  'me' otherwise the displayName of the sender.
         *  if for some reason the last message cannot be found then
         *  return 'me' as well (is this a real case?)
         *
         *  @param convo {object} conversation object
         */
        getLastSender:function (convo) {
            if ("messages" in convo) {
                var last = convo.messages[convo.messages.length - 1];
                var displayName = last.sender.senderDisplayName;
                var senderName = (displayName) ? displayName : last.sender.senderName;
                // TODO: _currentUser not yet set
                return (last.sender.senderName == _currentUser) ?
                    "Me" : senderName;
            }
            else {
                return "Me";
            }
        },

        /**
         *  get the sender of the given conversation or message, for convo it is
         *  the conversation originator
         *
         *  @param convoOrMessage {object} conversation or message object
         */
        getSender:function (convoOrMessage) {
            if ("conversationCategory" in convoOrMessage) {
                return utils.getLastSender(convoOrMessage);
            }
            else {
                var displayName = convoOrMessage.sender.senderDisplayName;
                return (displayName) ? displayName : convoOrMessage.sender.senderName;
                ;
            }
        },

        /**
         *  get the image associated with the message
         *
         *  TODO: currently we assume only one mediaContent and look there,
         *  we don't handle text + image, voice + image, text + voice + image
         *  or multiple images (not supported yet). we need to improve
         *  this accessor to go hunting for the image
         *
         *  @param {message} object
         *  @message {integer} size
         */
        getMessageImage:function (message, size) {
            var url = message.mediaContent[0].file;

            if (size) {
                url += ("?size=" + size);
            }

            return utils.fixResourceURL(url);
        },

        // TODO: the next series of methods only check mediaContent[0]  so
        // text + audtio won't work, image + audio may not work, video + audio
        // may not work (the first piece of content) at some point we need to inspect the entire
        // array and come up with a precidence funtion for what type a message
        // is. Also, we may need alternative functions that get a particular
        // mime type for a message returning 'undefined' if it doesn't contain
        // that type
        //
        // OR... we just remove them because I think we'll not need them, jos
        /**
         *  return the requested media attribute associated with the message
         *
         *  @param {message} object
         */
        getMessageAudio:function (message) {
            return utils.fixResourceURL(message.mediaContent[0].file);
        },

        getMessageText:function (convoOrMessage) {
            if ("conversationCategory" in convoOrMessage) {
                if ("lastMessageText" in convoOrMessage)
                    return convoOrMessage.lastMessageText;
                else
                    return "(think it's an audio)";
            }
            else {
                var type = utils.getMessageType(convoOrMessage);
                if (type == "text")
                    return convoOrMessage.mediaContent[0].caption;
                else if (type == "audio")
                    return "Audio Message";
                else if (type == "video")
                    return "Video Message";
                else if (type == "image")
                    return "Photo Message";
            }
        },

        getMessageType:function (message) {
            return message.mediaContent[0].type;
        },

        getMessageDuration:function (message) {
            return message.mediaContent[0].duration;
        },

        /**
         * shorten text provided to specified len ending it in elipses
         */
        shortenText:function (text, maxLen) {
            return (text.length > maxLen) ? text.substring(0, maxLen - 2) + "..." : text;
        },

        /**
         * remove this at some point????
         * hack needed just to run locally so that static resources are redirected to external server
         */
        fixResourceURL:function (url) {
            if (/mov$/.test(url)) {
                url = url.replace(".mov", ".m4v");
            }

            if (url.search("pulp") > 0)
                return url.replace(
                    "http://pulp.local:8084",
                    "http://integration.cloudtalk.me");
            else if (url.search("localhost:8080") > 0)
                return url.replace(
                    "http://localhost:8080",
                    "http://integration.cloudtalk.me");
            else if (url.search("localhost/dmitriytemesov") > 0)
                return url.replace(
                    "http://localhost/dmitriytemesov",
                    "http://integration.cloudtalk.me/integration");
            else if (url.search("localhost/jos") > 0)
                return url.replace(
                    "http://localhost/jos",
                    "http://integration.cloudtalk.me/integration");
            else return url;
        },

        /** we mean UTC ISO date string
         */
        ISODateString:function (d) {
            function pad(n) {
                return n < 10 ? "0" + n : n
            }

            if (d === undefined)
                d = new Date();

            return d.getUTCFullYear() + "-"
                + pad(d.getUTCMonth() + 1) + "-"
                + pad(d.getUTCDate()) + "T"
                + pad(d.getUTCHours()) + ":"
                + pad(d.getUTCMinutes()) + ":"
                + pad(d.getUTCSeconds()) + "-0000";
        },

        /** not all browsers behave correctly on parsing date strings
         *  so we do it the nasty long way here
         */
        safeDateFromDateString: function(dateString) {
            var dateParts = dateString.split(/[-T: ]/);
            // dateParts[1] - 1 because it's a zero based month index
            var date = new Date(dateParts[0], dateParts[1] - 1, dateParts[2], dateParts[3], dateParts[4], dateParts[5]);
            // now adjust for the local timezone
            var tzOffset = date.getTimezoneOffset();
            var miliDate = date.getTime() - (tzOffset * 60 * 1000);
            return new Date(miliDate);
        },


        /*
         * return clientWidth and clientHeight for a give html element
         *
         * param width {integer} number of pixels wide the actual recieving
         *    container will be. no default, failure to specify will cause exception
         * @return {object} with height and width attributes
         */
        textMetrics:function (el, width) {
            var h = 0, w = 0;
            var div = document.createElement("div");

            styles = {
                position:"absolute",
                left:-1000,
                top:-1000,
                display:"none"
            };

            if (width !== undefined)
                styles.width = width;

            document.body.appendChild(div);
            $(div).css(styles);
            $(div).html($(el).html());
            var styles = ["font-size", "font-style", "font-weight", "font-family", "line-height", "text-transform", "letter-spacing"];
            $(styles).each(function () {
                var s = this.toString();
                $(div).css(s, $(el).css(s));
            });

            h = $(div).outerHeight();
            w = $(div).outerWidth();

            $(div).remove();

            return { height:h, width:w };
        },

        /**
         * convert a # of second to a human understandable string values
         *
         * @param seconds {positive integer}
         * @return {string}
         */
        seconds2time:function (seconds) {
            var hours = Math.floor(seconds / 3600);
            var minutes = Math.floor((seconds - (hours * 3600)) / 60);
            var seconds = seconds - (hours * 3600) - (minutes * 60);
            var time = "";

            if (hours != 0) {
                time = hours + ":";
            }
            if (minutes != 0 || time !== "") {
                minutes = (minutes < 10 && time !== "") ? "0" + minutes : String(minutes);
                time += minutes + ":";
            }
            if (time === "") {
                time = seconds + "s";
            }
            else {
                time += (seconds < 10) ? "0" + seconds : String(seconds);
            }
            return time;
        },

        /** from quirksmode simple cookie manipulation for now
         *  always assigns to root path /
         *  cookie format is : name=value; path=/
         *                  or name=value; expires=<date>; path=/
         *
         * NOTES: secure can only be used if your scheme is HTTPS and httponly should probably be on all the time
         */
        createCookie:function (name, value, options) {
            var expires = "";
            var flags = "";
            var semiColon = String.fromCharCode(59);
            var forwardSlash = String.fromCharCode(47);
            if (options === undefined) {
                options = {}
            }
            if (options["delete"]) {
                expires = semiColon + " expires=Thu, 01-Jan-1970 00:00:01 GMT";
            }
            else {
                if (options.days) {
                    var date = new Date();
                    date.setTime(date.getTime() + (options.days * 24 * 60 * 60 * 1000));
                    expires = semiColon + " expires=" + date.toGMTString();
                }
                if (options.secure) {
                    flags += semiColon + " secure"
                }
                if (options.httpOnly) {
                    flags += semiColon + " HttpOnly"
                }
            }
            document.cookie = name + "=" + value + expires + semiColon + " path=" + forwardSlash + flags;
        },

        readCookie:function (name) {
            var nameEQ = name + "=";
            var ca = document.cookie.split(';');
            for (var i = 0; i < ca.length; i++) {
                var c = ca[i];
                while (c.charAt(0) == ' ') c = c.substring(1, c.length);
                if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length, c.length);
            }
            return null;
        },

        deleteCookie:function (name) {
            this.createCookie(name, "", {"delete":true});
        },


        /**
         * provided a conversation object returned from the getPublicConversation API call,
         * reformat it as a message object so it can be rendered
         *
         * @param {object} conversation object
         * @return {object} a message object}
         *
         * TODO: the messageID is not part of the conversation object, but can be gouged out
         *   of several of the convo fields, not needed right now so whoever inherits this...
         *   also, we really don't have valid headStatus and likeStatus fields to transport
         */
        conversation2Message:function (co) {
            return {
                messageID:"unknown",
                messageCategory:co.messageCategory,
                conversationID:co.conversationID,
                localTime:co.localTime,
                creationDate:co.creationDate,
                sender:{
                    senderID:co.userID,
                    senderName:co.userName,
                    senderDisplayName:co.displayName,
                    profileImageURL:co.profileImageURL,
                    profileImageHash:co.profileImageHash
                },
                streamID:co.streamID,
                mediaContent:co.firstMessageMediaContent,
                heardStatus:true,
                likeStatus:"neutral"
            }
        }

    };

    nameSpace.utils = utils;
})(window[window.NS_CT_UTILS] || window);


(function (nameSpace) {
    /**
     *  CTPLayer - a wrapper class to manage the jplayer jquery plugin.
     *  This class imposes a strategy of only allowing one active instance
     *  at any time. This is done to assure correct support on iPad/iPhone/iPod
     *  platforms which currently only allow a single audio tag and hence
     *  cannot support more than a single instance.
     *
     *  The practical aspect of this strategy, is that the caller of this object
     *  needs to instantiate the jplayer instance before playing and that instantiating
     *  an instance if one already exists, will destroy the current instance first.
     */
    var _parkedStyle = {
        position:"absolute",
        left:-1000,
        top:-1000,
        height:"0px",
        width:"0px"
    };

    function CTPlayer(containerID) {
        /** instantiate a CloudTalk jplayer controller

         @param containerID {string}  optional name of element where the player
         is instantiated. if not provided a div will be created and appended to body
         @return {boolean}  false if instantiation failed
         */
        if (!(this instanceof CTPlayer))
            return new CTPlayer(containerID);

        if (containerID === undefined) {
            // default name will be 'jplayermechanics'
            // class is required to be 'jp-jplayer'

            var jplayer = $("<div id='jplayerMechanics' class='jp-jplayer'></div>");
            $("body").append(jplayer);
            jplayer.css(_parkedStyle);
            this.__jplayerContainer = "#jplayerMechanics";
        } else {
            // assuming the caller is doing the setup
            this.__jplayerContainer = "#" + containerID;
        }
        this.__isInstantiated = false;
        this.__selectorAncestor = "";
        this.__showWarnings = false;
        this.__showErrors = true;
        this.__showMiscEvents = true;

        // debug console for jplayer
        if (CloudTalk._debug && jQuery.jPlayerInspector) {
            $("#jplayer_inspector").jPlayerInspector({jPlayer:$(this.__jplayerContainer)});
        }
    }

    CTPlayer.prototype = {
        /**
         * put the jplayer into its hidden 'parking' location
         */
        parkPlayer:function () {
            var jplayerContainer = $(this.__jplayerContainer);
            if (!jplayerContainer.parent().is("body")) {
                // we need to move it back
                jplayerContainer.detach().appendTo("body");
            }
            jplayerContainer.css(_parkedStyle);
        },


        /**
         * move player to specified position

         * @param left {positive integer} positive integer representing offset from
         *   left of window, if 0 supplied in fixed mode, player is centered in
         *   window
         * @param top {positive integer or string } positive integer
         *   representing offset from top of provided div inThisDiv, if string,
         *   string is % of distance from top of visible browser window. if 0 given
         *   in fixed mode player is centered vertically
         * @param width {positive integer} width of display area
         * @param height {positive integer} height of display area
         * @param inThisDiv { jquery or jquery selector string } if supplied must point to div
         *   that will contain the player, div is assumed to be empty and be correctly sized
         *
         *  TODO: check that top and left can be specified in % units eg. 10%, if works update docs
         *   and remove the % match check in code
         */
        showPlayerAt:function (top, left, width, height, inThisDiv) {
            var position = "fixed";
            var borderRadius = "none";
            var mozBoxShadow = "none";
            var webkitBoxShadow = "none";

            inThisDiv = $(inThisDiv);

            // if the caller presents us with a valid div to display player in,
            // then detach from the body and move
            if (inThisDiv.length > 0) {
                $(this.__jplayerContainer).detach().appendTo(inThisDiv);
                position = "relative";
            } else {
                borderRadius = "10px";
                mozBoxShadow = "0 0 90px 5px black";
                webkitBoxShadow = "0 0 90px black";
            }

            // absolut screen positiontion
            if (position === "fixed") {
                if (left === 0) {
                    left = ($(window).width() - width) / 2;
                }

                if (top === 0) {
                    top = ($(window).height() - height) / 2;
                } else if (/%$/.test(String(top))) {
                    top = ($(window).height() - height) * (parseInt(top) / 100);
                }
            }

            $(this.__jplayerContainer).css({
                "-moz-box-shadow":mozBoxShadow,
                "-webkit-box-shadow":webkitBoxShadow,
                "border-radius":borderRadius,
                position:position,
                top:top,
                left:left,
                height:height + "px",
                width:width + "px"
            }).jPlayer("option", "size", { width:width, height:height});
        },


        /** instantiate a jplayer pointing at controls at the specified element,
         *   kill any current player if needed if permission is granted.
         *
         *  There is expected to be an element with ID=playerPlayButton under the
         *  controlsID. This button will wired to start playback
         *
         *  @param controlsID {string} name of element where the player is instantiated
         *  @param killCurrentIfNeeded {boolean} permision to kill existing player if
         *         needed. if permision is not given then the instantiation will fail if
         *         a player instance already exists
         *
         *  @return {boolean} false if instantiation failed
         */
        instantiateWith:function (controlsID, playbackHandler, killCurrentIfNeeded) {
            if (this.isInstantiated()) {
                if (killCurrentIfNeeded) {
                    this.destroyCurrentJplayer();
                } else {
                    CloudTalk.log("CTPlayer: failed to instantiate new CTPlayer.jplayer because one exists and we don't have permission to kill it");
                    return false;
                }
            }

            var self = this; // hold onto reference to this for the event handlers
            this.__playbackHandler = playbackHandler;
            this.__selectorAncestor = "#" + controlsID;
            this.bringUpControls();
            this.__isInstantiated = true;
            this.__currentMedia = null;
            $(this.__jplayerContainer).jPlayer({
                cssSelectorAncestor:this.__selectorAncestor,
                swfPath:CloudTalk.params.flashBaseUrl,
                wmode:"window",
                supplied:"mp3,m4v",

                cssSelector:{
                    //play: '.jp-play',
                    //pause: '.jp-pause',
                    //stop: '.jp-stop',
                    seekBar:".jp-seek-bar",
                    playBar:".jp-play-bar",
                    videoPlay:".jp-video-play"
                },

                // this makes Safari 5 audio seeking and progress bar happy
                preload: "auto",

                // add this in when we start testing with flash
                //solution: "flash,html",

                /// event handlers for the jplayer instance 
                /// !! in these event handlers, if you need to access the enclosing 
                ///    CTPlayer instance you need to use the self variable that was
                ///    bound above
                ready:function () {
                    CloudTalk.log($.format("Jplayer: ready event handler; playPending=%s, current media=%s"
                        [self.__playPending, JSON.stringify(self.__currentMedia)]));
                    self.__playerReady = true;
                    if (self.__playPending) {
                        self.__playPending = false;
                        if (self.__needToSetMedia) {
                            self.__needToSetMedia = false;
                            $(self.__jplayerContainer).jPlayer("setMedia", self.__currentMedia);
                        }
                        self.play();
                    }
                },

                error:function (event) {
                    self.logJplayerEvent(event);
                    if (self.__playbackHandler.error)
                        self.__playbackHandler.error();
                },

                warning:function (event) {
                    self.logJplayerEvent(event);
                    if (self.__playbackHandler.warning)
                        self.__playbackHandler.warning();
                },

                ended:function (event) {
                    self.logJplayerEvent(event);
                    if (self.__playbackHandler.ended)
                        self.__playbackHandler.ended();
                },

                play:function (event) {
                    self.logJplayerEvent(event);
                    if (self.__playbackHandler.play)
                        self.__playbackHandler.play();
                },

                pause:function (event) {
                    self.logJplayerEvent(event);
                    if (self.__playbackHandler.pause)
                        self.__playbackHandler.pause();
                },

                playing:function (event) {
                    self.logJplayerEvent(event);
                    if (self.__playbackHandler.playing)
                        self.__playbackHandler.playing();
                },

                progress:function (event) {
                    self.logJplayerEvent(event);
                    if (self.__playbackHandler.progress)
                        self.__playbackHandler.progress();
                }
            });
            return true;
        },


        /**
         * add a click handler to the surface of the video player, usually called by a
         * PlaybackHandler instance
         *
         * @param handler {function} - if undefined, will unbind click handler,
         *   otherwise it is set as the click handler
         */
        addClickHandlerToVideoArea:function (handler) {
            if (handler)
                $(this.__jplayerContainer).click(handler);
            else
                $(this.__jplayerContainer).unbind("click");
        },


        /**
         * convenience checks to make sure we're instantiated and player is ready
         */
        canPlay:function () {
            return this.__isInstantiated && this.__playerReady;
        },

        isInstantiated:function () {
            return this.__isInstantiated;
        },

        destroyCurrentJplayer:function () {
            /* since the jplayer is created pointing to a specific div for 
             controls we need to destroy the current player before we move
             off to control play in a different cell */
            if (this.isInstantiated()) {
                CloudTalk.log("CTPLayer: destroying currentInstance");
                this.takeDownControls();
                this.__playbackHandler.ended("CTPlayer.destroyCurrentJplayer");
                $(this.__jplayerContainer).jPlayer("destroy");
                this.__currentMedia = null;
                this.__isInstantiated = false;
                this.__playerReady = false;
                this.__playPending = false;
            }
        },

        takeDownControls:function () {
            /* make the current progress bar hidden and reset any values needed
             acts on controls that are under this.__selectorAncestor */
            $(this.__selectorAncestor + ">.progress-wrapper").animate({opacity:.3});
        },

        bringUpControls:function () {
            /* make the current progress bar visible
             acts on controls that are under this.__selectorAncestor */
            $(this.__selectorAncestor + ">.progress-wrapper").animate({opacity:1});
        },

        /**
         * set the background color of the jplayer, setting to black or something
         * dark this will give appearance of a 'letterbox' presentation, setting to
         * color of your background will remove that effect
         *
         * @param setLetterColor {string} - css color, if undefined, set to black
         */
        setLetterBoxColor:function (color) {
            if (color === undefined)
                $(this.__jplayerContainer).css({background:"black"});
            else
                $(this.__jplayerContainer).css({background:color});
        },

        //// these are the jplayer controls passed through 
        ////
        play:function (time) {
            /**
             *  @param time : number : optional - time in seconds where to set the play head in seconds rom start
             */
            if (this.canPlay()) {
                //TODO: test that passing in undefined is same as not passing parameter at all
                CloudTalk.log("CTPLayer: play() playing current media: " + JSON.stringify(this.__currentMedia));
                $(this.__jplayerContainer).jPlayer("play", time);
                return true;
            } else if (this.isInstantiated()) {
                CloudTalk.log("CTPLayer: play() setting media play pending for: " + JSON.stringify(this.__currentMedia));
                this.__playPending = true;
                return true;
            }
            return false;
        },

        setMedia:function (media, playNow) {
            /**
             *  @param media {object} object containing key/value entries specifying audio types and the URLs to access them
             *  @param playNow {boolean} if true, start play as well
             *      Eg. { mp3 : "http://foo.com/pathTo/foo.mp3" }
             *  @return {boolean} true if player was set
             *  NOTE: canPlay() applies here too, we cannot set media until there's a player to accept it
             */
            this.__currentMedia = media;

            if (this.canPlay()) {
                CloudTalk.log("CTPLayer: setting media to: " + JSON.stringify(media));
                $(this.__jplayerContainer).jPlayer("setMedia", media);
                if (playNow) {
                    this.play();
                }
                return true;
            } else if (this.isInstantiated()) {
                CloudTalk.log("CTPLayer: setting setMedia to pending for: " + JSON.stringify(media));
                this.__playPending = true;
                this.__needToSetMedia = true;
                return true;
            } else {
                return false;
            }
        },

        playHead:function (position) {
            /**
             * @param position {number} - where to set the play head to % of seekable
             */
            if (this.canPlay()) {
                $(this.__jplayerContainer).jPlayer("playHead", position);
                return true;
            } else {
                return false;
            }
        },

        pause:function (time) {
            /**
             * @param time {number} optional - time in seconds where to set the play head in seconds from start
             */
            if (this.canPlay()) {
                CloudTalk.log("CTPLayer: pausing");
                $(this.__jplayerContainer).jPlayer("pause", time);
                return true;
            } else {
                return false;
            }
        },

        stop:function () {
            if (this.canPlay()) {
                $(this.__jplayerContainer).jPlayer("stop");
                return true;
            } else {
                return false;
            }
        },

        logJplayerEvent:function (event) {
            // currently handles errors and warnings
            if (event.jPlayer.error && this.__showErrors) {
                CloudTalk.error($.format("Jplayer error: type=%s, context=%s, message=%s, hint=%s",
                    [String(event.jPlayer.error.type), String(event.jPlayer.error.context),
                        String(event.jPlayer.error.message), String(event.jPlayer.error.hint)]));

            } else if (event.jPlayer.warning && this.__showWarnings) {
                CloudTalk.log($.format("Jplayer warning: type=%s, context=%s, message=%s, hint=%s",
                    [String(event.jPlayer.warning.type), String(event.jPlayer.warning.context),
                        String(event.jPlayer.warning.message), String(event.jPlayer.warning.hint)]));
            } else if (this.__showMiscEvents && !(event.jPlayer.warning || event.jPlayer.error)) {
                CloudTalk.log($.format("Jplayer event: type=%s", [event.type]));
                CloudTalk.log(event);
            }
        }
    };

    nameSpace.CTPlayer = CTPlayer;
})(window[window.NS_CT_CTPLAYER] || window);

(function (window) {
    /**
     * cloudtalk voices application
     */
        /// voices conversation application instance vars
        // convo we're looking at
        // TODO: these all might be better put into a subObject
    var _currentConversationID = CloudTalk.params.conversationID;
    var _currentMessageID = CloudTalk.params.messageID;
    var _initialPostDisplayName = CloudTalk.params.initialPostDisplayName;
    var _originalGroupPublishedTo = CloudTalk.params.originalGroupPublishedTo;

    // Type of actual page (conversation|stream)
    var _pageType = CloudTalk.params.pageType;

    // user who's viewing page
    var _currentUser = {}; // Fields: displayName, groups
    var _apiUser = null; // API bound to currently logged in user
    var _userID = CloudTalk.params.userID;
    var _feedUserID = CloudTalk.params.feedUserID;
    var _appAnchor = CloudTalk.params.appAnchor;
    var _userIsLoggedIn = false;

    // the content and info needed to display content
    var _initialPost = null; // convo object return from getPublicConversation minus extraneous object wrapper
    var _currentRenderedReplies = null;
    var _replyCursor = null; // object - actual returned payload from server
    var _replyCursorDirection = "backward"; //starting value, can be set to "forward" as well
    var _pagesInCurrentReplyCursor; // Number of pages the current cursor has
    var _totalAvailablePages; // Number of pages of replies in this conversation 
    var _startingPageNumber; // Number

    // constants for rendering
    var _postsDivWidth = 600
    var _contentWidth = _postsDivWidth - 55 - 3 - 3 - 60; // TODO: this needs to be set per User-Agent
    var _replyPageSize = 10; // 10 replies per page
    var _replyPageIncrement = 5; // number of pages a user can directly switch between
    var _replyReadIncrement = _replyPageIncrement * _replyPageSize; // read 5 * page size at a time
    // jos TODO - this needs to be tied to root setting at top of file... jsbase, cssbase, flash??
    var _flashBase = CloudTalk.params.flashBaseUrl;
    var _imgBase = CloudTalk.params.imgBaseUrl;
    var _appBase = CloudTalk.params.appBaseUrl;

    // User's stream data (user who's stream is being viewed)
    var _userStream = CloudTalk.params.userStream;
    var _displayName = CloudTalk.params.displayName;


    // dialogs 
    var _onboardingWorkflow; // OnboardingWorkFlow instance
    var _picturePopupTrigger;

    // for future notifications and websockets
    var _ctSocket = null;

    var _debug = false;

    ///
    /// instance var setters/getters for values that require more than simple scaler
    /// NO SIDE EFFECTS ALLOWED HERE
    ///
    function _setPagesInCurrentReplyCursor(val) {
        if (val === undefined)
            if (_pageType == 'stream') {
                _pagesInCurrentReplyCursor = Math.min(Math.ceil(_replyCursor.conversations.length / _replyPageSize), 5);
            } else {
                _pagesInCurrentReplyCursor = Math.min(Math.ceil(_replyCursor.messages.length / _replyPageSize), 5);
            }
        else
            _pagesInCurrentReplyCursor = val;
    }

    function _setTotalAvailablePages(val) {
        if (val === undefined)
            if (_pageType == 'stream') {
                _totalAvailablePages = Math.ceil(_replyCursor.conversationTotal / _replyPageSize);
            } else {
                _totalAvailablePages = Math.ceil(_replyCursor.messageTotal / _replyPageSize);
            }
        else
            _totalAvailablePages = val;
    }

    /**
     *  this operation requires a filter operation to remove the initial post, ok, this may technically
     *  a side effect :) it does mutate the incoming objects messages slot
     */
    function _setReplyCursor(cursorFromServer) {
        var filteredMessages = $(cursorFromServer.messages)
            .filter(function (index) {
                return (this.messageID !== _currentMessageID)
            });

        _replyCursor = cursorFromServer;
        _replyCursor.messages = filteredMessages;
    }

    /**
     * this is the active page within the current currsor set of pages
     */
    function _getCurrentActivePageInCursor() {
        return parseInt($(".replyControl.active").attr("idx"));
    }

    /**
     * this is the active page within the entire available set of reply pages
     * for this conversation
     */
    function _getCurrentActivePage() {
        return parseInt($(".replyControl.active").html());
    }


    /// 
    /// BEGIN remote logging
    /// 

    /**
     * the remoteLogger is an endpoint in the VoicesController that will log telemetry from the client
     *
     *  @method setupRemoteLogger creates the iframe and shoots up the initial bootup message to the server
     *  @method remoteLoggerLog method sends additional string messages to remote log
     *
     *  NOTE: Using @method in doc here, but logging services need to be better
     *  encapulated, either moved to the base CloudTalk namespace or added as an additional logging object
     */
    function setupRemoteLogger() {
        var src = $.format("/v2/voices/log?ss=%s&msg=Voices booting up: winHeight=%s winWidth=%s&ua=%s",
            [CloudTalk.params.sessionStamp, $(window).height(), $(window).width(), JSON.stringify($.browser)]);
        $("<iframe>").attr({
            id:"remoteLoggerFrame",
            src:encodeURI(src),
            style:"visibility:hidden; width:0px; height:0px;"
        }).appendTo("body");
    }

    function remoteLoggerLog(msg) {
        // TODO: figure out what params we need here!!
        var src = $.format("/v2/voices/log?ss=%s&msg=%s", [CloudTalk.params.sessionStamp, msg]);
        $("#remoteLoggerFrame").attr("src", src);
    }

    /// 
    /// END  remote logging
    /// 


    /// 
    /// BEGIN Message Sending Apparatus
    /// 

    /**
     * setupRecordControl - set up the message recording/sending control. we say
     * record but we mean message sending
     *
     * TODO: refactor name to mesgSending or something that matches more closely
     */
    function setupRecordControl() {
        // create the recordingform's fields
        // JOS TODO: we have another static content relocation path!
        $('<iframe>').attr({
            id:"secretSendingIframe",
            name:"secretSendingIframe",
            src:_appBase + "/emptyFrame.html",
            style:"visibility:hidden; width:0px; height:0px;"
        }).appendTo("body");

        $('<iframe>').attr({
            id:"secretSendingIframe2",
            name:"secretSendingIframe2",
            src:_appBase + "/emptyFrame.html",
            style:"visibility:hidden; width:0px; height:0px;"
        }).appendTo("body");

        $('<form>').attr({
            id:"recordingForm",
            action:"empty",
            method:"POST",
            enctype:"multipart/form-data",
            target:"secretSendingIframe"
        }).appendTo("#recordingControl");
        $('<input>').attr({
            type:'hidden',
            id:'recordingSubjectField',
            name:'subject',
            value:''
        }).appendTo("#recordingForm");
        $('<input>').attr({
            type:'hidden',
            id:'recordingParticipantsField',
            name:'participants',
            value:"[]"
        }).appendTo("#recordingForm");
        $('<input>').attr({
            type:'hidden',
            id:'recordingMessageTextField',
            name:'messageText',
            value:'default text'
        }).appendTo("#recordingForm");
        $('<input>').attr({
            type:'hidden',
            id:'recordingMessageTypeField',
            name:'messageType',
            value:"string"
        }).appendTo("#recordingForm");
        $('<input>').attr({
            type:'hidden',
            id:'recordingConversationIDField',
            name:'conversationID',
            value:''
        }).appendTo("#recordingForm");
        $('<input>').attr({
            type:'hidden',
            id:'recordingMessagePrivacyField',
            name:'messagePrivacy',
            value:"private"
        }).appendTo("#recordingForm");
        $('<input>').attr({
            type:'hidden',
            id:'recordingCreationDateField',
            name:'creationDate',
            value:''
        }).appendTo("#recordingForm");

        if (_userIsLoggedIn) {
            wireUpReplyButton();
        }
    }

    function wireUpReplyButton() {
        if (CloudTalk.params.twitterScreenName != '') {
            $("#tweetAsReplyLabel").html("Tweet as @" + CloudTalk.params.twitterScreenName);
        } else {
            $("#tweetAsReplyDiv").html('');
        }
        $("#replyBtn").unbind("click").click(function () {
            if ($("#replyTextMsg").val() ==  "" && $("#imageFile").val() == "" && $("#audioFile").val() == "") {
                $("#replyMediaError").html("At least one media field must be entered: text, audio, video, or photo");
            } else {
                $("#replyMediaError").html("");
                if ($("#imageFile").val() == "") {
                    $("#imageFile").remove();
                    $("#imageFileCaption").remove();
                } else if ($("#audioFile").val() == "") {
                    $("#audioFile").remove();
                    $("#audioFileCaption").remove();
                }
                var stream = (/^PUC/.test(_currentConversationID)) ? "Stream" : "";
                var action = $.format("%s/message/create%s?userTicket=%s", [_apiUser.__apiRoot, stream, _apiUser.authToken]);
                $("#replyForm").attr("action", action);
                $("#replySubjectField").attr("value", $("#replyTextMsg").val());
                $("#replyMessageTextField").attr("value", $("#replyTextMsg").val());
                $("#replyConversationIDField").attr("value", _currentConversationID);
                $("#replyCreationDateField").attr("value", CloudTalk.utils.ISODateString());
                $("#replyForm").submit(); // nothing to validate yet, so no submit handler
                $("#replyTextMsg").attr("value", "");
                $(".modalInput").overlay().close();
                // TODO: The posted/replied media might need to be transcoded if not - maybe we will need to
                // get the updated list from the server... here...
            }
        });
        if ($("#recordingSend").length > 0) {
            $("#recordingSend").unbind("click").click(function () {
                //CloudTalk.log("got a reply button click");
                $("#replyConversationIDField").attr("name", "conversationID");
                doPrereplyChecksThenSend(function () {
                    $("#replyToTitle").html("Reply to post");
                    $(".modalInput").trigger('click');
                })
            });
        }
        if ($("#displayNewPostForm").length > 0) {
            $("#displayNewPostForm").unbind("click").click(function () {
                $("#replyConversationIDField").attr("name", "NOT_conversationID");
                $("#replyToTitle").html("New Post");
                $("#postStreams").attr("value", _apiUser.streamID);
                $(".modalInput").trigger('click');
            });
        }
    }

//    function wireUpNewPostButton() {
//        $("#newPostBtn").unbind("click").click(function () {
//            if ($("#newPostTextMsg").val() ==  "" && $("#imageFile").val() == "" && $("#audioFile").val() == "") {
//                $("#newPostMediaError").html("At least one media field must be entered: text, audio, video, or photo");
//            } else {
//                $("#newPostMediaError").html("");
//                if ($("#imageFile").val() == "") {
//                    $("#imageFile").remove();
//                    $("#imageFileCaption").remove();
//                } else if ($("#audioFile").val() == "") {
//                    $("#audioFile").remove();
//                    $("#audioFileCaption").remove();
//                }
//                var stream = "";
//                var action = $.format("%s/message/create%s?userTicket=%s", [_apiUser.__apiRoot, stream, _apiUser.authToken]);
//                $("#newPostForm").attr("action", action);
//                $("#newPostSubjectField").attr("value", $("#newPostTextMsg").val());
//                $("#newPostMessageTextField").attr("value", $("#newPostTextMsg").val());
//                $("#newPostCreationDateField").attr("value", CloudTalk.utils.ISODateString());
//                $("#newPostForm").submit(); // nothing to validate yet, so no submit handler
//                $("#newPostStreams").attr("value", _apiUser.streamID);
//                $("#newPostTextMsg").attr("value", "");
//                $(".modalInputNewPost").overlay().close();
//                // TODO: The posted/replied media might need to be transcoded if not - maybe we will need to
//                // get the updated list from the server... here...
//            }
//        });
//        $("#displayNewPostForm").unbind("click").click(function () {
//            CloudTalk.log("got a newPost button click");
//            $(".modalInput").trigger('click');
//        });
//    }




    /**
     * before a user reply can go up, there are preconditions that need to be applied
     * the nature of fulfilling these may require an API call which is asynchronous, so
     * we can't just make some api calls and return, since the return of the API call
     * may be after this call returns. so we need to pass a continuation through the
     * checking call chain.
     *
     * @para submitFunction - continuation to be called if all the checks prove out
     */
    function doPrereplyChecksThenSend(submitFunction) {
        // currently just check for groupMembership, we can do this in a single function,
        // if we need to chain additional checks we'll need to break each into it's own 
        // continuation passing funcion
        // TODO: this check currently does not handle a reply to a private message, if such
        //  is the case we will need to apply different rules... currently this will fail
        if (userBelongsToGroup(_originalGroupPublishedTo)) {
            CloudTalk.log("ReplySend: have groups and current user is member, submitting now.");
            return submitFunction();
        } else if (_currentUser.groups) {
            // have groups, but not member, so make join request
            CloudTalk.log("ReplySend: have groups but current user is NOT member, joining now.");
            _apiUser.joinGroup({groupID:_originalGroupPublishedTo},
                function (response) {
                    CloudTalk.log("ReplySend: join succeeded, submitting now.");
                    submitFunction();
                },
                function (error) {
                    CloudTalk.error("ReplySend: join group request failed, message not sent");
                    // TODO: what other failure issues need to be attended to
                });
        } else {
            // need to get groups first, make that request an recursively call ourselves
            CloudTalk.log("ReplySend: don't have group list, getting groups now.");
            _apiUser.getGroups({}, function (usersGroups) {
                CloudTalk.log("ReplySend: gotGroup list, recalling doPrereplyChecksThenSend.");
                _currentUser.groups = usersGroups;
                doPrereplyChecksThenSend(submitFunction);
            });
        }
    }


    /**
     *  mebership inclusion test. false return does not imply _currentUser.groups has been set
     */
    function userBelongsToGroup(groupID) {
        var userBelongs = false;
        if (_currentUser.groups) {
            $.each(_currentUser.groups, function (idx, group) {
                if (group.groupID === groupID) {
                    userBelongs = true;
                    return false; // stop iteration
                }
            });
        }
        return userBelongs;
    }


    /**
     * tell the caller if there is a message ready to be sent
     *
     * NOTE: currently this test is pretty simple, is there a logged in user and
     * is there content. going forward the tests may become application context specific
     *
     * return {boolean} true if there is a valid message form ready to be displayed
     */
    function messageCanBeSent() {
        // very simple for now, logged in and text field is available
        return (_userIsLoggedIn && $("#replyMessageTextField").length > 0);
    }


    /// 
    /// END Message Sending Apparatus
    /// 


    /// 
    /// BEGIN PlaybackHandler class 
    /// 

    /**
     * create a play button handler class instance to handle the UI state
     * transitions of a single playback button. this handler is used in two contexts
     * and that is the reason we have had to create this object rather than just
     * using an anonymous function as is the norm. the first context is the "click"
     * handler for the button itself to handle the user driven events. the second
     * context is the jplayer event handlers which need a place to call back into to
     * inform the state of the playback. eg. the end event needs to reset the state
     * back to playable and change the button skin to the play arrow.
     *
     * @param item {jquery object} -  wrapping the cell div for this message
     * @param button {jquery object} -  wrapping the img.playControl button controlling playback
     * @param idx {positive integer} - specifiying which player we are referencing
     */
    function PlaybackHandler(item, button, idx, playImg, pauseImg, mediaType, id) {
//        CloudTalk.log("PlaybackHandler called with: "+
//                    "\n\titem: " + item +
//                    "\n\tbutton: " + button +
//                    "\n\tidx: " + idx +
//                    "\n\tplayImg: " + playImg +
//                    "\n\tpauseImg: " + pauseImg +
//                    "\n\tmediaType: " + mediaType +
//                    "\n\tid: " + id);
        button.attr("buttonstate", "playable"); // set initial state
        this.item = item; // gonna need this to figure out where to place a video overlay
        this.button = button;
        this.idx = idx;
        this.playImg = _imgBase + "/" + ((playImg) ? playImg : "playButton.png");
        this.pauseImg = _imgBase + "/" + ((pauseImg) ? pauseImg : "pauseButton.png");
        this.mediaType = (mediaType === "video")?".ctVideoControls":".ctAudioControls";
        this.id = id;
    }

    PlaybackHandler.prototype = {
        getMediaRequest:function () {
            var url = $("#ctPlaybackControl_" + this.idx + this.mediaType).attr("mediaURL");
            if (url==undefined) {
                url = $("#ctPlaybackControl_" + this.idx).attr("mediaURL");
            }
            //CloudTalk.log("PlaybackHandler.prototype(line ~ 2076) url: " + url +" id: " + this.id);
            if (this.media) {
                return this.media;
            } else {
                var media = {};
                switch (url.slice(-3)) {
                    case 'mp3':
                        media.mp3 = url;
                        break;
                    case 'm4a':
                        media.m4a = url;
                        break;
                    case 'm4v':
                        media.m4v = url;
                        break;
                    default:
                        throw new CloudTalk.Exception("Unsupported media: " + url, "PlaybackHandler");
                }
                this.media = media;
                return media;
            }
        },


        /**
         *  check to see if the media request is a video, if so move the video player to the location of play
         */
        makeVisibleIfNeeded:function (centered) {
            if (this.getMediaRequest().m4v) {
                if (centered) {
                    CloudTalk.mediaPlayer.showPlayerAt("30%", 0, 308, 308);
                } else {
                    // we're in a specific div, somewhere, first figure out which type
                    var initialPostImage = $("#initialPostImage, .videoThumb", this.item);
                    if (initialPostImage.length > 0) {
                        // we're in the initial post div, we need to move the player and make the controls visible
                        CloudTalk.mediaPlayer.setLetterBoxColor();
                        CloudTalk.mediaPlayer.showPlayerAt(0, 0, 450, 308, initialPostImage);

                        // the following line changes the  display style on an object containing the initial post
                        // or any element containing videoThumb class within object this.item
                        $('.ctVideoControls', $(initialPostImage).parent()).css("display", "block");

                    } else {
                        // we're in a reply cell div
                        CloudTalk.mediaPlayer.setLetterBoxColor("#F8F8F8");
                        CloudTalk.mediaPlayer.showPlayerAt(0, 0, 450, 308, $(".contentImage", this.item));
                    }
                }
                // hide the play icon hovering over the video thumbnail
                $(".jp-video-play", this.item).css("display", "none");

                // now put a click handler on the video surface to handle start stop clicks
                var self = this;
                CloudTalk.mediaPlayer.addClickHandlerToVideoArea(function () {
                    self.click();
                });
            }
        },


        hideIfNeeded:function () {
            // move the player back off screen
            CloudTalk.mediaPlayer.parkPlayer();
            // this next item only fires if we're in the initial post context
            //$("#ctPlaybackControl_initialPostVideo", this.item).css("display", "none"); // jos merge
            $(".ctVideoControls", this.item).css("display", "none");  // jos merge

            // bring back the play icon that hovers over the video thumbnail
            $(".jp-video-play", this.item).css("display", "block");

            // remove the clickhandler
            CloudTalk.mediaPlayer.addClickHandlerToVideoArea();
        },


        click:function () {
            /* state machine to manage play
             * playable (initial state) -> playing
             * paused -> playing
             * playing -> paused, stopped (stopped not implemented at this time)
             */
            var buttonState = $(this.button).attr("buttonstate");
            if (buttonState === "playable" || buttonState === "paused") {
                var ok = false;
                //CloudTalk.log("PlaybackHandler: play clicked for player# " + this.idx + " id: " + this.id);
                if (buttonState === "playable") {
                    //CloudTalk.log("PlaybackHandler: in playable state, instantiating new player. MediaType: "  + this.mediaType + " id: " + this.id);
                    CloudTalk.mediaPlayer.instantiateWith("ctPlaybackControl_" + this.idx + this.mediaType, this, true);
                    this.makeVisibleIfNeeded(); // this needs to come before setting media
//alert("this.getMediaRequest(): " + JSON.stringify(this.getMediaRequest()));
                    ok = CloudTalk.mediaPlayer.setMedia(this.getMediaRequest(), true);
                } else if (buttonState === "paused") {
                    //CloudTalk.log("PlaybackHandler: in paused state, restarting player. MediaType: "  + this.mediaType + " id: " + this.id);
                    ok = CloudTalk.mediaPlayer.play();
                    console.log(this.button);
                } else {
                    CloudTalk.log("PlaybackHandler entered unknown state:" + this.id);
                }
                if (ok) {
                    $(this.button).attr("buttonstate", "playing");
                    $(this.button).attr("src", this.pauseImg);
                } else {
                    // TODO: this fail clause needs to be a catch(), need to remove T/F in ctplayer and replace with a throw
                    this.hideIfNeeded();
                    CloudTalk.log("PlaybackHandler: error media is:" + JSON.stringify(this.getMediaRequest()));
                }
            } else if (buttonState === "playing") {
//                CloudTalk.log("PlaybackHandler: in playing state, pausing player. MediaType: " + this.mediaType + " id: " + this.id );
//console.log(this.button);
                $(this.button).attr("src", this.playImg);
                $(this.button).attr("buttonstate", "paused");
                CloudTalk.mediaPlayer.pause();
            }
            return false;
        },

        // entry points for CTPlayer.jPlayer events
        // for now we just need to handle the 'ended' event
        ended:function (endedByWhom) {
//            CloudTalk.log("PlaybackHandler: play ended, byWhom=" + endedByWhom);
            $(this.button).attr("src", this.playImg);
            $(this.button).attr("buttonstate", "playable"); // set initial state
            CloudTalk.mediaPlayer.pause(0);	 // reset back to zero
            this.hideIfNeeded();
        }
    }

    /// 
    /// END PlaybackHandler class 
    /// 

    /// 
    /// BEGIN OnboardingWorkFlow class 
    /// 

    /**
     *  Create an instance of the OnboardingWorkFlow. The LoginWorkFlow class is intended to
     *  be a singleton and its main purpose in this app is to reduce complexity in the
     *  app. It might serve as a mechanism that can be ported to our other apps, BUT
     *  NOTE that it is still entangled with the HTML from the current JSP and functions it
     *  expects in the application environment
     *
     *  The LoginWorkFlow currently has 4 worflow (WF) views
     *  1. the simple login WF          - #loginWorkflow
     *  2. the create user WF           - #createUserWorkflow
     *  3. the connect to Twitter WF    - #connectToTwitterWorkflow
     *  4. the finishProfile WF         - #finishProfileWorkflow
     *
     *  @param triggers {dictionary} object containing list of triggers and the selector
     *         to the view that each should start up in. each trigger should corrospond
     *         to an elementID selector specified with the '#' char. the view selectors
     *         should be one of the legal workflow view selectors. (see list above)
     */
    function OnboardingWorkflow(triggerMap) {
        this._errorShowing = false;
        this._notificationShowing = false;

        this._currentWF; // jQuery w/ current wf item being shown
        this._currentTrigger; // jQuery w/ current trigger
        this._triggers; // a jQuery with the actual triggers

        this._loginWFSelector = "#loginWorkflow";
        this._createUserWFSelector = "#createUserWorkflow";
        this._connectTwitterWFSelector = "#twitterConnectWorkflow";
        this._finishProfileWFSelector = "#finishProfileWorkflow";
        this._triggerMap = triggerMap;
        this._triggerSelectors = "";

        var self = this;

        $.each(triggerMap, function (trigger, wfSelector) {
            self._triggerSelectors += ("," + trigger);
            //TODO: consider checking that the wfSelectors are valid per list above
        });
        // lop off the first char which is a ','
        this._triggerSelectors = this._triggerSelectors.substring(1);


        // wire up the triggers to bring up the dialog

        // TODO: we very likely will want to set additional triggers during runtime and
        // not just at construction time. this means that this next block needs to be
        // callable as a method. if we intend to keep list of triggers we need to
        // provide some append there and we'll need to make sure that this closure will
        // operate in the context of a method (note the calls using "self")
        this._triggers = $(this._triggerSelectors).overlay({
            mask:{
                color:'#ebecff',
                loadSpeed:200,
                opacity:1
            },
            top:'30%',
            closeOnClick:false,
            onBeforeLoad:function () {
                self._currentTrigger = this.getTrigger();
                self.switchToWF(self._triggerMap["#" + self._currentTrigger.attr("id")]);
            },
            onLoad:function () {
                $("#loginPassword").unbind("keypress").keypress(
                    function (e) {
                        if (e.which === 13) { // return key
                            e.preventDefault();
                            $("#loginSubmitButton").click();
                            return false;
                        }
                    });
                $("#callToAction > a").unbind("click").click(
                    function () {
                        // TODO, add an iPhone transformation into the mix, dialog cannot be 720px wide
                        var callToAction = $(this).text();

                        if (callToAction === "Sign Up") {
                            self.switchToCreateUserWF();
                        } else if (callToAction === "Twitter Connect") {
                            self.openTwitterConnectWindow();
                        }
                    });
            }
        });

        /// wire up button actions, remember that in handlers the "this" from this
        /// context is stored in the local var "self"

        $("#connectToTwitterButton").click(function () {
            self.openTwitterConnectWindow();
        });
        $("#takeMeToLoginButton").click(function () {
            self.switchToLoginWF();
        });
        $("#takeMeToSignupButton").click(function () {
            self.switchToCreateUserWF();
        });

        $("#loginSubmitButton").click(function () {
            self.log("login submit!!");
            var name = $("#loginWorkflow #loginName").val();
            var pass = $("#loginWorkflow #loginPassword").val();
            var errorShowing = false;
            self.log($.format("name=%s, pass=%s, anchor=%s", [name, pass, _appAnchor]));
            //TODO: put up a spinny... or set wait cursor		
            if (name === "" || pass === "") {
                self.postFormError("You need to provide both a loginID and a password");
            } else {
                _apiUser.login(
                    { userName:name,
                        password:pass,
                        anchors:[_appAnchor]
                    },
                    function (returnValue) {
                        self.postFormNotification("Login Succeeded");
                        self.completeInternalLogin(returnValue);
                        //TODO takeDown wait cursor

                        // TODO: should be _app.wireUpReplyButton(), but we don't have app object yet
                        wireUpReplyButton();
                        wireUpLogoutButton();

                        self.sendPendingMessageIfNeeded();

                        // give user time to savor their login success
                        window.setTimeout(function () {
                            self.clearFormNotification();
                            self.close();
                        }, 1000);
                    },
                    function (status, errorReturn) {
                        self.log("login attempt failed with: " + JSON.stringify(errorReturn));
                        //errors
                        // JOS FIX THIS
                        // 1) {"error":"Invalid value for parameter emailAddress" }
                        //    slightly bogus error message due to history that email was loginID 
                        //    means login ID alphaNum or email or phone# is not found
                        // 2) {error: "There is no user associated with these credentials."}
                        //    this user doesn't exist
                        // 3) {"error":"Invalid password"}
                        //    bad password
                        var specificMessage = "";
                        if (/Invalid password/.test(errorReturn.error))
                            specificMessage = "Password not correct.";
                        else if (/Invalid value|no user associated/.test(errorReturn.error))
                            specificMessage = "User name not registered.";

                        self.postFormError("Login failure: " + specificMessage);
                    });
            }
        });

        $("#createUserSubmitButton").click(function () {
            self.log("createUser submit!!");
            self.clearFormNotification();
            var name = $("#createUserWorkflow #loginName").val();
            var pass = $("#createUserWorkflow #loginPassword").val();
            var passRepeated = $("#createUserWorkflow #loginPasswordRepeated").val();
            var email = $("#createUserWorkflow #emailAddress").val();
            self.log($.format("name=%s, pass=%s, pass2=%s, email=%s", [name, pass, passRepeated, email]));

            //TODO: put up a spinny... or set wait cursor		
            if (name === "" || pass === "" || email === "" || passRepeated === "") {
                return self.postFormError("All fields are required to create an account");
            }
            else if (passRepeated !== pass) {
                return self.postFormError("Passwords entered do not match");
            }
            else {
                _apiUser.createUser(
                    { userName:name,
                        password:pass,
                        emailAddress:email,
                        anchors:[_appAnchor]
                    },
                    function (returnValue) {
                        /**
                         * complete login logic; createUser does a login operation as well
                         * then continue on to the Twitter account association
                         * then, can we get just a little more information... UserName & profile pic
                         */
                        self.completeInternalLogin(returnValue);
                        self.postFormNotification("Account Creation Succeeded");
                        //TODO takeDown wait cursor

                        wireUpReplyButton();
                        wireUpLogoutButton();

                        // TODO NOTE by sending here, this will not be posted to twitter, to
                        // make a twitter post happen we'll need to put the
                        // sendPendingMessageIfNeeded() call after the user either
                        // decides to connect or not probably on some final close()
                        // hook, which doesn't exist yet
                        self.sendPendingMessageIfNeeded();

                        // give the user another second to read the success banner, then move onto Twitter Connect
                        window.setTimeout(function () {
                            self.clearFormNotification();
                            self.switchToConntectToTwitterWF();
                        }, 1000);
                    },
                    function (status, errorReturn) {
                        self.log("createUser attempt failed with: " + JSON.stringify(errorReturn));
                        //errors
                        // 1) {"error":"Invalid value for parameter emailAddress
                        //      (Message: User login, email or mobile already used in the system)"}
                        // 2) {"error":"Invalid value for parameter userName
                        //      (Message: User login, email or mobile already used in the system)"}
                        // NOTE: these messages are notorious for being incomplete, it will fail after just
                        // checking one value, even though 2 or more parameters might be invalid

                        var specificMessage = "";
                        if (/Invalid value for parameter emailAddress/.test(errorReturn.error))
                            specificMessage = "The email address you provided is invalid";
                        else if (/parameter emailAddress/.test(errorReturn.error))
                            specificMessage = "The email address you provided is already in use";
                        else if (/parameter userName/.test(errorReturn.error))
                            specificMessage = "The user name you provided is already in use";

                        self.postFormError("Account Create Failed: " + specificMessage);
                    });
            }
        });

        $("#letMeFinishMyProfileButton").click(function () {
            self.log("user elected to finish their profile w/o twitter...");
            self.clearFormNotification();
            self.switchToFinishProfileWF();
        });

    }


    OnboardingWorkflow.prototype = {
        switchToWF:function (wfSelector) {
            this.log($.format("Current workflow: %s is changing state to %s",
                [String($(this._currentWF).attr("id")), wfSelector]));
            $(this._currentWF).css("display", "none");
            this._currentWF = $(wfSelector);
            $(this._currentWF).css("display", "block");
            $(".loginBox.left", this._currentWF).height($("#onboardingDialog").innerHeight() - 20);
        },

        switchToLoginWF:function () {
            this.switchToWF(this._loginWFSelector);
//TODO: this is a web/mobile branch size setting
//	    $(".onboardingDialog").css("width", "350px");
            $("#loginName", this._currentWF).focus();
        },

        switchToCreateUserWF:function () {
            this.switchToWF(this._createUserWFSelector);
//TODO: this is a web/mobile branch size setting
//	    $(".onboardingDialog").css("width", "350px");
            $("#loginName", this._currentWF).focus();
        },

        switchToConntectToTwitterWF:function () {
            this.switchToWF(this._connectTwitterWFSelector);
//TODO: this is a web/mobile branch size setting
//	    $(".onboardingDialog").css("width", "720px");
        },

        switchToFinishProfileWF:function () {
            this.switchToWF(this._finishProfileWFSelector);
            $("#displayName", this._currentWF).focus();
        },

        postFormError:function (msg) {
            this.clearFormNotification();
            this._errorShowing = true;
            $(".onboardingWFMessagePanel", this._currentWF).append(
                $.format("<p class='loginErrorMessage'>%s</p>", [msg]));
        },

        postFormNotification:function (msg) {
            this.clearFormNotification();
            this._notificationShowing = true;
            $(".onboardingWFMessagePanel", this._currentWF).append(
                $.format("<p class='loginNoticeMessage'>%s</p>", [msg]));
        },

        clearFormNotification:function () {
            if (this._errorShowing || this._notificationShowing) {
                $(".onboardingWFMessagePanel :last-child", this._currentWF).remove();
                this._errorShowing = false;
                this._notificationShowing = false;
            }
        },

        close:function () {
            $(this._currentTrigger).overlay().close();
        },

        currentTriggerName:function () {
            //JOS should we be trying self._currentTrigger.selector here???
            return $(this._currentTrigger).attr("id")
        },

        /**
         * its possible that the "Reply" button was the trigger that cause the login
         * workflow to start. This can happen if there was no logged in user and a user
         * attempts to reply to the current post. After a successful login, this method
         * is called to see if we need to now send that reply
         */
        sendPendingMessageIfNeeded:function () {
            if (this.currentTriggerName() === "recordingSend" && messageCanBeSent()) {
                window.setTimeout(function () {
                    $("#recordingSend").click();
                }, 100);
            }
        },

        /**
         * @param returnValue {object} return value from createUser apicall
         */
        completeInternalLogin:function (returnValue) {
            this.log("completeInternalLogin got login/createUser return: " + JSON.stringify(returnValue));
            // app instance vars
            _currentUser.displayName = returnValue.displayName;
            _userIsLoggedIn = true;
            CloudTalk.utils.deleteCookie("authToken");
            CloudTalk.utils.createCookie("authToken", returnValue.userTicket);
        },

        openTwitterConnectWindow:function () {
            window.open("/v2/voices/twitterSignup", "Twitter Authentication", "width=650,height=760");
        },

        log:function (msg) {
            CloudTalk.log("OnboardingWorkflow: " + msg);
        },

        error:function (msg) {
            CloudTalk.error("OnboardingWorkflow: " + msg);
        }
    }


    /// 
    /// END OnboardingWorkflow class 
    /// 

    /**
     * render the initial post - check to see which if any of the media items are
     * present and wire up their controls as needed
     *
     */
    function renderInitialPost() {
        var initialPostDiv = $("#initialPost");
        var audioPlayControl = $("#ctPlaybackControl_initialPostAudio");
        if (audioPlayControl.length > 0) {
            var audioURL = CloudTalk.utils.fixResourceURL(audioPlayControl.attr("mediaURL"));
            audioPlayControl.attr("mediaURL", audioURL); // fix for running on dev stack
            //audioPlayControl.addClass("ctAudioControls");
            //$(audioPlayControl).css("float", "left");
            var audioButton = $(".audioPlayButton", audioPlayControl);
            var audioPlaybackHandler = new PlaybackHandler(initialPostDiv, audioButton, "initialPostAudio",
                "playbuttonbig@2x.png", "pausebuttonbig@2x.png", "audio");
            audioButton.click(function () {
                audioPlaybackHandler.click();
            });
        }
        var videoPlayControl = $("#ctPlaybackControl_initialPostVideo", initialPostDiv);
        if (videoPlayControl.length > 0) {
            var videoURL = CloudTalk.utils.fixResourceURL(videoPlayControl.attr("mediaURL"));
            videoPlayControl.attr("mediaURL", videoURL); // fix for running on dev stack
            var videoButton = $(".videoPlayButton", videoPlayControl);
            var videoPlaybackHandler = new PlaybackHandler(initialPostDiv, videoButton, "initialPostVideo",
                "videoPlayerPlayButton.png", "videoPlayerPauseButton.png", "video");
            videoButton.click(function () {
                videoPlaybackHandler.click();
            });
            $(".jp-video-play", initialPostDiv).click(function () {
                videoPlaybackHandler.click();
            });
        }
    }


    /**
     * emit HTML to render message item based on the cell media contents
     * text, audio, picture, video. create the DOM structure then resize
     * the maleable elements to fit the content
     *
     * @param message {object} message object object returned from inbox call
     * @return {string} HTML that renders the messageItem
     */
    function renderMessageItem(idx, message) {
//        CloudTalk.log("renderMessageItem("+idx+", "+message+") called");
        var createDate = CloudTalk.utils.safeDateFromDateString(message.creationDate).toUTCString();
        var renderObj = renderMessageItemMedia(idx, message);
        var controlsHTML = renderObj.controlsHTML;
        var sender = renderObj.sender;
        var newItem = "";

        if (sender == _initialPostDisplayName) {
//            CloudTalk.log("renderMessageItem() - initialPostDisplayName");
            newItem = $($.format(
                "<div class='item conversationItem' idx='%s'>\
                   <div class='avatarContainerSmall left'>\
                     <img src='%s' class='avatarLeft'/>\
                     <div class='avatarFrameSmall'>\
                         <a href='/v2/vu/%s'> \
                            <img src='%s/avatarFrameSmall.png'>\
                         </a>\
                     </div>\
                   </div>\
                   <div class='bubble'>\
                     <div class='lBubbleTL cornerBorder'/>\
                     <div class='lBubbleTC centerBorder'>\
                       <div class='itemParticipants'>%s</div>\
                       %s\
                     </div>\
                     <div class='lBubbleTR cornerBorder'/>\
                     <div class='lBubbleML middleBorder'/>\
                     <div class='lBubbleMC'>%s</div>\
                     <div class='lBubbleMR middleBorder'/>\
                     <div class='lBubbleBL cornerBorder'/>\
                     <div class='lBubbleBC centerBorder'><div class='timeAgo' title='%s'></div></div>\
                     <div class='lBubbleBR cornerBorder'/>\
                   </div>\
                 </div>",
                [idx, CloudTalk.utils.getUserAvatar(message), message.sender.senderName, _imgBase, sender, renderObj.controlsHTML, renderObj.mediaHTML, createDate]));

            $(".lBubbleML, .lBubbleMC, .lBubbleMR", newItem).css("height", String(renderObj.height) + "px");

        }
        else {
//            CloudTalk.log("renderMessageItem() - NOT initialPostDisplayName");
            newItem = $($.format(
                "<div class='item conversationItem' idx='%s'>\
                   <div class='bubble'>\
                     <div class='rBubbleTL cornerBorder'/>\
                     <div class='rBubbleTC centerBorder'>\
                       <div class='itemParticipants'>%s</div>\
                       %s\
                     </div>\
                     <div class='rBubbleTR cornerBorder'/>\
                     <div class='rBubbleML middleBorder'/>\
                     <div class='rBubbleMC'>%s</div>\
                     <div class='rBubbleMR middleBorder'/>\
                     <div class='rBubbleBL cornerBorder'/>\
                     <div class='rBubbleBC centerBorder'><div class='timeAgo' title='%s'></div></div>\
                     <div class='rBubbleBR cornerBorder'/>\
                   </div>\
                   <div class='avatarContainerSmall right'>\
                     <img src='%s' class='avatarRight'/>\
                     <div class='avatarFrameSmall'>\
                     <a href='/v2/vu/%s'> \
                        <img src='%s/avatarFrameSmall.png'>\
                     </a>\
                     </div>\
                   </div>\
                 </div>",
                [idx, sender, renderObj.controlsHTML, renderObj.mediaHTML, createDate,
                    CloudTalk.utils.getUserAvatar(message), message.sender.senderName,  _imgBase]));

            $(".rBubbleML, .rBubbleMC, .rBubbleMR", newItem).css("height", String(renderObj.height) + "px");
        }


        var videoPlayControl = $(".ctVideoControls", newItem);
        if (videoPlayControl.length > 0) {
            var videoURL = CloudTalk.utils.fixResourceURL(videoPlayControl.attr("mediaURL"));
            videoPlayControl.attr("mediaURL", videoURL); // fix for running on dev stack
            var videoButton = $(".videoPlayButton", newItem);
            var videoPlaybackHandler = new PlaybackHandler(newItem, videoButton, /*"ctPlaybackControl_"+*/idx,
                "videoPlayerPlayButton.png",
                "videoPlayerPauseButton.png", "video", (new Date()).getTime());
            videoButton.unbind("click").click(function () {
                videoPlaybackHandler.click();
            });
            $(".jp-video-play", newItem).unbind("click").click(function () {
                videoPlaybackHandler.click();
            });
        }
        var audioPlayControl = $(".ctAudioControls", newItem);
        if (audioPlayControl.length > 0){
            // Set audio player handlers
            var button = $(".audioPlayButton", newItem);
            if (button.length > 0) {
                var audioPlaybackHandler = new PlaybackHandler(newItem, button, idx,
                    "playButton.png",
                    "pauseButton.png", "audio", (new Date()).getTime());
                button.unbind("click").click(function () {
//                    CloudTalk.log("AUDIO HANDLER CALLED");
                    audioPlaybackHandler.click();
                });
            }
        }
        return newItem;
    }


    /**
     *  render the individual media sections of a message; manage the reality
     *  that a message is a bundle of mime types and may have text+audio or
     *  text+pic or text+pic+audio or just a pic or just an audio or text
     *
     * @param idx {index} of the item being rendered
     * @param message {object} representation of message object object returned from inbox call
     * @return {object} containing attributes HTML, height width etc that describe message rendering
     */
    function renderMessageItemMedia(idx, message, options) {
//        CloudTalk.log("renderMessageItemMedia("+idx+", "+message+", "+options+") called");
        var maxTextLength = 4096;

        if (options !== undefined) {
            if (options.maxTextLength !== undefined)
                maxTextLength = options.maxTextLength;
        }

        // init our return object
        var renderObj = {};
        var mediaDiv = $("<div class='itemMedia'></div>");
        var itemText = $("<div class='itemText'></div>");
        var imageElement = null;
        var metrics = null;
        var audioControlsHTML = "";
        var videoControlsHTML = "";

        mediaDiv.append(itemText);
        renderObj.sender = CloudTalk.utils.getSender(message);
        renderObj.types = {};

        for (i in message.mediaContent) {
            var content = message.mediaContent[i];
//CloudTalk.log("idx: " + idx +"\tcontent.type: " + content.type);
            switch (content.type) {
                case "text":
                    // userText takes precidence, overwrite and remove other classes
                    $(itemText).html(CloudTalk.utils.shortenText(content.caption, maxTextLength - renderObj.sender.length));
                    $(itemText).addClass("userText");
                    $(itemText).removeClass("audioText imageText videoText");
                    renderObj.types.text = 1;
                    break;

                case "audio":
                    // TODO: need to understand how video controls will be differnt from audio!!
                    audioControlsHTML = $.format(
                        "<div id='ctPlaybackControl_%s' class='ctAudioControls' mediaURL='%s'>\
                       <span class='duration'>%s</span>\
                       <div class='progress-wrapper'>\
                         <div class='jp-progress'>\
                           <div class='progress jp-seek-bar'>\
                             <div class='elapsed jp-play-bar'></div>\
                           </div>\
                         </div>\
                       </div>\
                       <img class='playControl audioPlayButton' src='%s/playButton.png' />\
                     </div>",
                        [idx, CloudTalk.utils.fixResourceURL(content.file), CloudTalk.utils.seconds2time(content.duration), _imgBase]);
//  		    [idx, "http://integration.cloudtalk.me:/v2/html/media/goldAudio.mp3", CloudTalk.utils.seconds2time(content.duration), _imgBase]);
// TODO: !!! fix URL reference
                    addAutoText(itemText, "audioText");
                    renderObj.types.audio = 1;
                    renderObj.duration = content.duration;
                    break;

                case "video":
//                    CloudTalk.log("Video HTML controls creation");
                    imageElement = $($.format("<div class='contentImage videoThumb' style='background:url(%s); background-repeat:no-repeat;"+
                        "background-position: center;height:308px;padding-top:1px'>"+
                        /* this NBSP is needed to move the image i line-height below... */
                        "<div class='jp-video-play' style='top: 150px;position: relative;'>"+
                        "<a href='javascript:;' class='jp-video-play-icon'>play</a></div>" +
                        "</div>" +
                        "<div id=\"ctPlaybackControl_%s\" class=\"ctVideoControls\"" +
                        "mediaURL=\"%s\" style=\"display:none;\">"+
                        "<div style=\"float: left;\">"+
                        "<img class=\"playControl videoPlayButton\" src=\"%s/videoPlayerPlayButton.png\"/>"+
                        "</div>"+
                        "<div class='progress-wrapper jp-progress'>\
                             <div class=''>\
                                <div class='progress jp-seek-bar'>\
                                  <div class='elapsed jp-play-bar'></div>\
                                </div>\
                            </div>\
                        </div>"+
                        "</div>",
                        [CloudTalk.utils.fixResourceURL(content.file + "?size=thumbhd"),
                            idx, CloudTalk.utils.fixResourceURL(content.file),
                            CloudTalk.utils.fixResourceURL(content.file),_imgBase]));


                    addAutoText(itemText, "videoText")
                    renderObj.types.video = 1;
                    renderObj.duration = content.duration;

                    break;

                case "image":
                    // vertical image 205x308 horizontal 308x205, reduce by half for screen item cell size (154x103)
                    // NOTE: the background repeat and position styles only seem to work here if they are inline
                    var url = CloudTalk.utils.fixResourceURL(content.file);
                    imageElement = $($.format("<div class='contentImage contentImagePopup' style='background:url(%s); background-repeat:no-repeat; background-position:center center;' rel='#picturePopup' pictureUrl='%s'></div>",
                        [url + "?size=thumbhd", url + "?size=screenhd"]));
// TODO: !!! fix access method getMessageImage() 
//		    [CloudTalk.utils.getMessageImage(message, "thumbhd")]))

                    addAutoText(itemText, "imageText");
                    renderObj.types.image = 1;
                    break;

                default:
                    break;
            }
        }

        // now finish up
        $(mediaDiv).append(imageElement);
        renderObj.controlsHTML = audioControlsHTML + videoControlsHTML;
        renderObj.mediaHTML = $(mediaDiv).outerHTML();

        metrics = CloudTalk.utils.textMetrics(itemText, _contentWidth);
        renderObj.height = metrics.height;
        renderObj.width = metrics.width;

        // this is a travesty adding a hard coded constant, but a combined mediaDiv metrics isn't working
        // yet, we need a better method that will calcualte the size of an arbitrary div...
        if (imageElement) {
            renderObj.height += 340; // 308 image height + 12 for margin
            renderObj.width += 320;
        }

        return renderObj;
    }


    /**
     * decorate textItem with "auto text" based on current text state. state is managed
     * using additional class attributes on the textItem div
     *
     * if no existing text then just add auto text based on content type
     * if userText exists, then just exit, userText rules
     * if another auto text(s) exists, modify current text to add new type
     *
     * @param textElement {jquery object} jquery object pointing at a .textItem div
     * @param contentTypeClass {string} one of "audioText", "imageText", "videoText"
     * @return undefined
     */
    function addAutoText(textElement, contentTypeClass) {
        if (!$(textElement).hasClass("userText")) {
            $(textElement).addClass(contentTypeClass);
            var classNames = $(textElement).attr("class").split(' ');
            var autoText = "";
            $.each(classNames.sort(), function (idx, className) {
                switch (className) {
                    case "audioText":
                        autoText += (autoText.length == 0) ? "Audio " : "+ Audio ";
                        break;
                    case "imageText":
                        autoText += (autoText.length == 0) ? "Picture " : "+ Picture ";
                        break;
                    case "videoText":
                        autoText += (autoText.length == 0) ? "Video " : "+ Video ";
                        break;
                }
            });
            autoText += "Message";
            // $(textElement).html("<span class='itemText' style='font-style:italic;'>" + autoText + "</span>");
            $(textElement).html("<span class='itemText' style='font-style:italic;'>&nbsp;</span>");
        }
    }


    /*
     * size all message items on page to be specified width limitations: all
     * message items are sized to same size, there's no way currently to filter
     * or separate out one set of items from another
     *
     * make sure all of the callers of this (currently just repliesAreDone and
     * initialPostIsDone) have called before triggering the resize
     * 
     * NOTE: initialPost rendering no longer requires resizing, but we're leaving
     *  in the apparatus for future where we might go back to multiple dynamically
     *  sized sections
     *
     * @param width {integer} width in px that the item divs will be set to
     *
     * TODO:
     *   1. still need to set the height of the top "poster" div
     */
    function resizeAccordingToUserAgent(callerID) {
        // remember who's called us
        resizeAccordingToUserAgent[callerID] = true;

        //if (resizeAccordingToUserAgent.repliesAreDone && resizeAccordingToUserAgent.initialPostIsDone) {
        if (resizeAccordingToUserAgent.repliesAreDone) {
            if (_pageType == 'stream') {

                // heights - we need to set the height of the .item container to the natural size of bubble container
                var totalHeight = 0;
                $(".item").each(function () {
                    //$(this).css("height", $(".itemContainer", $(this)).height()+"px");
                    totalHeight += $(".itemContainer", $(this)).height();
                });
                $('#messageList').css("height", totalHeight + "px");

                // initial post
                /*$("#initialPostImage").css("width", String(contentWidth) + "px");
                 $("#initialPostImage").css("height", "308px");*/
            } else {
                // avatar 55px
                // avatar margin 3px
                // corners 60px
                // bubble margin 3px
                var bubbleWidth = _postsDivWidth - 55 - 3 - 3; // arvatar + avatarMargin + bubbleMargin
                var contentWidth = _postsDivWidth - 55 - 3 - 3 - 60; // arvatar + avatarMargin + bubbleMargin + corners

                // widths are made proportional to the width of the enclosing container
                $(".itemList, .items, .item").css("width", String(_postsDivWidth) + "px");
                $(".bubble").css("width", String(bubbleWidth) + "px");
                $(".itemMedia, .lBubbleTC, .rBubbleTC, .lBubbleMC, .rBubbleMC, .lBubbleBC, .rBubbleBC, .itemText")
                    .css("width", String(contentWidth) + "px");

                // heights - we need to set the height of the .item container to the natural size of bubble container
                $(".item").each(function () {
                    $(this).css("height", $(".bubble", $(this)).height() + "px");
                });

                // initial post
                $("#initialPostImage").css("width", String(contentWidth) + "px");
                $("#initialPostImage").css("height", "308px");
            }
        }
    }


    /**
     * seek to the nth page available in the current cursor
     *
     * @param pageNum {positive integer} bounded by 1 and _pagesInCurrentReplyCursor inclusive
     * @return {boolean} false if out of bounds, true otherwise
     */
    function seekToPageInCurrentCursor(pageNum) {
        if (pageNum < 1 || pageNum > _pagesInCurrentReplyCursor)
            return false;
        else {
            var scrollAPI = $("#messageList").data("scrollable");
            scrollAPI.seekTo((pageNum - 1) * _replyPageSize, 0);
            $(".replyControl.active").toggleClass("active");
            $(".replyControl.numberPage[idx=" + pageNum + "]").toggleClass("active");
            return true;
        }
    }

    /**
     * render set of page navigation links based on the current state of the replies cursor data
     */
    function renderReplyControlPageLinks() {
        var replyControls = $("#replyControls").empty();
        replyControls.append("<span class='browse prevPage replyControl'>prev</span>");
        for (var i = 0; i < _pagesInCurrentReplyCursor; i++) {
            replyControls.append($.format("<span class='replyControl numberPage' idx='%s'>%s</span>",
                [String(i + 1), String(_startingPageNumber + i)]));
        }
        replyControls.append("<span class='browse nextPage replyControl'>next</span>");

        // wire up actions
        $(".nextPage").click(
            function () {
                var currentPageNum = _getCurrentActivePage();
                var nextActivePageIdx = _getCurrentActivePageInCursor() + 1;

                if (nextActivePageIdx <= _pagesInCurrentReplyCursor) {
                    // we are still operating within the range of visible pages, the #'s between prev & next
                    seekToPageInCurrentCursor(_getCurrentActivePageInCursor() + 1);
                } else if (currentPageNum < _totalAvailablePages) {
                    // this mens we can go forward another set of pages
                    getNextSetOfReplyPages();
                    return; // force tail exit here
                }
            });

        $(".prevPage").click(
            function () {
                var currentPageNum = _getCurrentActivePage();
                var nextActivePageIdx = _getCurrentActivePageInCursor() - 1;

                if (nextActivePageIdx > 0) {
                    // we are still operating within the range of visible pages, the #'s between prev & next
                    seekToPageInCurrentCursor(_getCurrentActivePageInCursor() - 1);
                } else if (currentPageNum > _replyPageIncrement) {
                    // this means we can go backwards another set of pages
                    getPreviousSetOfReplyPages();
                    return; // force tail exit here
                }
            });

        $(".numberPage").click(
            function () {
                // next as in page we're changing to, but really the idx of
                // "this" since user selected this page to go to
                var nextActivePageIdx = Number($(this).attr("idx"));
                seekToPageInCurrentCursor(nextActivePageIdx);
            });

        // interaction controls
        $(".replyControls").disableTextSelect();
        $("*.replyControl").hover(function () {
            $(this).toggleClass("hover");
        });
    }


    function initializeReplyPageControl() {
        _setTotalAvailablePages();
        _setPagesInCurrentReplyCursor();
        _startingPageNumber = 1;

        renderReplyControlPageLinks();

        $(".replyCount").html(String(_replyCursor.messageTotal - 1) + " replies");
    }


    /**
     * clear the current reply display cells and rerender anew from the reply
     * cursor
     */
    function renderReplyPageSet() {
        var items = $("#messageListItems");
        items.empty()
        _currentRenderedReplies = _replyCursor.messages;

        // normalize the time order of the message list, if we're traveling
        // forward the messages are delivered in ascending time order, our
        // normal is descending time order
        if (_replyCursorDirection === "forward")
            _currentRenderedReplies.reverse();

        $.each(_currentRenderedReplies,
            function (idx, message) {
                items.append(renderMessageItem(idx, message));
            });

        resizeAccordingToUserAgent("repliesAreDone");
        $(".timeAgo").timeago(); // time fields
        setPicturePopupTriggers();
    }


    /**
     *  get the next set of pages goind "backward" in time based on current
     *  currsor location, there is guard code to make sure you don't run off the
     *  end of the world
     */
    function getNextSetOfReplyPages() {
        var nextStartingDate;
        var numCurrentRenderedReplies = _currentRenderedReplies.length;

        if (numCurrentRenderedReplies < _replyReadIncrement)
            return; // there's no way there's any more replies left 

        if (_replyCursorDirection === "backward") {
            nextStartingDate = _replyCursor.nextStartingDate;
        } else {
            // we have to go fetch date from last item in current set
            var date = new Date(_currentRenderedReplies[numCurrentRenderedReplies - 1].creationDate);
            nextStartingDate = CloudTalk.utils.ISODateString(date);
        }

        showLoadingIndicator(true);
        _startingPageNumber += _replyPageIncrement;
        _replyCursorDirection = "backward";
        _apiUser.getMessages({ conversationID:_currentConversationID,
                startingDate:nextStartingDate,
                searchDirection:_replyCursorDirection,
                messageCount:_replyReadIncrement},
            newPageSetLoadHandler);
        // and we're out of here, this is continued in the handler
    }


    /**
     *  get the previous set of pages going "forward" in time based on current
     *  currsor location, there is guard code to make sure you don't run off the
     *  end of the world
     *
     *  NOTE: we can work out way to get new replies, but lets wait until after
     *   we get this thing released.
     *
     */
    function getPreviousSetOfReplyPages() {
        var nextStartingDate;
        var numCurrentRenderedReplies = _currentRenderedReplies.length;

        if (_startingPageNumber < _replyPageIncrement)
            return; // there's no way there's any more previous replies left

        if (_replyCursorDirection === "forward") {
            nextStartingDate = _replyCursor.nextStartingDate;
        } else {
            // we have to go fetch date from first item in current set
            var date = new Date(_currentRenderedReplies[0].creationDate);
            nextStartingDate = CloudTalk.utils.ISODateString(date);
        }

        showLoadingIndicator(true);
        _startingPageNumber -= _replyPageIncrement;
        _replyCursorDirection = "forward";
        _apiUser.getMessages({ conversationID:_currentConversationID,
                startingDate:nextStartingDate,
                searchDirection:_replyCursorDirection,
                messageCount:_replyReadIncrement},
            newPageSetLoadHandler);
        // and we're out of here, this is continued in the handler
    }


    /**
     *  handle the loading of a new cursor page set either going backwards
     *  (older) or forwards (newer) in time. there should be no diffeneces
     *  between the two from this vantage point
     *
     *  @param {object} db cursor from the getMessages api call
     */
    function newPageSetLoadHandler(cursor) {
        _setReplyCursor(cursor);
        _setPagesInCurrentReplyCursor();
        showLoadingIndicator(false); // putting it here so we can see how fast the render is...
        renderReplyPageSet();
        renderReplyControlPageLinks();
        // note: seek has to happen last, after DOM is setup for the new pageset
        seekToPageInCurrentCursor((_replyCursorDirection === "forward") ? 5 : 1);
    }

    /**
     * does what it says, called after the login button has successfully
     * completed to change it to a logout button
     */
    function wireUpLogoutButton() {
        $(".loginButton").unbind("click").click(
            function () {
                CloudTalk.utils.deleteCookie("authToken");
                window.location.reload();
            }).html("Logout");
    }


    /**
     * showLoadingIndicator - put up or take down the loading indicator
     *
     * @param visibility {boolean} - true to show indicator, false to take down
     *
     * TODO: login needs cursor too... we need to have ability specify where
     * this goes, and which cursor to put up or we just need a second routine
     */
    function showLoadingIndicator(visibility) {
        if (visibility & $(".loadingIndicator").length == 0)
            $(".replyContainer").append("<div class='loadingIndicator'></div>");
        else
            $(".loadingIndicator").remove();
    }


    /**
     *  associate the the picturePopup with the selected items
     *
     *  @param selector optional {string or jquery} css selector or jquery to allow
     *   override of default selector, note the default selector should always pick up
     *   the initial post picture if it exists
     *  @return the jQuery for the selector
     */
    function setPicturePopupTriggers(selector) {
        selector = selector ? selector : ".contentImagePopup";
        return $(selector).each(function () {
            var self = this;
            // self is an actual div here, so it needs to be wrapped
            $(self).overlay({
                top:'10%',
                onBeforeLoad:function () {
                    var popupDiv = $("#picturePopup");
                    var url = $(self).attr("pictureUrl");

                    $("img", popupDiv).remove();
                    $($.format("<img id='popupDialogImage' src='%s' style='width:400px;'/>", [url])).appendTo(popupDiv);
                    _picturePopupTrigger = this.getTrigger();
                }
            });
        });
    }


////  TODO - this should be folded into the newPageSetLoadHandler
//// 
    /*
     * callback handler for getMessages; sets up the initial post div and the 
     * the repsonses div, at this point reply cursor is pointing at the first set of pages
     * @param messages {object} CT db cursor for message query (see getConversationMessages api doc)
     */
    function handleConversationReplies(cursor) {
        _setReplyCursor(cursor);
        renderReplyPageSet();
        initializeReplyPageControl();
        seekToPageInCurrentCursor(1);
    }


    /*
     * Callback handler for getPosts. It's used on the User's stream page.
     *  
     * 
     */
    function handleUserConversations(cursor) {
        CloudTalk.log("User Stream posts request recieved.");
        _replyCursor = cursor;
        renderConversationSet(cursor);
        CloudTalk.log("User's stream conversetaions rendered.");
        initializeConversationsPageControl();
        seekToPageInCurrentCursor(1);
    }

    /*
     * Populates the user's conversation. Used on the User's stream page. 
     *
     * 
     */
    function renderConversationSet(cursor) {
//        CloudTalk.log("Rendering User's stream conversations...");

        var items = $("#messageListItems");
        items.empty()
        _conversationSet = cursor.conversations;

//        CloudTalk.log("Starting User's stream conversations iteration...");
        $.each(_conversationSet,
            function (idx, conversation) {
                items.append(renderConversationItem(idx, conversation));
            }
        );

        resizeAccordingToUserAgent("repliesAreDone");
        $(".timeAgo").timeago(); // time fields
        setPicturePopupTriggers();
    }

    /**
     * emit HTML to render message item based on the cell media contents
     * text, audio, picture, video. create the DOM structure then resize
     * the maleable elements to fit the content
     *
     * @param message {object} message object object returned from inbox call
     * @return {string} HTML that renders the messageItem
     */
    function renderConversationItem(idx, conversation) {
//        CloudTalk.log("renderConversationItem("+idx+", "+conversation+"); called");
        var createDate = CloudTalk.utils.safeDateFromDateString(conversation.creationDate).toUTCString();
        var renderObj = renderConversationItemMedia(idx, conversation);
        var controlsHTML = renderObj.controlsHTML;
        var vControlsHTML = renderObj.vControlsHTML;
        var sender = renderObj.sender;
        var newItem = "";

        var replyText = (conversation.messageCount > 1) ? conversation.messageCount + "<br/> Replies" : "Reply";

        newItem = $($.format(
            "<div class='item conversationItem' idx='%s'>\
	            	<div class='itemContainer'>\
	             		<div class='itemContainerLeftBlock'>\
	             		    <div class='itemDate'>%s</div>\
			             	<div class='timeAgo' title='%s' style='line-height: 2em;' >&nbsp;</div>\
			                <div class='itemControls'>%s</div>\
			                <div class='itemMedia'>%s</div>\
			            </div>\
		                <div class='itemReply'><a href='/v2/m/"+conversation.conversationID+"'>" + replyText + "</a></div>\
		            </div>\
	            </div>",
            [idx, createDate, createDate, renderObj.controlsHTML, renderObj.mediaHTML]));

        //$(".itemContainer", newItem).css("height", String(renderObj.height)+"px");

        // Set video player handlers
        var videoPlayControl = $(".ctVideoControls", newItem);
        if (videoPlayControl.length > 0) {
            var url = videoPlayControl.attr("videoMediaURL");
            if (url == undefined) {
                url = videoPlayControl.attr("mediaURL");
            }
            var videoURL = CloudTalk.utils.fixResourceURL(url);
            videoPlayControl.attr("videoMediaURL", videoURL); // fix for running on dev stack
            var videoButton = $(".videoPlayButton", newItem);
            var videoPlaybackHandler = new PlaybackHandler(newItem, videoButton, /*"ctPlaybackControl_"+*/idx,
                "videoPlayerPlayButton.png",
                "videoPlayerPauseButton.png");
            videoButton.click(function () {
                videoPlaybackHandler.click();
            });
            $(".jp-video-play", newItem).click(function () {
                videoPlaybackHandler.click();
            });
        } else {
            // Set audio player handlers
            var button = $("img.playControl", newItem);
            if (button.length > 0) {
                var audioPlaybackHandler = new PlaybackHandler(button, button, idx,
                    "playbuttonbig@2x.png",
                    "pausebuttonbig@2x.png");
                // DT added unbind before binding to click event
                button.unbind("click").click(function () {
                    audioPlaybackHandler.click();
                });
            }
        }

        return newItem;
    }


    /**
     *  render the individual media sections of a conversation; manage the reality
     *  that a conversation is a bundle of mime types and may have text+audio or
     *  text+pic or text+pic+audio or just a pic or just an audio or text
     *
     * @param idx {index} of the item being rendered
     * @param conversation {object} representation of conversation object object returned from inbox call
     * @return {object} containing attributes HTML, height width etc that describe conversation rendering
     */
    function renderConversationItemMedia(idx, conversation, options) {
//        CloudTalk.log("renderConversationItemMedia("+idx+", "+conversation+", "+options+"); called");
        var maxTextLength = 4096;

        if (options !== undefined) {
            if (options.maxTextLength !== undefined)
                maxTextLength = options.maxTextLength;
        }

        // init our return object
        var renderObj = {};
        var mediaDiv = $("<div class='itemMedia'></div>");
        var itemText = $("<div class='itemText'></div>");
        var imageElement = null;
        var metrics = null;
        var mediaControlsHTML = ""

        mediaDiv.append(itemText);
        renderObj.sender = CloudTalk.utils.getSender(conversation);
        renderObj.types = {};

        for (i in conversation.firstMessageMediaContent) {
            var content = conversation.firstMessageMediaContent[i];

            switch (content.type) {
                case "text":
                    // userText takes precidence, overwrite and remove other classes
                    $(itemText).html(CloudTalk.utils.shortenText(content.caption, maxTextLength - renderObj.sender.length));
                    $(itemText).addClass("userText");
                    $(itemText).removeClass("audioText imageText videoText");
                    renderObj.types.text = 1;
                    break;

                case "audio":
                    // TODO: need to understand how video controls will be differnt from audio!!
                    mediaControlsHTML = $.format(
                        "<div id='ctPlaybackControl_%s' class='initialPostPlayControl ctAudioControls' mediaURL='%s' style='float:none;padding-left:10px;padding-top: 2px;'>\
                       <img class='playControl' src='%s/playbuttonbig@2x.png'/>\
                       <span class='duration'>%s</span>\
                       <div class='progress-wrapper' style='float: left; margin-top: -17px;padding-left: 14px;'>\
                         <div class='jp-progress'>\
                           <div class='progress jp-seek-bar'>\
                             <div class='elapsed jp-play-bar'></div>\
                           </div>\
                         </div>\
                       </div>\
                     </div>",
                        [idx, CloudTalk.utils.fixResourceURL(content.file), _imgBase, CloudTalk.utils.seconds2time(content.duration)]
                    );
                    addAutoText(itemText, "audioText");
                    renderObj.types.audio = 1;
                    renderObj.duration = content.duration;
                    break;

                case "video":
                    mediaControlsHTML = $.format(
                        '<div id="ctPlaybackControlImage_%s" class="videoThumb"\
			  style="background:url(%s); background-position: center center; background-repeat: no-repeat;">\
		     </div>\
		     <div class="jp-video-play" style="display:block;">\
		        <a href="javascript:;" class="jp-video-play-icon">play</a>\
		     </div>\
	             <div id="ctPlaybackControl_%s" class="ctVideoControls" \
		          mediaURL="%s" style="display:none;">\
		       <div style="float: left;">\
                          <img class="playControl videoPlayButton" src="%s/videoPlayerPlayButton.png"/>\
                       </div>\
		       <div class="progress-wrapper jp-progress">\
		          <div class="progress jp-seek-bar">\
			     <div class="elapsed jp-play-bar"></div>\
			  </div>\
		       </div>\
		     </div>',
                        [idx, CloudTalk.utils.fixResourceURL(content.file + "?size=thumbhd"), idx, CloudTalk.utils.fixResourceURL(content.file), _imgBase]
                    );

                    addAutoText(itemText, "videoText")
                    renderObj.types.video = 1;
                    renderObj.duration = content.duration;
                    break;

                case "image":
                    // vertical image 205x308 horizontal 308x205, reduce by half for screen item cell size (154x103)
                    // NOTE: the background repeat and position styles only seem to work here if they are inline
                    var url = CloudTalk.utils.fixResourceURL(content.file);
                    imageElement = $($.format(
                        "<div class='contentImage contentImagePopup'\
                          style='background:url(%s); background-repeat:no-repeat; background-position:center center;' rel='#picturePopup' pictureUrl='%s'></div>",
                        [url + "?size=thumbhd", url + "?size=screenhd"]));
                    // TODO: !!! fix access method getMessageImage() 
                    //	[CloudTalk.utils.getMessageImage(message, "thumbhd")]))
                    addAutoText(itemText, "imageText");
                    renderObj.types.image = 1;
                    break;
            }
        }

        // now finish up
        $(mediaDiv).append(imageElement);
        renderObj.controlsHTML = mediaControlsHTML;
        renderObj.mediaHTML = $(mediaDiv).outerHTML();

        metrics = CloudTalk.utils.textMetrics(itemText, _contentWidth);
        renderObj.height = metrics.height;
        renderObj.width = metrics.width;

        // this is a travesty adding a hard coded constant, but a combined mediaDiv metrics isn't working
        // yet, we need a better method that will calcualte the size of an arbitrary div...
        if (imageElement) {
            renderObj.height += 320; // 308 image height + 12 for margin
            renderObj.width += 320;
        }

        return renderObj;
    }

    function initializeConversationsPageControl() {
        _setTotalAvailablePages();
        _setPagesInCurrentReplyCursor();
        _startingPageNumber = 1;

        renderReplyControlPageLinks();

        $(".replyCount").html(String(_replyCursor.conversationTotal - 1) + " replies");
    }


    /**
     * Make the User's profile elements edititable if the loggedInUser is
     * the same user as the profile being viewed.
     */
    function enableUserProfileEditing() {
        // If User is logged in, prepare the update buttons and actions.
        if (_userIsLoggedIn && _feedUserID === _userID) {
            // Add the update button and the input for the username
            var displayNameContainer = $('#displayName');
            $(displayNameContainer).html(
                '<input type="text" id="displayNameChange" value="' + _displayName + '"/>\
                 <span class="grayButton updateButton"><a href="#"><span>Update</span></a></span>');
            // Set the update button action
            $('.updateButton a', displayNameContainer).click(function () {
                CloudTalk.log('Changing display name.');
                $('#displayNameChange').attr('disabled', 'disabled');
                var anchor = this;
                $('span', anchor).addClass('loading');
                _apiUser.setUserProfile({displayName:$('#displayNameChange').val()},
                    function () {
                        CloudTalk.log('Display name changed.');
                        $('#displayNameChange').removeAttr('disabled');
                        $('span', anchor).removeClass('loading');
                    },
                    function () {
                        $('#displayNameChange').removeAttr('disabled');
                        CloudTalk.log('Error while changing display name.');
                        $('span', anchor).removeClass('loading');
                    }
                );
            });

            // setup an iframe to point the avatarUploadForm to
            var avatarUploadIframe = $('<iframe>')
                .attr({ id:"avatarUploadIframe",
                    name:"avatarUploadIframe",
                    style:"visibility:hidden; width:0px; height:0px;border:none"
                })
                .appendTo(displayNameContainer)
                .load(function () {
                    // note we could use $(this).contents().find("pre").html() and get markup if needed
                    var response = $(this).contents().find("pre").text();
                    CloudTalk.log('Upload image done, response:' + response);
                    $("#avatarUploadButton").removeClass('loading');

                    // add a toast for user that gives feedback,
                    // 1. /Flash Sucks/.test(response)  we're ok
                    // 2. /invalid/.test(response) - bad authToken
                    if (/Flash Sucks/.test(response)) {
                        // refresh avatar
                        var avatar = $("#initialPost .avatar");
                        var avatarFrame = $("#initialPost .avatarFrame"); // This reloads the frame around the avatar
                        avatar.attr("src", avatar.attr("src")); // This reloads the newly uploaded avatar
                        avatarFrame.attr("src", avatarFrame.attr("src")); // This reloads the frame around the avatar
                    } else if (/invalid/.test(response)) {
                        alert("Error: NOTE TO SELF, token expired, we need to find resolution");
                        // JOS TODO: bug, if the authToken goes stale we'll get the bad authToken
                    }
                });

            // Add the "Change avatar" and the "Logout of twitter" buttons, the avatar upload is
            // a form tied to the "Charnge Avatar" button, the logout of twitter will be accomplished
            // with a click handler
            var avatarUploadForm = $('<form>')
                .attr({
                    id:"avatarUploadForm",
                    action:$.format("%s/profile/uploadImage?userTicket=%s", [_apiUser.__apiRoot, _apiUser.authToken]),
                    method:"POST",
                    enctype:"multipart/form-data",
                    accept: "image/*",
                    target:"avatarUploadIframe"
                })
                .appendTo(displayNameContainer.parent())
                .append("<div>\
                            <span class='grayButtonBig buttonWithLoader'>\
                               <a href='#'><span id='avatarUploadButton'>Change<br/>Avatar</span></a>\
                               <input type='file' id='changeAvatar' name='request' class='fileUploadButton'/>\
                            </span>\
                            <span id='logoutOfTwitterButton' class='grayButtonBig'>\
                               <a href='#'>Logout of<br/>Twitter</a>\
                            </span>\
                            <span id='displayNewPostForm' class='grayButtonBig'>\
                               <a href='#'>New Post</a>\
                            </span>\
                         </div>");

            // on change of state of the file input element in the uploadAvatarForm
            $('#changeAvatar').change(function () {
                CloudTalk.log('Submitting the image.');
                $("#avatarUploadButton").addClass('loading');
                avatarUploadForm.submit();
            });
            // By DT. This is a nice feature to have, once user clicks on their own avatar it pops up a file select cmd
            $("#initialPost .avatarFrame").unbind('click').bind("click", function() {
                $('#changeAvatar').click();
            });
        }
    }


    /**
     * VOICES application initialization.
     *  We're down to just one API call to get started
     *
     *  By DT: This is the initializer which connects all workflow including post, reply, login, etc
     *  If ".scrollable" class element is found - api getPosts or api getMessages is executed
     *
     *  TODO:
     *   1. rig remote logging up to be turned on by parameter
     *   2. turn off all logging for production, will need to shim that into the logging methods
     *
     */
    function initializeVoicesApp(cursor) {
        if ($(".scrollable").length > 0) {
            // enable the scrollable and disable text selection in the list, we still use
            // the scrollable this needs to happen before we render as it puts the scrolling
            // api in play
            $(".scrollable").scrollable({ touch: false, vertical:true, keyboard:false, mousewheel:false, speed:0 });
            $(".scrollable").disableTextSelect();

            CloudTalk.log($("#messageList").data("scrollable").getConf());

            renderInitialPost(); //@TODO: cnicola check this later

            // Check the type of current page (depending on the pageType var value)
            if (_pageType == "stream") {
                CloudTalk.log("Going into User Stream init...");

                enableUserProfileEditing();

                _apiUser.getUserProfile({ streamID:_userStream }, function(result){
                    _apiUser.streamID = result.streamID;
                });

                CloudTalk.log("Making User Stream posts request...");
                _apiUser.getPosts({ streamID:_userStream },
                    handleUserConversations)



            } else { // By default treat the page as a conversation page

                // get and then render the reply messages
                _apiUser.getMessages({ conversationID:_currentConversationID,
                        startingDate:"now",
                        searchDirection:"backward",
                        messageCount:_replyReadIncrement},
                    handleConversationReplies);

            }
            if ($("#initialPost .avatarFrameLarge").length > 0) {
                $("#initialPost .avatarFrameLarge").unbind('click').bind("click", function() {
                    var username = $("#initialPost .avatarFrameLarge").attr("username");
                    document.location = '/v2/vu/' + username;
                });
            }
        }
        setupRecordControl();

        CloudTalk.mediaPlayer = new CloudTalk.CTPlayer();

        // any additional triggers should be added to this list. you must make sure that
        // none of the selectors inadvertantly point to something inside the onboarding
        // dialog, that would be bad :)
        var onboardingWorkflowSelectors = {};

        if (_userIsLoggedIn) {
            wireUpLogoutButton();
            onboardingWorkflowSelectors["#voicesLogo"] = "#twitterConnectWorkflow";
            // TODO: removing this for now, if first reply is too slow, we can have it preloaded here,
            //   but the concern here is that we limit the work done on each page load
            //_apiUser.getGroups({}, function(groups) { _currentUser.groups = groups; });
        } else {
            onboardingWorkflowSelectors["#loginButton"] = "#loginOrSignupWorkflow";
            onboardingWorkflowSelectors["#recordingSend"] = "#loginOrSignupWorkflow";
            onboardingWorkflowSelectors["#voicesLogo"] = "#twitterConnectWorkflow";
            onboardingWorkflowSelectors["#twitterConnect"] = "#twitterConnectWorkflow";
            // onboardingWorkflowSelectors["#conversationReply"] = "#replyDialogWorkflow";
        }

        _onboardingWorkflow = new OnboardingWorkflow(onboardingWorkflowSelectors);
        if (_debug)
            window.owf = _onboardingWorkflow;
    }




    /**
     *  Browser entry point, just set up the outer environment here and then hand off to
     *  the application initialization sequence
     *
     *  TODO:
     *   1. debug stuff parameterized from server
     */
    $(document).ready(
        function () {
            _debug = true;
            if (_debug) {
                CloudTalk.log($().jquery);
                CloudTalk.log(window.location.href);
                CloudTalk.log(navigator.appVersion);
            }
//            CloudTalk.log("Document.Ready loaded 1st");
            // collect validated authToken from server
            var authToken = CloudTalk.params.authToken;

            // the authToken we get may well be the one that was already there, but the one
            // we get from the server is always correct/confirmed, so we write it out
            // TODO: consider having the authToken set serverside
            CloudTalk.utils.createCookie("authToken", authToken);

            // check to see if the user is logged in, this is the case if the user is not an anonymous
            // login, which curently is denoted by the userName "panamaAnonymousUser"

            _userIsLoggedIn = (CloudTalk.params.userName !== "panamaAnonymousUser")

            // create an api instance bound to user, then initialize app
            _apiUser = new CloudTalk.API(authToken, _userID);

            initializeVoicesApp();

            // export some stuff, the voices app is not a first class object yet, lots of work
            // needed to do that, so we just expose just what we need. We do it here since we 
            // are using resources that are only available after booting up
            window.CloudTalk = window.CloudTalk || {};
            window.CloudTalk.voices = {
                onboardingWorkflow:_onboardingWorkflow,
                app:this
            }

            // remote logging from client back to server
            if (_debug) {
                setupRemoteLogger();
                window.CloudTalk._debug = true;
            }

            /// these are regression tests for websockets and notifications, remove for deployment
            // websocket test
            //_ctSocket = connect();
            // execute 3 sec from now to make sure that the flash widget is initialized,
            // we will need to move tihs into "our own" web_socket.js later
            // this next test requires a valid _userID
            //setTimeout(testNotificationChannel, 3000);
        });


    //// STUFF THAT STILL NEEDS DISPOSITION
    ////
    ////   websockets
    ////   notifications


    function testCookies() {
        /** notes:
         *    1. httponly worth using
         *    2. secure can't be set unless you are using https scheme so it's not going to work for us here
         */
            // cookie tests
        CloudTalk.utils.createCookie("testNonsecure", "haveLogin", { days:1 });
        // can only set when page was loaded via https
        CloudTalk.utils.createCookie("testSecure", "securePassword", { secure:1, days:1 });
        CloudTalk.utils.createCookie("testHttpOnlyWithDays", "withdays", { httpOnly:1, days:1 });
        CloudTalk.utils.createCookie("testHttpOnlyWithOutDays", "withoutdays", { httpOnly:1 });
        //	    CloudTalk.utils.deleteCookie("testHttpOnlyWithDays");
        //	    CloudTalk.utils.deleteCookie("testHttpOnlyWithOutDays");
        //	    CloudTalk.utils.deleteCookie("testNonsecure");
        //	    CloudTalk.utils.deleteCookie("testSecure");
    }


    function testNotificationChannel() {
        CloudTalk.log("testing notifications");
        WebSocket.setupCloudTalkNotificationChannel(_apiUser.userID,
            _apiUser.authToken,
            _appAnchor,
            "http://integration.cloudtalk.me:7781",
            "1.0");
        var channel = WebSocket.getCloudTalkNotificationChannel();
        CloudTalk.log("got channel: " + channel);

    }

    function send(socket, userId, token) {
        try {
            CloudTalk.log("sending now");
            socket.send(JSON.stringify(connectInfo));
        } catch (exception) {
            CloudTalk.log(exception);
        }
    }

    function connect() {
        // Let the library know where WebSocketMain.swf is:
        //WEB_SOCKET_FORCE_FLASH = true;
        //WEB_SOCKET_SWF_LOCATION = "WebSocketMainInsecure.swf";
        WEB_SOCKET_DEBUG = true;

        //var host = "ws://integration.cloudtalk.me:7781/";  
        var host = "ws://integration.cloudtalk.me:8181/";
        //var host = "ws://localhost:8081/";  
        //var host = "ws://echo.websocket.org/";  
        var socket = null;

        try {
            // need this until policy server is back up on port 843
            //CloudTalk.log("setting my own policy file");
            //WebSocket.loadFlashPolicyFile("http://integration.cloudtalk.me/crossdomain.xml");

            CloudTalk.log("connecting now");
            socket = new WebSocket(host);

            socket.onopen = function (event) {
                CloudTalk.log('Socket Status: ' + socket.readyState + ' (open)');
                CloudTalk.log(event);
            }

            socket.onmessage = function (event) {
                CloudTalk.log('Socket Received: ' + event.data);
                CloudTalk.log(event);
            }

            socket.onclose = function (event) {
                CloudTalk.log('Socket Status: ' + socket.readyState + ' (Closed)');
                CloudTalk.log(event);
            }

            socket.onerror = function (event) {
                CloudTalk.log('Socket Error: ' + event + ' (Closed)');
                CloudTalk.log(event);
            }

            CloudTalk.log(socket);

        } catch (exception) {
            CloudTalk.log('Error' + exception);
        }
        return socket;
    }


})(window);
