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

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


class MyTwitter():

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

    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()):
            using_results = False

            pane = self.tweet_notebook.get_nth_page(i)
            list_name = pane.get_list_name()

            if pane.get_single_tweet() is not None:
                continue
            elif 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)
                using_results = True
            else:
                statuses = self.api.GetListStatuses(list_name, per_page=self.num_entries)

            pane.update_window(statuses, using_results=using_results)

        # 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.update_status_bar('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 re.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, data):
        self.update_entry.set_text('@' + data['screen_name'] + ' ')
        self.reply_id = data['id']
        self.update_entry.grab_focus()


    def on_retweet(self, widget, data):
        self.api.PostRetweet(data['id'])


    def on_reply_to(self, widget, data):
        self.add_to_notebook(data['name'], data['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, single_tweet=None):
        new_pane = TweetPane(name, self.num_entries, single_tweet)
        self.tweet_notebook.append_page(new_pane, new_pane.get_tab_label())
        new_pane.get_tab_label().connect('close-clicked', self.remove_view, name)
        new_pane.connect('tweet-reply', self.on_reply)
        new_pane.connect('tweet-retweet', self.on_retweet)
        new_pane.connect('tweet-in-reply-to', self.on_reply_to)

        # Special logic for single tweet pane
        if single_tweet is not None:
            try:
                statuses = []
                statuses.append(self.api.GetStatus(single_tweet))
                new_pane.update_window(statuses)
            except HTTPError:
                self.tweet_notebook.remove_page(-1)
                self.update_status_bar('Error retrieving tweet id: ' + str(single_tweet))
                return

        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)
        pane.set_tweets_read()


    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()
        

    def update_status_bar(self, text):
        self.status_bar.pop(self.context_id)
        self.status_bar.push(self.context_id, text)

### 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, num_entries=20, single_tweet=None):
        gtk.ScrolledWindow.__init__(self)

        self.updated_once = False

        self.single_tweet = single_tweet

        self.list_name = list_name

        self.tab_label = CloseTabLabel(self.list_name)

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

        self.tweets = []

        self.num_entries = num_entries
        if self.single_tweet is not None:
            self.num_entries = 1

        self.init_widgets()


    def init_widgets(self):
        self.tab_label.connect('label_clicked', self.set_tweets_read_callback)

        tweet_box = gtk.VBox()
        viewport = gtk.Viewport()

        # Build us some labels...
        for i in range(0, self.num_entries):
            self.tweets.append(TweetBox())
            tweet_box.pack_start(self.tweets[i], expand=False)
            self.tweets[i].connect('reply', self.on_tweet_reply)
            self.tweets[i].connect('retweet', self.on_retweet)
            self.tweets[i].connect('in-reply-to', self.on_tweet_reply_to)

        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, raw_statuses, using_results=False):
        if using_results:
            statuses = self.statuses_from_results(raw_statuses)
        else:
            statuses = raw_statuses

        if self.updated_once is False:
            self.updated_once = True

        # 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.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

        self.update_tab_label()


    # Update the label with the number of unread tweets
    def update_tab_label(self):
        pane_text = self.list_name
        if self.num_new_tweets > 0:
            pane_text += ' (' + str(self.num_new_tweets) + ')'
        self.tab_label.set_label_text(pane_text)


    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
        self.update_tab_label()


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

    
    def get_tab_label(self):
        return self.tab_label


    def get_single_tweet(self):
        return self.single_tweet


    def updated_once(self):
        return self.updated_once


    def on_tweet_reply(self, widget):
        self.emit('tweet-reply', {'screen_name': widget.screen_name, 'id': widget.id})


    def on_retweet(self, widget):
        self.emit('tweet-retweet', {'id': widget.id})


    def on_tweet_reply_to(self, widget, data):
        self.emit('tweet-in-reply-to', data)


    # To keep things simple elsewhere and improve code reuse
    # we'll build a list of home-cooked Status objects out of results.
    # Why is this even necessary?
    # Why can't we have more consistency out of the Twitter API?
    def statuses_from_results(self, results):
        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.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
            statuses.append(status)
        return statuses

### end class TweetPane

# signals for TweetPane

gobject.signal_new("tweet-reply", TweetPane,
                   gobject.SIGNAL_RUN_LAST,
                   gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
gobject.signal_new("tweet-retweet", TweetPane,
                   gobject.SIGNAL_RUN_LAST,
                   gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
gobject.signal_new("tweet-in-reply-to", TweetPane,
                   gobject.SIGNAL_RUN_LAST,
                   gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))



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.in_reply_to_id = None
        self.in_reply_to_screen_name = 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, 5, 10, 5)
        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)
            self.text.connect('button-press-event', self.on_mouse_clicked)

        # Build the buttons
        button_box_align = gtk.Alignment()
        button_box_align.set_padding(0, 15, 0, 0)
        button_box = gtk.HBox()
        self.pack_start(button_box)

        self.reply_to_button = gtk.Button("")
        self.reply_to_button.set_relief(gtk.RELIEF_NONE)
        button_box.pack_start(self.reply_to_button, expand=False)
        self.reply_to_button.connect("clicked", self.on_in_reply_to_clicked)
        
        reply_button = gtk.Button("Reply")
        reply_button.set_relief(gtk.RELIEF_HALF)
        button_box.pack_end(reply_button, expand=False)
        reply_button.connect("clicked", self.on_reply_clicked)

        retweet_button = gtk.Button("Retweet")
        retweet_button.set_relief(gtk.RELIEF_HALF)
        button_box.pack_end(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
        self.in_reply_to_id = status.in_reply_to_status_id
        self.in_reply_to_screen_name = status.in_reply_to_screen_name

        # ... 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)

        # If this is in reply to something, set appropriate label
        if self.in_reply_to_screen_name:
            self.reply_to_button.set_label('in reply to ' + self.in_reply_to_screen_name)
        


    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_in_reply_to_clicked(self, widget):
        self.emit('in-reply-to', {'id': self.in_reply_to_id, 'name': self.in_reply_to_screen_name})


    def on_mouse_clicked(self, widget, event):
        if event.button == 1:
            self.set_read(True)
        # fixme: call on_url_clicked if there is an active uri
        #  Apparently, this must wait until pygtk 2.18

    
    def on_url_clicked(self, widget):
        # fixme: we're catching this signal just to debug why it doesn't get emitted
        #   seems to be related to EventBox?
        print 'debug: on_url_clicked()'
        return True


# end class TweetBox

# signals for TweetBox
gobject.signal_new("reply", TweetBox,
                   gobject.SIGNAL_RUN_LAST,
                   gobject.TYPE_NONE, ())
gobject.signal_new("retweet", TweetBox,
                   gobject.SIGNAL_RUN_LAST,
                   gobject.TYPE_NONE, ())
gobject.signal_new("in-reply-to", TweetBox,
                   gobject.SIGNAL_RUN_LAST,
                   gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))



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)

        self.connect('button-press-event', self.on_button_press)

        # 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('close-clicked')


    def on_button_press(self, event, direction):
        self.emit('label-clicked')


### end class CloseTabLabel

# signals for CloseTabLabel
gobject.signal_new("close-clicked", CloseTabLabel,
                   gobject.SIGNAL_RUN_LAST,
                   gobject.TYPE_NONE, ())
gobject.signal_new("label-clicked", CloseTabLabel,
                   gobject.SIGNAL_RUN_LAST,
                   gobject.TYPE_NONE, ())



class Status():
    def __init__(self):
        self.user = User()
        self.id = None
        self.created_at = None

class User():
    def __init__(self):
        self.screen_name = None
        self.name = None


# main

my_twitter = MyTwitter()
gtk.main()