#!/usr/bin/python
#
# Custom twitter client... mostly for learning Python

import sys, twitter, ConfigParser, os, datetime, dateutil.tz, gtk, gtk.glade, gobject, re, subprocess


class MyTwitter():

    """ Display Tweets, post to twitter """

    # Precompile a regex for searching for @ at the beginning of a string
    at_check = re.compile('^@')


    def __init__(self):
        config = ConfigParser.ConfigParser()
        config.read(os.path.expanduser("~/.mytwitter"))
        self.username = config.get('global', 'username')
        self.password = config.get('global', 'password')

        self.num_entries = int(config.get('global', 'entries'))

        self.refresh_time = int(config.get('global', 'refreshtime'))
        if self.refresh_time < 10:
            self.refresh_time = 10

        self.reply_id = None

        # Authenticate with twitter, set up the API object
        self.api = twitter.Api(username=self.username, password=self.password)

        # Load up all the GUI stuff
        self.init_user_interface('./default.glade')
        self.init_widgets()


    def init_user_interface(self, path_to_skin):
        self.widget_tree=gtk.glade.XML(path_to_skin, "window")
        self.widget_tree.signal_autoconnect(self)


    def init_widgets(self):
        # Get widgets from glade
        self.tweet_notebook = self.widget_tree.get_widget('tweet_notebook')
        self.view_menu = self.widget_tree.get_widget('view_menu')
        self.update_entry = self.widget_tree.get_widget('update_entry')
        self.update_count = self.widget_tree.get_widget('update_count')
        self.status_bar = self.widget_tree.get_widget('status_bar')
        self.search_entry = self.widget_tree.get_widget('search_entry')

        self.context_id = self.status_bar.get_context_id('message')

        self.tweet_notebook.remove_page(0) # kill the page that glade makes us have

        # When we change tabs, any unread tweets in it become read
        self.tweet_notebook.connect('switch_page', self.on_tab_change)

        # Add the Home tab to the notebook
        self.add_to_notebook('Home')

        # Put Home, @user, and lists in the View menu...
        lists = self.api.GetUserLists()
        list_names = []
        for l in lists['lists']:
            list_names.append(l.name)
        list_names.sort()
        list_names.insert(0, 'Home')
        list_names.insert(1, '@' + self.username)
        for l in list_names:
            menu_item = gtk.MenuItem(l)
            self.view_menu.append(menu_item)
            menu_item.connect('activate', self.on_view_selected, l)
            menu_item.show()

        # Timer to update periodically
        self.update_windows()
        gobject.timeout_add(self.refresh_time * 1000, self.update_windows)


    def update_windows(self):
        for i in range(0, self.tweet_notebook.get_n_pages()):
            pane = self.tweet_notebook.get_nth_page(i)
            list_name = pane.get_list_name()

            if list_name is None or list_name == 'Home':
                statuses = self.api.GetHomeTimeline(count=self.num_entries)
            elif list_name == '@' + self.username:
                statuses = self.api.GetMentions(count=self.num_entries)
            elif re.match(r'user: ', list_name):
                statuses = self.api.GetUserTimeline(re.sub(r'^user: ', r'', list_name), count=self.num_entries)
            elif re.match(r'#', list_name):
                statuses = self.api.Search(list_name, rpp=self.num_entries)
            else:
                statuses = self.api.GetListStatuses(list_name, per_page=self.num_entries)

            pane.update_window(statuses)

            # Update the label with the number of unread tweets
            pane_text = list_name
            if pane.get_num_new_tweets() > 0:
                pane_text += ' (' + str(pane.get_num_new_tweets()) + ')'
            self.tweet_notebook.get_tab_label(pane).set_label_text(pane_text)

        # We have to return true, so the timeout_add event will keep happening
        return True


    def update_windows_callback(self, widget):
        self.update_windows()


    def update_status(self):
        text = self.update_entry.get_text()
        self.update_entry.set_text("")
        self.api.PostUpdate(text, in_reply_to_status_id=self.reply_id)
        self.reply_id = None
        self.status_bar.push(self.context_id, 'Tweet Posted')
        

    def update_status_callback(self, widget):
        self.update_status()


    def text_watcher(self, widget):
        ''' Watch text entered on the update_entry, update things '''
        text_len = self.update_entry.get_text_length()
        new_count = str(text_len) + "/140"
        self.update_count.set_label(new_count)

        # If reply_id is set, unset it if we have removed the @ symbol
        if self.reply_id is not None and not MyTwitter.at_check.match(self.update_entry.get_text()):
            self.reply_id = None


    def gtk_main_quit(self, widget):
        gtk.main_quit()


    def on_about(self, widget):
        print "STUB: help->about not yet implemented"


    def on_reply(self, widget):
        self.update_entry.set_text('@' + widget.screen_name + ' ')
        self.reply_id = widget.id
        self.update_entry.grab_focus()


    def on_retweet(self, widget):
        self.api.PostRetweet(widget.id)


    def on_view_selected(self, event, name):
        # If it already exists, don't add it, just switch to it
        for i in range(self.tweet_notebook.get_n_pages()):
            pane = self.tweet_notebook.get_nth_page(i)
            if pane.get_list_name() == name:
                self.tweet_notebook.set_current_page(i)
                return

        # Now, add a new tab with this list
        self.add_to_notebook(name)
        
        # And, to propagate it:
        self.update_windows()


    # Remove one of the views from the tweet notebook.
    # Called when the close button is clicked on one of the views
    def remove_view(self, event, name):
        for i in range(self.tweet_notebook.get_n_pages()):
            pane = self.tweet_notebook.get_nth_page(i)
            if (pane.get_list_name() == name):
                self.tweet_notebook.remove_page(i)
                return


    # This code modified from create_custom_tab in:
    # http://www.daa.com.au/pipermail/pygtk/2006-April/012216.html
    def add_to_notebook(self, name):
        new_pane = TweetPane(name, self)

        tab_label = CloseTabLabel(name)
        self.tweet_notebook.append_page(new_pane, tab_label)
        tab_label.connect('clicked', self.remove_view, name)
        self.tweet_notebook.set_current_page(-1) # switch to the new pane


    def on_tab_change(self, event, page, page_num):
        pane = self.tweet_notebook.get_nth_page(page_num)
        tab_label = self.tweet_notebook.get_tab_label(pane)

        pane.set_tweets_read()
        tab_label.set_label_text(pane.get_list_name())


    def on_search(self, event):
        search_string = self.search_entry.get_text()
        self.search_entry.set_text('')
        if re.match(r'#', search_string):
            self.add_to_notebook(search_string)
        else:
            # Assume this is a user name we want to lookup
            self.add_to_notebook('user: ' + search_string)

        self.update_windows()
        

### end class MyTwitter


class TweetPane(gtk.ScrolledWindow):
    '''
    Box that holds all the TweetBoxes for a given feed

    This box will not update itself, the parent should do that.
    
    It will connect num_entries listeners to its parent's on_reply() and on_retweet()

    It also gets some data from its parent, including num_entries
    '''

    def __init__(self, list_name, mytwitter):
        gtk.ScrolledWindow.__init__(self)

        self.list_name = list_name
        self.mytwitter = mytwitter

        # These handle determining which tweets are unread
        self.last_tweet_read = None
        self.latest_tweet = None
        self.num_new_tweets = 0

        self.tweets = []

        self.init_widgets()


    def init_widgets(self):
        tweet_box = gtk.VBox()
        viewport = gtk.Viewport()

        # Build us some labels...
        for i in range(0, self.mytwitter.num_entries):
            self.tweets.append(TweetBox())
            tweet_box.pack_start(self.tweets[i])
            self.tweets[i].connect('reply', self.mytwitter.on_reply)
            self.tweets[i].connect('retweet', self.mytwitter.on_retweet)

        viewport.add(tweet_box)

        # Several different actions should mark the tweets as 'read'
        self.connect('focus', self.set_tweets_read_callback)
        viewport.connect('button_press_event', self.set_tweets_read_callback)
        self.connect('scroll-event', self.set_tweets_read_callback)
        self.connect('scroll-child', self.set_tweets_read_callback)

        self.add(viewport)
        self.set_policy(gtk.POLICY_NEVER, gtk.POLICY_ALWAYS)
        self.show_all()


    def update_window(self, statuses):
        # If this is our first load of this list, don't treat anything as new!
        if self.last_tweet_read is None:
            self.last_tweet_read = statuses[0].id

        # Keep count of the new tweets for posting a status message
        self.num_new_tweets = 0

        for i in range(0, self.mytwitter.num_entries):
            read = True
            if i < len(statuses):
                if statuses[i].id > self.last_tweet_read:
                    self.num_new_tweets += 1
                    read = False
                self.tweets[i].set_status(statuses[i], read)
            else:
                self.tweets[i].clear_status()

        self.latest_tweet = statuses[0].id


    def get_list_name(self):
        return self.list_name


    def set_tweets_read(self):
        self.last_tweet_read = self.latest_tweet
        self.num_new_tweets = 0


    def set_tweets_read_callback(self, event, arg1=None, arg2=None):
        self.set_tweets_read()

    
    def get_num_new_tweets(self):
        return self.num_new_tweets

### end class TweetPane


class TweetBox(gtk.VBox):

    '''
    GUI for displaying one tweet and associated buttons
    
    Also stores the data necessary for replying or retweeting (id, screen name)
    '''

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

        self.screen_name = None
        self.id = None

        self.init_widgets()


    def init_widgets(self):
        ## Build the header
        self.header = gtk.Label()
        label_eb = gtk.EventBox()
        label_eb.add(self.header)
        self.pack_start(label_eb)

        # Set the header's properties
        label_eb.modify_text(gtk.STATE_NORMAL,gtk.gdk.color_parse("#ffffff"))
        label_eb.modify_bg(gtk.STATE_NORMAL,gtk.gdk.color_parse("#8888ff"))
        self.header.set_alignment(0.0, 0.0)
        self.header.set_selectable(True)
        self.header.set_line_wrap(True)

        ## Build the text
        self.text = gtk.Label()
        text_align = gtk.Alignment()
        text_align.add(self.text)
        self.text_eb = gtk.EventBox()
        self.text_eb.add(text_align)
        self.pack_start(self.text_eb)

        # Set the text's properties
        text_align.set_padding(2, 10, 3, 0)
        self.text.set_alignment(0.0, 0.0)
        self.text.set_selectable(True)
        self.text.set_line_wrap(True)
        if gtk.gtk_version[0] > 2 or (gtk.gtk_version[0] == 2 and gtk.gtk_version[1] >= 18):
            self.text.connect('activate-link', self.on_url_clicked)

        button_box = gtk.HBox()
        self.pack_start(button_box)

        reply_button = gtk.Button("Reply")
        button_box.pack_start(reply_button, expand=False)
        reply_button.connect("clicked", self.on_reply_clicked)
        retweet_button = gtk.Button("Retweet")
        button_box.pack_start(retweet_button, expand=False)
        retweet_button.connect("clicked", self.on_retweet_clicked)
        

    def set_status(self, status, read=True):
        self.set_read(read)

        timezone = dateutil.tz.gettz()
        time_format = "%Y.%m.%d %H:%M:%S %Z"

        # Get the user object
        user = status.user

        # Get user's data for retweeting / replying
        self.screen_name = user.screen_name
        self.id = status.id

        # ... and a formatted timestamp
        timestamp = datetime.datetime.strptime(status.created_at, "%a %b %d %H:%M:%S +0000 %Y")
        timestamp = timestamp.replace(tzinfo=dateutil.tz.gettz('UTC'))
        timestring = timestamp.astimezone(timezone).strftime(time_format)

        # Set the header
        self.header.set_markup(user.name + " (" + user.screen_name + ") " + timestring)

        # and the text
        new_text = status.text
        new_text = re.sub(r'&([^;]*?)( |$)', r'&amp;\1\2', new_text)
        if gtk.gtk_version[0] > 2 or (gtk.gtk_version[0] == 2 and gtk.gtk_version[1] >= 18):
            new_text = re.sub(r"(http://.*?)( |$)", r'<a href="\1">\1</a>\2', new_text)
        self.text.set_markup(new_text)


    def clear_status(self):
        self.header.set_markup('')
        self.text.set_markup('')
        self.screen_name = None
        self.id = None
        self.set_read(True)


    def set_read(self, read=True):
        if read:
            self.text_eb.modify_bg(gtk.STATE_NORMAL,
                                   gtk.gdk.color_parse("#f2f1f0"))
        else:
            self.text_eb.modify_bg(gtk.STATE_NORMAL,
                                   gtk.gdk.color_parse("#dbffdb"))


    def on_reply_clicked(self, widget):
        self.emit('reply')


    def on_retweet_clicked(self, widget):
        self.emit('retweet')


    def on_url_clicked(self, widget):
        # fixme: for now, hard code firefox, since that's what I use
        #   Eventually make this configgable
        # fixme: this doesn't work at all right now... figure out how to make this signal happen
        print 'debug: on_url_clicked()'
#        subprocess.Popen('/usr/bin/firefox ' + self.text.get_current_uri(), shell=False).pid
        
# end class TweetBox


class CloseTabLabel(gtk.EventBox):
    '''
    This class holds a label and a button with an 'I' in it.  This button causes the CloseTabLabel
    to emit a clicked signal
    '''

    def __init__(self, name=None):
        gtk.EventBox.__init__(self)
        self.init_widgets(name)


    # This code is still heinous, but at least it is
    # isolated to its own class
    def init_widgets(self, name):
        #create a custom tab for notebook containing a 
        #label and a button with STOCK_ICON
        tabBox = gtk.HBox(False, 2)
        tabButton=gtk.Button()
        tabButton.connect('clicked', self.on_clicked)

        self.label = gtk.Label(name)

        #Add a picture on a button
        iconBox = gtk.HBox(False, 0)
        image = gtk.Image()
        image.set_from_stock(gtk.STOCK_CLOSE,gtk.ICON_SIZE_MENU)
        gtk.Button.set_relief(tabButton,gtk.RELIEF_NONE)
        settings = gtk.Widget.get_settings(tabButton)
        (w,h) = gtk.icon_size_lookup_for_settings(settings,gtk.ICON_SIZE_MENU)
        gtk.Widget.set_size_request(tabButton, w + 4, h + 4);
        iconBox.pack_start(image, True, False, 0)
        tabButton.add(iconBox)

        tabBox.pack_start(self.label, False)
        tabBox.pack_start(tabButton, False)

        # needed, otherwise even calling show_all on the notebook won't
        # make the hbox contents appear.
        tabBox.show_all()
        self.add(tabBox)


    def set_label_text(self, new_text):
        self.label.set_text(new_text)


    def on_clicked(self, event):
        self.emit('clicked')


### end class CloseTabLabel



# main

# Create custom events for TweetBox
gobject.signal_new("clicked", CloseTabLabel,
                   gobject.SIGNAL_RUN_LAST,
                   gobject.TYPE_NONE, ())
gobject.signal_new("reply", TweetBox,
                   gobject.SIGNAL_RUN_LAST,
                   gobject.TYPE_NONE, ())
gobject.signal_new("retweet", TweetBox,
                   gobject.SIGNAL_RUN_LAST,
                   gobject.TYPE_NONE, ())

my_twitter = MyTwitter()
gtk.main()