function twidget() {
    this.defaults = {
        'refresh'           : 60,                               // 1 min refresh
        'limit'             : 10,                               // 10 tweets per refresh, max 100
        'createHrefs'       : true,                             // Create href links on tweets
        'container'         : 'twidget',                        // wrapper div container id
        'enablePosting'     : false,                            // Set to true to enable posting, requires a valid defaults.apiUrl
        'postButtonText'    : 'Post',                           // Post button text
        'loginButtonText'   : 'Login',                          // Login button text
        'author'            : 1,                                // Show author, 0, 1
        'avatar'            : '2',                                // 0, 1 or 2 => off, norm, mini
        'apiUrl'            : '',                               // Proxy url
        'spinnerUrl'        : '/images/twidget/spinner.gif',    // Spinner url
        'prepend'           : '',                               // Prepend this to the status update (tweet)
        'append'            : '',                               // append this to the status update (tweet)
        'postWaitSecs'      : '30',                             // Time delay after a status post
        'dateFormat'        : 'mm/dd/yyyy',                     // Date/Time formating see: http://blog.stevenlevithan.com/archives/date-time-format

        // Search params
        'from'          : '',                           // From a user
        'to'            : '',                           // To a user
        'mention'       : '',                           // with @user
        'tag'           : '',                           // #tag

        'loadText'      : 'Loading tweets...'           // Loading text on initial load
    };

    this.showSpinner = function() {this.spinnerImage.show();};
    this.hideSpinner = function() {this.spinnerImage.hide();};
    this.dbgPrint = function(e) {if(typeof(console) !== 'undefined'){if(console && console.log) {console.log("twidget: " + e);}}};
    this.dbgDir = function(e) {if(typeof(console) !== 'undefined'){if(console && console.dir) {console.dir(e);}}};
	this.disablePosting = function(self) {
	    self.postButton.hide();
	    self.waitSecs = self.defaults.postWaitSecs;
	    self.setPostTimeCookie();
	    self.waitContainer.html('You have ' + self.waitSecs + ' secs. until you may post again');
	    self.waitContainer.fadeIn(2000);
	    self.waitTimer = setInterval(function() {
	        self.waitSecs--;
	        if(self.waitSecs <= 0) {
	            clearInterval(self.waitTimer);
	            self.waitContainer.hide();
	            if(self.loggedIn()) {
	                self.postButton.show();
	            }
	            self.delPostTimeCookie();
	        }
	        self.waitContainer.html('You have <strong>' + self.waitSecs + ' secs.</strong> until you may post again');
	    }, 1 * 1000);
	};	
}

twidget.prototype.init = function(options) {
    try {
        $.extend(this.defaults, options);

	    this.cookieUser = this.defaults.container + 'user';
	    this.cookiePass = this.defaults.container + 'pass';

        // god knows why it's removing the trialing whitespace in the module but it is somehow...
        this.defaults.allowedLen = 140 - (this.defaults.prepend.length + this.defaults.append.length + 2);
        
        // Tabbed in for your [visual] pleasure
        var markup = '';
        markup += '<div id="' + this.defaults.container + '" class="twidget">';
			if(this.defaults.enablePosting === '1') {
            markup += '<div id="' + this.defaults.container + 'Login" class="twlogon">';
                markup += '<h4>Sign into your Twitter account</h4>';
                markup += '<input id="' + this.defaults.container + 'Username" type="text" />';
                markup += '<input id="' + this.defaults.container + 'Password" type="password" />';
                markup += '<input id="' + this.defaults.container + 'LoginButton" type="button" value="' + this.defaults.loginButtonText + '" />';
                markup += '<div class="clear" /></div>';
            markup += '</div>';

            markup += '<div id="' + this.defaults.container + 'Logout" class="twlogout">';
                markup += '<h4>Post a Twitter message</h4>';
                markup += '<span id="' + this.defaults.container + 'LogoutText"></span>';
                markup += '<input id="' + this.defaults.container + 'LogoutButton" type="button" value="Logout" />';
                markup += '<div class="clear" /></div>';
            markup += '</div>';
			}
            markup += '<div id="' + this.defaults.container + 'Spinner" class="twspinner">';
                markup += '<img src= "' + this.defaults.spinnerUrl + '" />';
                markup += '<div class="clear" /></div>';
            markup += '</div>';

            markup += '<div id="' + this.defaults.container + 'Post" class="twpost">';
                markup += '<textarea id="' + this.defaults.container + 'PostText" rows="3"></textarea>';
                markup += '<div id="' + this.defaults.container + 'Counter"></div>';
                markup += '<input id="' + this.defaults.container + 'PostButton" type="button" value="' + this.defaults.postButtonText + '" />';
                markup += '<div class="clear" /></div>';
            markup += '</div>';
            

            markup += '<div id="' + this.defaults.container + 'Wait" class="twwait"></div>';
            markup += '<div class="clear"></div>';
            markup += '<div id="' + this.defaults.container + 'Info" class="twinfo"></div>';
            markup += '<div class="clear"></div>';
            markup += '<div id="' + this.defaults.container + 'Tweets" class="twtweets"></div>';
        markup += '</div>';
        document.write(markup); // To browser

        // Get the container jQuery objects for use later
        // Hide containers that get toggled by loginToggle()
        this.container = $('#' + this.defaults.container);

        this.loginContainer = $('#' + this.defaults.container + 'Login').hide();
        this.logoutContainer = $('#' + this.defaults.container + 'Logout').hide();
        this.logoutText = $('#' + this.defaults.container + 'LogoutText');
        this.postContainer = $('#' + this.defaults.container + 'Post').hide();
        this.counterContainer = $('#' + this.defaults.container + 'Counter');
        this.waitContainer = $('#' + this.defaults.container + 'Wait').hide();
        this.infoContainer = $('#' + this.defaults.container + 'Info');
        this.tweetsContainer = $('#' + this.defaults.container + 'Tweets');
        this.spinnerContainer = $('#' + this.defaults.container + 'Spinner');
        this.spinnerImage = $('#' + this.defaults.container + 'Spinner > img');
        this.postTextArea = $('#' + this.defaults.container + 'PostText');

        if(this.defaults.from || this.defaults.to || this.defaults.mention || this.defaults.tag) {
            this.infoContainer.html('<span>' + this.defaults.loadText + '</span>');
        }
        else {
            // Nothing to tweetypie
            this.infoContainer.html( '<span>Invalid search parameters.</span>' );
            return;
        }
        
        // Button handlers
        var self = this;

        this.loginButton = $('#' + this.defaults.container + 'LoginButton').bind('click', function(e) {
            self.onLoginClick(e);
        });
        this.logoutButton = $('#' + this.defaults.container + 'LogoutButton').bind('click', function(e) {
            self.delCredentials();
            self.toggleLogin();
        });
        this.postButton = $('#' + this.defaults.container + 'PostButton').bind('click', function(e) {
            var msg = self.defaults.prepend + ' ' + $('#' + self.defaults.container + 'PostText').val() + ' ' + self.defaults.append;
            self.proxyApiCall( {
                'apicall': 'statuses/update',
                'status' : msg
                }, self.onPostComplete);
        });

        this.postTextArea
            .focus(function(){self.updateCounter();})
            .blur(function(){self.updateCounter();})
            //.keydown(function(){self.limitCounter();})
            .keydown(function(){self.updateCounter();})
            .keypress(function(){self.updateCounter();})
            .keyup(function(){self.updateCounter();});
        this.updateCounter();

        if(this.getPostTimeCookie()) {
            this.disablePosting(this);
        }
        
        this.toggleLogin();
        this.onSearchTwitter();

        this.intervalId = setInterval(function() {
                self.onSearchTwitter();
            }, this.defaults.refresh * 1000);
    }
    catch(e) {
        this.dbgPrint(e);
    }        
};

twidget.prototype.updateCounter = function() {
    if(this.postTextArea.val().length >= this.defaults.allowedLen) {
        this.postTextArea.val(postTextArea.val().substr(0, this.defaults.allowedLen));
    }
    this.counterContainer.html(this.defaults.allowedLen - this.postTextArea.val().length);
};

//twidget.prototype.limitCounter = function(e) {
//    if(this.postTextArea.val() >= this.allowedLen && e.keyCode != 8)
//        return false;
//    return true;
//};

twidget.prototype.onLoginClick = function(e) {
    this.username = $('#' + this.defaults.container + 'Username').val();
    this.password = $('#' + this.defaults.container + 'Password').val();
    
    if(!this.validateCredentials()) {
        return false;
    }

    this.proxyApiCall({'apicall': 'account/verify_credentials'}, this.onLoginComplete);
};

twidget.prototype.onLoginComplete = function(self, response) {
    if(response.resultArray.http_code == '200') {
        self.setCredentials();
        self.toggleLogin();
    }
    else {
        self.showInfoMessage(response.result.error);
    }
};

twidget.prototype.onPostComplete = function(self, response) {
    if(response.resultArray.http_code == '200') {
        self.showInfoMessage('Thank you for posting a tweet!');
        self.postTextArea.val('').blur();

        var avatar = response.result.user.profile_image_url;
        var from = response.result.user.screen_name;
        var tweet = response.result.text;
        var date = new Date(response.result.created_at);
        var fdate = date.format(self.defaults.dateFormat);

        var markup = '';
        // Dump them out to browser
        markup += '<li class="tweet">';
            markup += '<span class="tweetAvatar"><a class="linkOut" href="http://twitter.com/' + from + '"><img src="' + avatar + '" class="twidgetAvatar" /></a></span><!-- /tweetAvatar -->';
            markup += '<span class="tweetAuthor"><a class="linkOut" href="http://twitter.com/' + from + '">' + from + '</a></span><!-- /tweetAuthor -->';
            markup += '<span class="tweetText">' + tweet + '<br />' + fdate + '</span><!-- /tweetText -->';
        markup += '</li><!-- /tweet -->';
        
        $('.' + self.defaults.container + 'TweetList').prepend(markup);
        
        self.disablePosting(self);
        
    }
    else {
        self.showInfoMessage(response.result.error);
    }
};


twidget.prototype.onSearchTwitter = function() {
    var self = this;
    try {
        this.showSpinner();
        var query = '';

        query += (this.defaults.from) ? 'from:' + this.defaults.from + ' OR ' : '';
        query += (this.defaults.to) ? 'to:' + this.defaults.to + ' OR ' : '';
        query += (this.defaults.mention) ? '@' + this.defaults.mention + ' OR ' : '';
        query += (this.defaults.tag) ? '#' + this.defaults.tag + ' OR ' : '';

        // Remove trailing ' OR '
        query = query.substring(0, query.length - 4);
        query = encodeURIComponent(query);
        var url = 'http://search.twitter.com/search.json?rpp=' + this.defaults.limit + '&q=' + query

        $.ajax({
            url : url,
            cache: false,
            type : 'GET',
            success : function(resp) {
                self.onSearchTwitterCallback(resp);
            },
            error : function(XMLHttpRequest, textStatus, errorThrown) {
                self.dbgPrint(textStatus + ' ' + errorThrown);
            },
            complete : function(XMLHttpRequest, textStatus) {
                self.hideSpinner();
            },
            dataType : 'jsonp'
        });        
    }
    catch(e) {
        self.hideSpinner();
        self.dbgPrint(e);
    }
};

twidget.prototype.onSearchTwitterCallback = function(json) {
    try {
        var markup = '<ul class="tweetList ' + this.defaults.container + 'TweetList">';
        for(var i in json.results) {
            // Get tweet info into intermediate variables
            var from = json.results[i].from_user;
            var avatar = json.results[i].profile_image_url;
            
            if(this.defaults.avatar === '2') {
                avatar = avatar.replace(/normal.jpg/i, 'mini.jpg');
            }
            var tweet = this.createLinks(json.results[i].text);
            var date = new Date(json.results[i].created_at);
/*             var fdate = date.format(this.defaults.dateFormat); */
            var fdate = date.pretty();

            // Dump them out to browser
			var line = (i%2) ? ' odd' : ' even';
            markup += '<li class="tweet'+line+'">';
                if(this.defaults.avatar != 0) {
                    markup += '<span class="tweetAvatar"><a class="linkOut" href="http://twitter.com/' + from + '"><img src="' + avatar + '" class="twidgetAvatar" /></a></span><!-- /tweetAvatar -->';
                }
                if(this.defaults.author != 0) {
                    markup += '<span class="tweetAuthor"><a class="linkOut" href="http://twitter.com/' + from + '">' + from + '</a></span><!-- /tweetAuthor -->';
                }
                markup += '<span class="tweetText">' + tweet + '<br />' + fdate + '</span><!-- /tweetText -->';
            markup += '<div class="clear"></div></li><!-- /tweet -->';
            //a href to wrap the author and avatar upon parameter needs class="linkOut" INSIDE the span, ALL links need class="linkOut"
        }
        markup += '</ul><!-- /tweetList -->';
        this.infoContainer.html('');
        this.tweetsContainer.html(markup);
    }
    catch(e) {
        this.dbgPrint(e);
    }
};

twidget.prototype.createLinks = function(inStr)
{
    if(!this.defaults.createHrefs)
        return inStr;
        
    // Array of pattern/replace strings. arr0 finds all 'http:// OR www. xxxxx'    arr1 finds all @xxxxx
    // and replaces converts them to a href links
    var pattern = /((http:\/\/|www\.)[^\s]*)/g;
    var replace = '<a class="linkOut" href="$1">$1</a>';
            
    inStr = inStr.replace(pattern, replace);

    pattern = /(@)([a-zA-Z0-9-_]+)/g;
    replace = '$1<a class="linkOut" href="http://twitter.com/$2">$2</a>';
    
    inStr = inStr.replace(pattern, replace);
    
    return inStr;
};

twidget.prototype.proxyApiCall = function(twitterParams, callback) {
    if(typeof(twitterParams) == 'undefined')
        return;
    if(typeof(callback) == 'undefined')
        return;

    if(!this.validateCredentials(false)) {
        return false;
    }

    this.showSpinner();
    
    var url = this.defaults.apiUrl;
    var data = {'name': this.username,'pass': this.password};
	
    for(var i in twitterParams) {
        if(typeof(i) != 'undefined') {
            data[i] = twitterParams[i];
        }
    }
 
    var self = this;
    var cb = callback;

    $.ajax({
        url : self.defaults.apiUrl,
        cache: false,
        type : 'GET',
        data: data,
        success : function(resp) {
            cb(self, resp);
        },
        error : function(XMLHttpRequest, textStatus, errorThrown) {
            self.dbgPrint(textStatus + ' ' + errorThrown);
        },
        complete : function(XMLHttpRequest, textStatus) {
            self.hideSpinner();
        },
        dataType : 'jsonp'
    });        
};

twidget.prototype.toggleLogin = function() {
    if(this.loggedIn()) {
        this.loginContainer.hide();
        this.postContainer.show();
        this.logoutText.html('You are logged in as ' + this.username + '<br />&quot;' + this.defaults.append + '&quot; will be added to your tweet');
        this.logoutContainer.show();
    }
    else {
        this.postContainer.hide();
        this.logoutContainer.hide();
        this.loginContainer.show();
    }
};

twidget.prototype.loggedIn = function() {
    this.getCredentials();
    if(this.username == '' || this.password == '')
        return false;
    return true;
};

twidget.prototype.showInfoMessage = function(message, time) {       // time is msecs
    var displayTime = 1000 * 5;
    if(typeof(time) != 'undefined') {
        displayTime = time * 1000;
    }

    if(this.infoTimer > 0) {
        clearInterval(this.infoTimer);
    }
    
    var self = this;
    this.infoContainer.hide().text(message).slideDown(600);
    setTimeout(function() { 
            clearInterval(self.infoTimer); 
            self.infoContainer.slideUp(600); 
        }, displayTime );
};

twidget.prototype.validateCredentials = function(displayError) {
    try {
        if(typeof(displayError) == 'undefined') {
            displayError = true;
        }
    
        if(typeof(this.username) != 'undefined') {
            if(this.username == 'username' || this.username.length < 4) {
                if(displayError)
                    this.showInfoMessage('Invalid or too short username');
                return false;
            }
        }
        else {
            if(displayError)
                this.showInfoMessage('Invalid username');
            return false;
        }
        
        if(typeof(this.password) != 'undefined') {
            if(this.password == 'password' || this.password.length < 4) {
                if(displayError)
                    this.showInfoMessage('Invalid or too short password');
                return false;
            }
        }
        else {
            if(displayError)
                this.showInfoMessage('Invalid password');
            return false;
        }
    }
    catch(e) {
        return false;
    }

    return true;
};

twidget.prototype.getCredentials = function() {
    try {
        Cookies = new CookieHandler();
        this.username = Cookies.getCookie(this.cookieUser);
        this.password = Cookies.getCookie(this.cookiePass);
    }
    catch(e) { 
        this.username = '';
        this.password = '';
    }
    
        
    if((typeof(this.username) == 'undefined') || (this.username == 'null') || (this.username == null)) {
        this.username = '';
    }
    if((typeof(this.password) == 'undefined') || (this.password == 'null') || (this.password == null)) {
        this.password = '';
    }
};

twidget.prototype.setCredentials = function() {
    try {
        Cookies = new CookieHandler();
        Cookies.setCookie(this.cookieUser, this.username);
        Cookies.setCookie(this.cookiePass, this.password);
    }
    catch(e){
    }
};

twidget.prototype.delCredentials = function() {
    try {
        Cookies = new CookieHandler();
        Cookies.setCookie(this.cookieUser, null);
        Cookies.setCookie(this.cookiePass, null);
    }
    catch(e){
    }

    this.username = '';
    this.password = '';
};

twidget.prototype.getPostTimeCookie = function() {
    try {
        Cookies = new CookieHandler();
        if(!Cookies.getCookie(this.cookiePost))
            return false;
    }
    catch(e) { 
        return false;
    }
    
    return true;
};

twidget.prototype.setPostTimeCookie = function() {
    try {
        Cookies = new CookieHandler();
        Cookies.setCookie(this.cookiePost, 'post-delay', this.defaults.postWaitSecs);
    }
    catch(e){
    }
};

twidget.prototype.delPostTimeCookie = function() {
    try {
        Cookies = new CookieHandler();
        Cookies.deleteCookie(this.cookiePost);
    }
    catch(e){
    }
};