# Python module that handles calls to the twitter API as a separate thread

import re
import gtk, gobject
from threading import Thread,RLock
import twitter_pb2
from oauthtwitter import OAuthApi
from urllib2 import HTTPError,URLError
import usercache
import webbrowser
from time import sleep

# These are the global constants for the twitter oauth entry for our app
CONSUMER_KEY = 'jGu64TPCUtyLZKyWyMJldQ'
CONSUMER_SECRET = 'lTRrTyvzRfJab5HsAe16zkV8tqFVRp0k0cTfHL0l4GE'


class CustomApi(OAuthApi):
    '''
    This is a Twitter API with an RLock for multi-threaded access
    Also included is logic for processing the list names when the object is
    instantiated
    '''

    def __init__(self, access_token):
        OAuthApi.__init__(self, CONSUMER_KEY, CONSUMER_SECRET, access_token)
        self.lock = RLock()
        self.sig_proxy = SigProxy()

        self.username = self.GetUserInfo().screen_name
        self._username = self.username

        thread = GetUserLists(api=self)
        thread.sig_proxy.connect('lists-ready', self.on_lists_ready)
        thread.start()
        

    def on_lists_ready(self, widget, lists, ignored):
        list_names = []
        for l in lists['lists']:
            list_names.append(l.name)
        list_names.sort()
        self.sig_proxy.emit('lists-ready', self.username, list_names)

# End class CustomApi


class ApiThread(Thread):
    def __init__(self, api):
        Thread.__init__(self)
        self.api = api
        self.setDaemon(True)

# End class ApiThread



class GetTweets(ApiThread):
    def __init__(self, api, list_name, pane, num_entries, username):
        ApiThread.__init__(self, api)
        self.list_name = list_name
        self.pane = pane
        self.num_entries = num_entries
        self.username = username


    def run(self):
        try:
            # username/Home entries need to load the appropriate Home feed
            if self.list_name == self.username + '/Home':
                with self.api.lock:
                    statuses = self.api.GetHomeTimeline(count=self.num_entries)

            # For @username, check if it is one of our usernames, or
            # just needs to be searched on
            elif self.list_name == '@' + self.username:
                with self.api.lock:
                    statuses = self.api.GetMentions(count=self.num_entries)
            elif re.match('@', self.list_name):
                with self.api.lock:
                    results = self.api.Search(self.list_name, rpp=self.num_entries)
                statuses = results_to_statuses(results, self.api)

            # Direct Messages should match like /Home, above
            elif self.list_name == self.username + '/Direct Messages':
                with self.api.lock:
                    dms = self.api.GetDirectMessages()
                statuses = dms_to_statuses(dms)
                
            # User lookups go straight to the user
            elif re.match(r'user: ', self.list_name):
                with self.api.lock:
                    statuses = self.api.GetUserTimeline(re.sub(r'^user: ', r'', self.list_name), count=self.num_entries)

            # Lists load the appropriate list from the appropriate account
            elif re.match(r'list: ', self.list_name):
                real_list = re.sub(r'list: .*/(.*)', r'\1', self.list_name)
                with self.api.lock:
                    statuses = self.api.GetListStatuses(real_list, per_page=self.num_entries)

            # Everything else is a straight search
            else:
                with self.api.lock:
                    results = self.api.Search(self.list_name, rpp=self.num_entries)
                statuses = results_to_statuses(results, self.api)

        except (HTTPError, URLError):
            statuses = None

        # For each user id present, populate the UserCaches
        if statuses:
            for status in statuses:
                usercache.add_to_av_cache(status.user)
                usercache.add_to_name_cache(status.user, self.api)

        gtk.gdk.threads_enter()
        try:
            self.pane.update_window(statuses)
        finally:
            gtk.gdk.threads_leave()


### End class GetTweets



class GetSingleTweet(ApiThread):
    def __init__(self, api, pane, single_tweet):
        ApiThread.__init__(self, api)
        self.pane = pane
        self.single_tweet = single_tweet


    def run(self):
        statuses = []
        with self.api.lock:
            try:
                statuses.append(self.api.GetStatus(self.single_tweet))
            except (HTTPError, URLError):
                statuses = None

        # In case we've never seen this user, grab their profile image
        for status in statuses:
            usercache.add_to_av_cache(status.user)
            usercache.add_to_name_cache(status.user)

        gtk.gdk.threads_enter()
        try:
            self.pane.update_window(statuses)
        finally:
            gtk.gdk.threads_leave()


### End class GetSingleTweet



class GetConversation(ApiThread):
    def __init__(self, api, pane, root_tweet_id):
        ApiThread.__init__(self, api)
        self.pane = pane
        self.root_tweet_id = root_tweet_id


    def run(self):
        statuses = []
        last_tweet = None

        # Get the root tweet
        try:
            with self.api.lock:
                last_tweet = self.api.GetStatus(self.root_tweet_id)
            statuses.append(last_tweet)
        except (HTTPError, URLError):
            statuses = None
            last_tweet = None

        # get tweets in a loop, until there is no in_reply_to_status_id
        while last_tweet and last_tweet.in_reply_to_status_id:
            try:
                with self.api.lock:
                    last_tweet = self.api.GetStatus(last_tweet.in_reply_to_status_id)
                statuses.append(last_tweet)
            except (HTTPError, URLError):
                last_tweet = None
        
        # In case we've never seen some of these users, grab their profile images and cache them
        for status in statuses:
            usercache.add_to_av_cache(status.user)
            usercache.add_to_name_cache(status.user)

        statuses.reverse()

        gtk.gdk.threads_enter()
        try:
            self.pane.update_window(statuses)
        finally:
            gtk.gdk.threads_leave()


### End class GetConversation



class GetFollowing(ApiThread):
    def __init__(self, api, pane, user):
        ApiThread.__init__(self, api)
        self.pane = pane
        self.user = user


    def run(self):
        screen_name = re.sub('user: ', '', self.user)

        try:
            with self.api.lock:
                relationship = self.api.ShowFriendships(target_screen_name=screen_name)
            following = relationship.source.following
        except (HTTPError, URLError):
            following = False

        self.pane.user_box.set_following(following)

### End class GetFollowing



class GetUserInfo(ApiThread):
    def __init__(self, api, pane, user):
        ApiThread.__init__(self, api)
        self.pane = pane
        self.user = user


    def run(self):
        screen_name = re.sub('user: ', '', self.user)

        try:
            with self.api.lock:
                user = self.api.GetUser(screen_name)
            usercache.add_to_av_cache(user)
            usercache.add_to_name_cache(user)
        except (HTTPError, URLError):
            verified = False
            user = None

        gtk.gdk.threads_enter()
        try:
            self.pane.user_box.update_info(user)
        finally:
            gtk.gdk.threads_leave()

### End class GetUserInfo


class GetUserLists(ApiThread):
    def __init__(self, api):
        ApiThread.__init__(self, api)
        self.sig_proxy = SigProxy()


    def run(self):
        lists = []
        done = False

        while not done:
            done = True
            try:
                with self.api.lock:
                    lists = self.api.GetUserLists()
            except (HTTPError, URLError) as exception:
                print 'debug: GetUserLists had error: ' + str(exception.code)
                sleep(5)
                done = False

        self.sig_proxy.emit('lists-ready', lists, None)

### End class GetUserLists



class PostUpdate(ApiThread):
    def __init__(self, api, update, reply_id):
        ApiThread.__init__(self, api)
        self.sig_proxy = SigProxy()

        self.update = update
        self.reply_id = reply_id


    def run(self):
        try:
            with self.api.lock:
                self.api.PostUpdate(self.update, in_reply_to_status_id=self.reply_id)
                success = True
        except (HTTPError, URLError):
            success = False

        self.sig_proxy.emit('update-posted', success)

### End class PostUpdate



class PostRetweet(ApiThread):
    def __init__(self, api, retweet_id):
        ApiThread.__init__(self, api)
        self.sig_proxy = SigProxy()
        self.retweet_id = retweet_id


    def run(self):
        try:
            with self.api.lock:
                self.api.PostRetweet(self.retweet_id)
                success = True
        except (HTTPError, URLError):
            success = False

        self.sig_proxy.emit('retweet-posted', success)

### End class PostRetweet



class ChangeFriendship(ApiThread):
    def __init__(self, api, pane, user_name, follow=True):
        ApiThread.__init__(self, api)
        self.sig_proxy = SigProxy()
        self.user_name = user_name
        self.follow = follow
        self.pane = pane


    def run(self):
        try:
            with self.api.lock:
                if self.follow:
                    user = self.api.CreateFriendship(self.user_name)
                else:
                    user = self.api.DestroyFriendship(self.user_name)

                if user.__class__ == twitter_pb2.User:
                    success = True
                else:
                    success = False

        except (HTTPError, URLError):
            success = False

        gtk.gdk.threads_enter()
        try:
            if self.follow:
                self.pane.user_box.set_following(success)
            else:
                self.pane.user_box.set_following(not success)
        finally:
            gtk.gdk.threads_leave()        

        self.sig_proxy.emit('friendship-changed', {'user_name': self.user_name, 'follow': self.follow, 'success': success})

### End class ChangeFriendship



class SigProxy(gtk.Alignment):
    """
    This little class exists just so that we can have a gtk class in our
    threads that can emit signals.  That way, we can communicate data back to
    the gtk interface easily.
    """

    def __init__(self):
        gtk.Alignment.__init__(self)

# End class SigProxy

gobject.signal_new("lists-ready", SigProxy,
                   gobject.SIGNAL_RUN_LAST,
                   gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,gobject.TYPE_PYOBJECT))

gobject.signal_new("update-posted", SigProxy,
                   gobject.SIGNAL_RUN_LAST,
                   gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))

gobject.signal_new("retweet-posted", SigProxy,
                   gobject.SIGNAL_RUN_LAST,
                   gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))

gobject.signal_new("friendship-changed", SigProxy,
                   gobject.SIGNAL_RUN_LAST,
                   gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))


# We use these classes to emulate a Status object when we need
# one to be built out of something else.
class User(object):
    def __init__(self):
        self.profile = Profile()

class Profile(object):
    pass

class Status(object):
    def __init__(self):
        self.user = User()


def results_to_statuses(results, api):
    """
    Since the REST API and the Search API return different result types, this
    function converts Search API Results into custom-baked Status objects that
    mimic those returned by the REST API.

    To get the 'in reply to' data, it has to grab the individual tweet for any
    status that has a to_user_id field set.  This can slow things down a lot.
    fixme: add an option to disable this for speed...
    """
    statuses = []
    for result in results.results:
        status = Status()
        status.id = result.id
        status.user = User()
        status.user.screen_name = result.from_user
        status.user.name = ''
        status.user.profile.image_url = result.profile_image_url
        status.in_reply_to_screen_name = result.to_user
        # The Twitter Search API has different timestamps than the
        # REST API... balls
        # fixme:
        # Gotta be a cleaner way to do this, but I can't think of it
        # right now
        created_at = re.sub(',', '', result.created_at)
        created_split = re.split(' ', created_at)
        status.created_at = created_split[0] + ' ' + created_split[2] + ' ' + created_split[1] + ' ' + created_split[4] + ' ' + created_split[5] + ' ' + created_split[3]

        status.text = result.text
        status.source = result.source

        # If this is in reply to something, get the relevant tweet
        if result.to_user_id is not None:
            try:
                with api.lock:
                    tweet = api.GetStatus(result.id)
                status.in_reply_to_status_id = tweet.in_reply_to_status_id
            except (HTTPError, URLError):
                pass # Just move along, leave off the 'in reply to' data

        statuses.append(status)
    return statuses


def dms_to_statuses(direct_messages):
    """
    To make it easier for our widgets to handle, we convert Direct Message
    results to our home-baked Status objects that mimic the REST API Statuses.
    """
    statuses = []
    for dm in direct_messages:
        status = Status()
        status.id = dm.id
        status.user = User()
        status.user.screen_name = dm.sender_screen_name
        status.user.name = dm.sender.name
        status.user.profile.image_url = dm.sender.profile.image_url
        status.created_at = dm.created_at
        status.text = dm.text
        status.source = None
        status.in_reply_to_status_id = None
        status.in_reply_to_screen_name = None
        statuses.append(status)
    return statuses            


def get_access_token(window):
    auth_api = OAuthApi(CONSUMER_KEY, CONSUMER_SECRET)
    request_token = auth_api.getRequestToken()
    authorization_url = auth_api.getAuthorizationURL(request_token)

    webbrowser.open(authorization_url)
    auth_api = OAuthApi(CONSUMER_KEY, CONSUMER_SECRET, request_token)

    dialog = gtk.Dialog("Enter PIN", window, gtk.DIALOG_MODAL, (gtk.STOCK_OK, gtk.RESPONSE_OK, gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL))
    entry = gtk.Entry()
    dialog.vbox.pack_start(entry)
    entry.show()
    response = dialog.run()
    dialog.hide()
    
    if response == gtk.RESPONSE_OK:
        pin = entry.get_text()

        try:
            access_token = auth_api.getAccessToken(pin)
        except HTTPError:
            access_token = None

        return access_token

    else:
        return None