var Twitter = (function (){
    var Twitter = $H();
    
    var Tweet = new Class({
        // represents a status update on twitter.
        'initialize': function init (id, screenName, avatar, createdAt, text, reply) {
            this.id = id;
            this.screenName = screenName;
            this.avatar = avatar;
            this.reply = reply;
            this.text = text;
            this.createdAt = Date.parse(datefix(createdAt));
        },

        // toKey() -> string: generates a string key to store this Tweet in a Hash
        'toKey': function () {
            return "<Twitter:{screenName} #{id}>".substitute(this);
        },

        'toElement': function () {
            if (!(this.element)) this.element = Twitter.templates.basic(this);
            return this.element;
        }
    });

    Tweet.extend({
         // Tweet.fromHash(h) -> Tweet
         //
         // A utility to process the JSON format that the Twitter API returns.
         //
         // It's a little convoluted because the Twitter Search API returns a
         // slightly different format
        'fromHash': function (h) {
            var reply = h.get('in_reply_to_status_id') ? {
                'screenName': h.get('in_reply_to_screen_name'),
                'userID':     h.get('in_reply_to_user_id'),
                'statusID':   h.get('in_reply_to_status_id')
            } : null ;

            return new Twitter.Tweet(
                h.get('id'), 
                (h.has('user') ?  h.get('user')['screen_name'] : h.get('from_user')),
                (h.has('user') ?  h.get('user')['profile_image_url'] : h.get('profile_image_url')),
                h.get('created_at'), h.get('text'), reply
            );
        }
    });
    
    Twitter.extend({'Tweet': Tweet});

    var Timeline = new Class({
        // Contains the Tweets recieved by a particular API Call.
        // Periodically repeats that API call to update our 
        // collection of Tweets with the server.

        'Implements': [Options, Events],
        'initialize': function (options) {
            this.setOptions(options);
            this.latest = null;
            this.tweets = [];

            this.request = new Request.JSONP({
                method: 'get',
                url: this.queryUrl,
                onComplete: this.addData.bind(this)
            });

            if (this.options.updateOnStart) this.update();
            if (this.options.updatePeriod) this.startUpdates();
        },
        
        'options': {
            'count': 10,
            'updateOnStart': true,
            'updatePeriod': 90000
            // onUpdate: $empty
        },

        'timer': null,

        'startUpdates': function (period) {
            period = period || this.options.updatePeriod;
            this.timer = this.update.periodical(period, this);
            return this;
        },

        'stopUpdates': function () {
            $clear(this.timer);
            return this;
        },

        'queryUrl': "http://api.twitter.com/1/statuses/public_timeline.json",

         // get the next n tweets (since this.latest)
         // add them to this.tweets and fire the update event
        'update': function (n) {
            var data = this.sendData;
            
            if (this.latest != null) data = $merge(data, {'since_id': this.latest });

            this.request.send({'data': data});

            return this;
        },

        'addData': function (data) {
            var newTweets = $splat(data).map(function (n) {
                return Tweet.fromHash($H(n));
            });
            
            if (newTweets.length) { 
                this.tweets = newTweets.concat(this.tweets);
                this.latest = $H(newTweets[0]).get('id');
                this.fireEvent('update', [this, newTweets]);
            }
        },

        'toElement': function () {
            var element = new Element('div', {'class': 'twitterStream'});
            
            element.adopt(this.tweets);

            this.addEvent(
                'update', function (tweets, newTweets) {
                    element.adopt(newTweets);
                });

            element.store('timeline', this);
            return element;
        }
    });

    var UserTimeline = new Class({
        'Extends': Timeline, 
        'initialize': function (username, options) {
            this.username = username.toLowerCase();
            this.userData = null;
            this.sendData = {'screen_name': this.username, 'count': this.options.count || 9};
            this.parent(options);
        },

        'addData': function (data) {
            this.parent(data);

            if(!this.userData) {
                this.userData = userData.get(this.username);
                this.fireEvent("updateUserData", [this]);
            }
        },

        'userData': null,
        'queryUrl': "http://twitter.com/statuses/user_timeline.json"
    });


    var SearchTimeline = new Class({
        'Extends': Timeline,

        'initialize': function (searchTerm, options) {
            this.setOptions(options);
            this.searchTerm = searchTerm;
            this.sendData = {'q': searchTerm, 'rpp': this.options.count};
            this.parent(options);
        },

        'queryUrl': "http://search.twitter.com/search.json",

        'addData': function (data) {
            if ('results' in data) this.parent(data.results);
        }
    });    

    var ListTimeline = new Class({
        'Extends': Timeline,

        'initialize': function (user, listName, options) {
            var str = "http://api.twitter.com/1/{user}/lists/{listName}/statuses.json";
            this.queryUrl = str.substitute({
                'user': user,
                'listName': listName
            });

            this.parent(options);
        }
    });

    Twitter.extend({'UserTimeline': UserTimeline, 'SearchTimeline': SearchTimeline, 'ListTimeline': ListTimeline});
    
    Twitter.extend({'templates': $H()});

    Twitter.templates.extend({
        'basic': function (tweet) {
            var element = new Element('div', {'class': "tweet"});
            var T = Twitter.templates;

            element.adopt(
                T.renderAvatar(tweet),
                T.renderScreenName(tweet),
                T.renderBody(tweet),
                T.renderTimestamp(tweet)
            );

            if (tweet.reply) element.adopt(T.renderReply(tweet));

            return element;
        },

        'renderAvatar': function (tweet){
            return new Element('img', {'src': tweet.avatar, 'class': "avatar"});
        },

        'renderBody': function (tweet){
            return tweetBody(tweet.text);
        },

        'renderTimestamp': function (tweet) {
            var timestamp = new Element('a', {
                'class': 'date',
                'href': "http://twitter.com/{screenName}/statuses/{id}".substitute(tweet),
                'text': $try(
                    function () { return tweet.createdAt.timeDiffInWords() },
                    $lambda("A while ago"))
            });

            (function () {
                timestamp.set('text', tweet.createdAt.timeDiffInWords());
            }).periodical(60000);

            return timestamp;
        },

        'renderScreenName': function (tweet) {
            return new Element('a', {
                'text': tweet.screenName,
                'class': "screenName",
                'href': "http://twitter.com/{screenName}".substitute(tweet)
            });
        },

        'renderReply': function (tweet) {
            return new Element('a', {
                'class': 'reply',
                'href': "http://twitter.com/{screenName}/statuses/{statusID}".substitute(tweet.reply),
                'text': "in reply to @{screenName}".substitute(tweet.reply)
            });
        }
    });

    var View = new Class({
        'Implements': [Options, Events],

        'initialize': function (options) {
            this.setOptions(options);
            this.subscribe.apply(this, $splat(options.feeds));
            this.tweets = [];
            this.htmlCache = $H();
        },

        'options': {
            'template': Twitter.templates.basic,
            'count': 5,
            'feeds': []
        },

        'subscriptions': [],        
        'watchers': [],        

        'subscribe': function (timeline/*, ... */) {
            var view = this;

            $A(arguments).each(
                function (t) {
                    var timeline = timelineify(t);
                    var watcher = function (source, newTweets) {
                        view.add(newTweets, this);   
                    }; 

                    timeline.addEvent('update', watcher);
                    view.subscriptions.extend([timeline]);
                    view.watchers.extend([watcher]);
                });

            // timelineify(string) -> Timeline
            // timelineify(Timeline) -> Timeline
            //
            // Always tries to return a Timeline.
            //
            // Converts @name and #tag to a UserTimeline and a
            // SearchTimeline respectively.
            function timelineify (t) {
                var listP = /^@([a-z-_]+)\/([a-z-_]+)/i.exec(t),
                    nameP = /^@([a-z-_]+)/i.exec(t);

                return $type(t) !== 'string' ? t
                    :  listP ? new ListTimeline(listP[1], listP[2])
                    :  nameP ? new UserTimeline(nameP[1])
                    :  new SearchTimeline(t);
            }
        },

        'unsubscribe': function (feed){
           var index = this.subscriptions.indexOf(feed); 
           if (index) {
                var watcher = this.watchers[index];

                this.subscriptions.splice(index, 1);
                this.watchers.splice(index, 1);

                feed.removeEvent('update', watcher);
                return true;
            } else {
                return false;
            }
        },

        'add': function (tweets, source) {
            this.tweets.extend(tweets);
            this.tweets.sort(newest);

            this.fireEvent('update', [tweets, source, this]);

            function newest (a, b) {
                function age(n) {
                    return n.createdAt.getTime();
                }
                
                return age(b) - age(a);
            }
        },

        'toElement': function () {
            var element = new Element('div', {'class': 'TwitterView'});
            element.adopt(this.tweets.slice(0, this.options.count).map(this.options.template));

            this.addEvent('update', function(tweets, source, view) {
                var render = this.renderTweet.bind(this),
                    children = view.tweets.slice(0, this.options.count).map(render);

                element.getChildren().map(function (child) {
                    child.dispose();
                });                        

                element.adopt(children);
            });

            element.store('View', this);

            return element;
        },

        'renderTweet': function (tweet) {
            var template = this.options.template,
                htmlCache = this.htmlCache,
                key = tweet.toKey();

            if (!htmlCache.has(key)) htmlCache.set(key, template(tweet));

            return htmlCache.get(key);
        }
    });

    Twitter.extend({'View': View});

    function tweetBody(text) { 
        function linkify(str) {
            // Makes @mentions, #tags and urls into links
            return str.replace(/(\s|^)(mailto\:|(news|(ht|f)tp(s?))\:\/\/\S+)/g,'$1<a href="$2">$2</a>')
                .replace(/(\s|^)@(\w+)/g, '$1<a class="mention" href="http://twitter.com/$2">@$2</a>')
                .replace(/(\s|^)#(\w+)/g, '$1<a class="hashTag" href="http://twitter.com/search?q=%23$2">#$2</a>');
        };

        return new Element('p', {'class': 'tweetBody', 'html': linkify(text)});
    }

    function datefix(datestring) {
         // Twitter seems to give some wacky date format
         // that IE can't handle, so I convert it to something more normal.
        return  datestring.replace(/^(.+) (\d+:\d+:\d+) ((?:\+|-)\d+) (\d+)$/, "$1 $4 $2 GMT$3");
    };

    return Twitter; 
}());
Twitter.extend({
    'imageSites': $H({
        'twitpic.com':    "show/{size}/{id}",
        'yfrog.com':      "{id}.th.jpg",
        'yfrog.us':       "{id}.th.jpg",
        'tweetphoto.com': "show/{size}/{id}",
        'img.ly':         "show/{size}/{id}"
    }),

    'imageLinkToThumbnail': function (link, size) {
        var uri    = new URI(link),
            domain = uri.get('host'),
            id     = uri.get('file'),
            site   = Twitter['imageSites'].get(domain),
            tsize  = (size && size.match(/(thumb)|(mini)/)) ? size : 'thumb',
            src    = site && site.substitute({'id': id, 'size': tsize});

        return src && new Element('a', {'href': link, 'class': 'thumbnail'}).grab(new Element('img', {
            'src': "http://{0}/{1}".substitute([domain, src]),
            'alt': 'thumbnail provided by ' + domain
        }));
    },

    'expandImageLinks': function (html) {
        var thumbs = html.getElements('a')
                .map(function (link) {
                    return Twitter.imageLinkToThumbnail(link, 'thumb')
                })
                .filter($arguments(0));

        return html.adopt(thumbs);
    }
});

Twitter.templates.extend({'imagethumbs': function (twitter) {
        var html = Twitter.templates.basic(twitter);
        return Twitter.expandImageLinks(html);
    }
});

Twitter.extend({
    'urlShorteners': ['bit.ly', 'tinyurl.com', 'is.gd', 'tr.im', 'ow.ly'],
    'deshortenUrl': 'http://thecyberplains.com:8000/',
    'deshorten': function deshorten(links) {
        var urls  = links.map(getHref),
            table = $H(links.associate(urls)),
            req   = new Request.JSONP ({
                url: Twitter['deshortenUrl'],
                data: { 'short': urls.join(',') },
                onComplete: callBack
            });

        req.send();

        function getHref(o) { return o.get('href'); }

        function callBack (json) {
            $H(json).each(
                function(longUrl, shortUrl){
                    var oldLink = table.get(shortUrl),
                        newLink = new Element('a', {
                            'href': longUrl,
                            'text': longUrl
                        });

                    newLink.replaces(oldLink);
                });
        }
    }
});

Twitter.templates.extend({
    'deshortened': function (tweet) {
        var html = Twitter.templates.basic(tweet),
            links = html.getElements('a'),
            shorts = links.filter(isShortened);

        Twitter.deshorten(shorts);
        return html;

        function isShortened(link) {
            var domain = new URI(link.get('href')).get('host');
            return Twitter.urlShorteners.contains(domain);
        }
    }
});

Twitter.extend({
    'videoSites': $H({
        'twitvid.com': {
            'embed': 'http://www.twitvid.com/player/{code}',
            'height': 344,
            'width': 425
        },

        'vid.ly': {
            'embed': 'http://www.vidly.com/embed/{code}',
            'height': 344,
            'width': 425
        }
    }),

    'expandVideos': function (html) {
        if (Browser.Plugins.Flash.version) html.getElements('a').each(function (link) {
            var url = new URI(link.get('href')),
                domain = url.get('host'),
                code = url.get('file');

            if (Twitter.videoSites.has(domain)) {
                makeEmbed(Twitter.videoSites.get(domain), code);
            }
        });
    
        function makeEmbed(site, id) {
            var src = site.embed.substitute({'code': id});

            html.adopt(
                new Swiff(src, {
                    'height': site.height,
                    'width': site.width,
                    'wmode': 'transparent'
                }));
        }

        return html;
    }
});

Twitter.templates.extend({
    'videos': function (tweet) {
        var html = Twitter.templates.basic(tweet);
        return Twitter.expandVideos(html);
    }
});

Twitter.templates.extend({
    'images+video': function (tweet) {
        var html = Twitter.templates.imagethumbs(tweet);
        return Twitter.expandVideos(html);
    },

    'deluxe': function (tweet) {
        var html = Twitter.templates.deshortened(tweet);
        
        html = Twitter.expandVideos(html);
        html = Twitter.expandImageLinks(html);

        return html;
    }
});
