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

import sys, ConfigParser, os, re, optparse, shelve
import gtk, gtk.glade, gobject
from urllib2 import HTTPError,URLError
from twitterwidgets import TweetPane
import apithreads


class MyTwitter():

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

    def __init__(self, config_file, resize):
        self.resize = resize

        config = ConfigParser.ConfigParser()
        config.read(os.path.expanduser(config_file))

        # Set config options to defaults, if they are not present
        new_data = False
        if not config.has_section('global'):
            config.add_section('global')
            new_data = True
        if not config.has_option('global', 'entries'):
            config.set('global', 'entries', '20')
            new_data = True
        if not config.has_option('global', 'refreshtime'):
            config.set('global', 'refreshtime', '30')
            new_data = True
        if not config.has_option('global', 'dbfile'):
            config.set('global', 'dbfile', '~/.mytwitter.db')
            new_data = True

        # Write out new config data, if needed
        if new_data:
            config_filehandle = open(os.path.expanduser(config_file), 'wb')
            config.write(config_filehandle)
            config_filehandle.close()

        if len(config.sections()) < 2:
            print "Error: You must define at least one [account] section in " + config_file
            sys.exit(1)

        # Init the glade stuff here, so we don't have a race condition with
        # the lists-ready signal
        self.init_user_interface('./default.glade')

        self.accounts = {}
        for item in config.sections():
            if (re.match(r'account', item)):
                username = config.get(item, 'username')
                self.accounts[username] = apithreads.CustomApi(username=username, password=config.get(item, 'password'))
                self.accounts[username].sig_proxy.connect('lists-ready', self.on_lists_ready)
        
        self.username = self.accounts.keys()[0]
        self.api = self.accounts[self.username]

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

        db_file = os.path.expanduser(config.get('global', 'dbfile'))
        self.db = shelve.open(db_file)

        if not self.db.has_key('active_page'):
            self.db['active_page'] = 0

        if not self.db.has_key('open_tabs'):
            self.db['open_tabs'] = [(self.username + '/Home', None), ('@' + self.username, None), (self.username + '/Direct Messages', None)]

        if self.refresh_time < 10:
            self.refresh_time = 10

        self.reply_id = None

        # Load up all the programmatic GUI stuff
        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)

        # Get widgets from glade
        self.window = self.widget_tree.get_widget('window')
        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.following_button = self.widget_tree.get_widget('following_button')
        self.at_button = self.widget_tree.get_widget('at_button')
        self.verified_label = self.widget_tree.get_widget('verified_label')
        self.account_select = self.widget_tree.get_widget('account_select')


    def init_widgets(self):
        # Set the main window size
        if self.resize and self.db.has_key('width') and self.db.has_key('height'):
            self.window.resize(self.db['width'], self.db['height'])

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

        # Manual tweaks to the glade UI, to overcome its limitations
        self.tweet_notebook.remove_page(0)
        self.account_select.remove_text(0)

        # Add entries to the account select box, set the default entry
        for username in self.accounts.keys():
            self.account_select.append_text(username)
        self.account_select.set_active(0)

        # Add the tabs from last session to the notebook
        page_num = self.db['active_page']
        for tab, single_tweet in self.db['open_tabs']:
            self.add_to_notebook(tab, single_tweet)
        self.tweet_notebook.set_current_page(page_num)

        self.update_windows()

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


    # Spawns a thread for each pane, which updates that pane.
    def update_windows(self):
        for i in range(0, self.tweet_notebook.get_n_pages()):
            pane = self.tweet_notebook.get_nth_page(i)
            self.update_single_window(pane)

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


    def update_single_window(self, pane):
        list_name = pane.get_list_name()

        # Single tweets should never be updated here
        if pane.get_single_tweet() is not None:
            return

        # Determine username and appropriate account to use
        found = False

        username = re.sub('/Home', '', list_name)
        if self.accounts.has_key(username):
            account = self.accounts[username]
            found = True

        if not found:
            username = re.sub('@', '', list_name)
            if self.accounts.has_key(username):
                account = self.accounts[username]
                found = True

        if not found:
            username = re.sub('/Direct Messages', '', list_name)
            if self.accounts.has_key(username):
                account = self.accounts[username]
                found = True

        if not found:
            account = self.api
            username = self.username
            
        apithreads.GetTweets(api=account,
                             list_name=list_name,
                             pane=pane,
                             num_entries=self.num_entries,
                             username=username
                             ).start()                


    def update_window_callback(self, widget):
        pane = self.tweet_notebook.get_nth_page(self.tweet_notebook.get_current_page())
        self.update_single_window(pane)


    def update_status(self):
        reply_id = self.reply_id
        text = self.update_entry.get_text()
        self.update_entry.set_text("")

        with self.api.lock:
            try:
                self.api.PostUpdate(text, in_reply_to_status_id=reply_id)
            except HTTPError,URLError:
                self.update_status_bar('Failed to post tweet')
                self.reply_id = None
                return

        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 and not re.match('@', self.update_entry.get_text()):
            self.reply_id = None


    def gtk_main_quit(self, widget):
        self.db.close()
        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):
        with self.api.lock:
            try:
                self.api.PostRetweet(data['id'])
            except HTTPError,URLError:
                self.update_status_bar('Failed to retweet')


    def on_reply_to(self, widget, data):
        self.add_to_notebook(data['name'], data['id'])


    def on_view_selected(self, event, username, name):
        if name == 'Home' or name == 'Direct Messages':
            full_name = username + '/' + name
        elif name == '@' + username:
            full_name = name
        else:
            full_name = 'list: ' + username + '/' + name

        # Now, add a new tab with this list
        self.add_to_notebook(full_name)


    # Remove one of the views from the tweet notebook.
    # Called when the close button is clicked on one of the views
    # or Ctrl + W is pressed while the view is active
    def remove_view(self, name, single_tweet):
        ot = self.db['open_tabs']
        ot.remove((name,single_tweet))
        self.db['open_tabs'] = ot

        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


    def remove_view_callback(self, event, name, single_tweet):
        self.remove_view(name, single_tweet)


    def add_to_notebook(self, name, single_tweet=None):
        # 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)
            # Unless it is a single tweet... ignore those unless
            # we are also a single tweet... then, special logic
            if pane.get_single_tweet() is not None:
                if pane.get_single_tweet() == single_tweet:
                    self.tweet_notebook.set_current_page(i)
                    return

            elif pane.get_list_name() == name:
                self.tweet_notebook.set_current_page(i)
                return

        # Add the pane to the persistent database of open panes
        if (name, single_tweet) not in self.db['open_tabs']:
            ot = self.db['open_tabs']
            ot.append((name,single_tweet))
            self.db['open_tabs'] = ot

        is_user = False
        if re.match('user:', name):
            is_user = True
        new_pane = TweetPane(name, num_entries=self.num_entries, single_tweet=single_tweet, is_user=is_user)

        if is_user:
            apithreads.GetFollowing(api=self.api, pane=new_pane, user=name).start()
            apithreads.GetVerified(api=self.api, pane=new_pane, user=name).start()

        self.tweet_notebook.append_page_menu(new_pane, new_pane.get_tab_label(), gtk.Label(name))
        self.tweet_notebook.set_tab_reorderable(new_pane, True)
        new_pane.get_tab_label().connect('close-clicked', self.remove_view_callback, name, single_tweet)
        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)
        new_pane.connect('show-user', self.show_user_callback)
        new_pane.connect('following-set', self.update_follow_button)
        new_pane.connect('verified-set', self.update_verified_label)

        # Special logic for single tweet pane
        if single_tweet is not None:
            apithreads.GetSingleTweet(api=self.api,
                                      pane=new_pane,
                                      single_tweet=single_tweet).start()
        else:
            self.update_single_window(new_pane)


    def on_tab_change(self, event, page, page_num):
        last_page = self.db['active_page']
        self.db['active_page'] = page_num

        # Now get the new page, and set everything up
        pane = self.tweet_notebook.get_nth_page(page_num)
        pane.set_tweets_read()
        self.update_follow_button(pane)
        if pane.get_is_user():
            self.at_button.show()
        else:
            self.at_button.hide()

        self.update_verified_label(pane)


    def on_tabs_reordered(self, widget, child, page_num):
        self.db['active_page'] = page_num

        # Clear the persistent tabs list, and recreate it
        # from scratch
        tab_names = []
        
        for i in range(self.tweet_notebook.get_n_pages()):
            pane = self.tweet_notebook.get_nth_page(i)
            tab_names.append((pane.get_list_name(), pane.get_single_tweet()))

        self.db['open_tabs'] = tab_names


    def on_search(self, event):
        search_string = self.search_entry.get_text()
        self.search_entry.set_text('')
        self.add_to_notebook(search_string)
        

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


    def on_following_button_clicked(self, event):
        current_pane = self.tweet_notebook.get_nth_page(self.tweet_notebook.get_current_page())
        user_name = re.sub('^user: ', '', current_pane.get_list_name())
        if current_pane.get_following():
            with self.api.lock:
                try:
                    self.api.DestroyFriendship(user_name)
                except HTTPError,URLError:
                    self.update_status_bar('Failed to unfollow user.')
                    return
            current_pane.set_following(False)
        else:
            with self.api.lock:
                try:
                    self.api.CreateFriendship(user_name)
                except HTTPError,URLError:
                    self.update_status_bar('Failed to follow user.')
                    return
            current_pane.set_following(True)

        self.update_follow_button(current_pane)


    # pane should be the currently active pane
    def update_follow_button(self, pane):
        if not pane.get_is_user():
            self.following_button.set_label('')
            self.following_button.hide()
        elif pane.get_following():
            self.following_button.set_label('Unfollow')
            self.following_button.show()
        else:
            self.following_button.set_label('Follow')
            self.following_button.show()


    def update_verified_label(self, pane):
        if pane.get_verified():
            self.verified_label.show()
        else:
            self.verified_label.hide()


    def show_user(self, name):
        self.add_to_notebook('user: ' + name)


    def show_user_callback(self, widget, data):
        self.show_user(data)


    def on_at_button_clicked(self, widget):
        current_pane = self.tweet_notebook.get_nth_page(self.tweet_notebook.get_current_page())
        user_name = re.sub('^user: ', '', current_pane.get_list_name())
        self.add_to_notebook('@' + user_name)


    def global_key_press_handler(self, widget, event):
        keyname = gtk.gdk.keyval_name(event.keyval)
        if keyname == 'w' and event.state & gtk.gdk.CONTROL_MASK:
            self.close_current_tab()
        elif (keyname == 'Tab' and event.state & gtk.gdk.SHIFT_MASK and event.state & gtk.gdk.CONTROL_MASK) or (keyname == 'ISO_Left_Tab' and event.state & gtk.gdk.CONTROL_MASK) or (keyname == 'Page_Up' and event.state & gtk.gdk.CONTROL_MASK):
            self.tweet_notebook.prev_page()
            return True
        elif (keyname == 'Tab' and event.state & gtk.gdk.CONTROL_MASK) or (keyname == 'Page_Down' and event.state & gtk.gdk.CONTROL_MASK):
            self.tweet_notebook.next_page()
            return True


    def close_current_tab(self):
        current_pane = self.tweet_notebook.get_nth_page(self.tweet_notebook.get_current_page())
        self.remove_view(current_pane.get_list_name(), current_pane.get_single_tweet())


    def on_account_changed(self, widget):
        new_user = self.account_select.get_active_text()
        if self.accounts.has_key(new_user):
            self.username = new_user
            self.api = self.accounts[self.username]


    def on_lists_ready(self, widget, username, list_names):
        # Setup the new sub-menu
        outer_menu_item = gtk.MenuItem(username)
        self.view_menu.append(outer_menu_item)
        new_menu = gtk.Menu()
        outer_menu_item.set_submenu(new_menu)

        # Insert the default list items
        list_names.insert(0, 'Home')
        list_names.insert(1, '@' + username)
        list_names.insert(2, 'Direct Messages')

        # Add the items to the submenu, connect handler
        for l in list_names:
            menu_item = gtk.MenuItem(l)
            new_menu.append(menu_item)
            menu_item.connect('activate', self.on_view_selected, username, l)
            menu_item.show()
        
        outer_menu_item.show()


    def on_resize(self, widget, event):
        self.db['width'] =  event.width
        self.db['height'] = event.height
        

### end class MyTwitter


# main
parser = optparse.OptionParser()
parser.add_option('-c' ,'--config', dest="filename", default="~/.mytwitter.conf", help="read configuration from FILENAME instead of the default ~/.mytwitter.conf")
parser.add_option('-n' ,'--no-resize', dest="resize", action='store_false', default=True, help="use the default window size instead of the size from the last session")
(options, args) = parser.parse_args()

my_twitter = MyTwitter(options.filename, options.resize)

gtk.gdk.threads_init()
gtk.gdk.threads_enter()
gtk.main()
gtk.gdk.threads_leave()