#!/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
from threading import enumerate
import apithreads


class Hrafn():
    """ Display Tweets, post to twitter """

    def __init__(self, config_file, resize):
        global debug

        self.resize = resize

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

        # Set config options to defaults, if they are not present
        self._check_config(config, config_file)

        if config.get('global', 'debug') == '1':
            debug = True

        if config.get('global', 'trayicon') == '1':
            self.use_trayicon = True
            if config.get('global', 'taskbar_when_minimized') == '1':
                self.taskbar_min = True
            else:
                self.taskbar_min = False
        else:
            self.use_trayicon = False
            self.taskbar_min = True
            
        # Init the glade stuff here, so we don't have a race condition with
        # the lists-ready signal
        self.init_user_interface('./ui/default.glade')
        self.first_account_item = None

        # And init the DB stuff here
        db_file = os.path.expanduser(config.get('global', 'dbfile'))
        self.db = shelve.open(db_file)

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

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

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

        if not self.db.has_key('active_user'):
                self.db['active_user'] = None

        # Now set up the accounts and their corresponding APIs
        self.accounts = {}
        for token in self.db['tokens']:
            api = apithreads.CustomApi(token)
            self.add_account(api)

        self.username = self.db['active_user']

        self.minimized = False

        try:
            self.api = self.accounts[self.username]
        except KeyError:
            self.api = None

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

        # refresh_time is in minutes... convert to seconds here
        self.refresh_time *= 60

        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.accounts_menu = self.widget_tree.get_widget('accounts_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_label = self.widget_tree.get_widget('account_label')
        self.help_menu = self.widget_tree.get_widget('help_menu')


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

        # Add debug options to help menu
        if debug:
            menu_item = gtk.MenuItem('debug: Show Threads')
            menu_item.connect('activate', self.debug_show_threads)
            self.help_menu.append(menu_item)
            menu_item.show()

        # Add a system tray icon
        if self.use_trayicon:
            self.tray_icon = gtk.status_icon_new_from_file('ui/icon.svg')
            self.tray_icon.connect('activate', self.on_tray_icon_clicked)
            self.tray_icon.connect('popup-menu', self.on_tray_icon_popup)
            self.tray_menu = gtk.Menu()
            quit_item = gtk.ImageMenuItem(gtk.STOCK_QUIT)
            quit_item.connect('activate', self.gtk_main_quit)
            self.tray_menu.append(quit_item)
            quit_item.show()

        # Set the account label
        self.update_account_label()

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

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

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


    def update_account_label(self):
        if self.username is not None:
            self.account_label.set_text(self.username + ': ')


    # 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
        account = None

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

        if account is None:
            username = re.sub('@', '', list_name)
            if self.accounts.has_key(username):
                account = self.accounts[username]

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

        if account is None:
            username = re.sub(r'list: (.*)/.*', r'\1', list_name)
            if self.accounts.has_key(username):
                account = self.accounts[username]

        if account is None:
            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.get_current_pane()
        self.update_single_window(pane)


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

        thread = apithreads.PostUpdate(self.api, text, reply_id)
        thread.sig_proxy.connect('update-posted', self.on_update_posted)
        self.update_entry.set_sensitive(False)
        self.update_status_bar('Posting...')
        thread.start()


    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: Hrafn.on_about()"


    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):
        thread = apithreads.PostRetweet(self.api, data['id'])
        thread.sig_proxy.connect('retweet-posted', self.on_retweet_posted)
        self.update_entry.set_sensitive(False)
        self.update_status_bar('Posting retweet...')
        thread.start()


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


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


    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, conversation):
        ot = self.db['open_tabs']
        ot.remove((name,single_tweet,conversation))
        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, conversation):
        self.remove_view(name, single_tweet, conversation)


    def get_current_pane(self):
        return self.tweet_notebook.get_nth_page(self.tweet_notebook.get_current_page())


    def add_to_notebook(self, name, single_tweet=None, conversation=False):
        # 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, conversation) not in self.db['open_tabs']:
            ot = self.db['open_tabs']
            ot.append((name,single_tweet,conversation))
            self.db['open_tabs'] = ot

        is_user = False
        if re.match('user:', name):
            is_user = True

        entries=self.num_entries
        if single_tweet and not conversation:
            entries=1

        new_pane = TweetPane(name, num_entries=entries, single_tweet=single_tweet, is_user=is_user, conversation=conversation)
        new_pane.connect('new-tweets', self.on_read_tweets_changed)
        new_pane.connect('tweets-read', self.on_read_tweets_changed)

        if is_user:
            new_pane.connect('at-clicked', self.on_at_button_clicked)
            new_pane.connect('follow-clicked', self.on_follow_button_clicked)
            apithreads.GetFollowing(api=self.api, pane=new_pane, user=name).start()
            apithreads.GetUserInfo(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, conversation)
        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('tweet-conversation', self.on_conversation)
        new_pane.connect('show-user', self.show_user_callback)

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

        # Switch to the new pane
        self.tweet_notebook.set_current_page(-1)


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


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

        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_friendship_changed(self, widget, data):
        if data['success']:
            if data['follow']:
                self.update_status_bar('Now following ' + data['user_name'])
            else:
                self.update_status_bar('No longer following ' + data['user_name'])
        else: # didn't succeed
            if data['follow']:
                self.update_status_bar('Failed to follow ' + data['user_name'])
            else:
                self.update_status_bar('Failed to unfollow ' + data['user_name'])


    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, user_name):
        self.add_to_notebook('@' + user_name)


    def on_follow_button_clicked(self, widget, follow):
        user_name = re.sub('^user: ', '', widget.get_list_name())
 
        thread = apithreads.ChangeFriendship(self.api, widget, user_name, follow)
        thread.sig_proxy.connect('friendship-changed', self.on_friendship_changed)
        thread.start()



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

        # Ctrl + Shift + Tab or Ctrl + PgUp or Ctrl + Left should go to prev tab
        elif event.state & gtk.gdk.CONTROL_MASK and ((keyname == 'Tab' and event.state & gtk.gdk.SHIFT_MASK) or keyname == 'ISO_Left_Tab' or keyname == 'Page_Up' or keyname == 'Left'):
            self.tweet_notebook.prev_page()
            return True

        # Ctrl + Tab or Ctrl + PgDown or Ctrl + Right should go to next tab
        elif event.state & gtk.gdk.CONTROL_MASK and (keyname == 'Tab' or keyname == 'Page_Down' or keyname == 'Right'):
            self.tweet_notebook.next_page()
            return True

        else:
            scrolltype = None
            if keyname == 'Page_Down':
                scrolltype = gtk.SCROLL_PAGE_FORWARD
            elif keyname == 'Page_Up':
                scrolltype = gtk.SCROLL_PAGE_BACKWARD
            elif keyname == 'Up':
                scrolltype = gtk.SCROLL_STEP_BACKWARD
            elif keyname == 'Down':
                scrolltype = gtk.SCROLL_STEP_FORWARD

            if scrolltype:
                self.get_current_pane().emit('scroll-child', scrolltype, False)
                return True
                  


    def close_current_tab(self):
        current_pane = self.get_current_pane()
        self.remove_view(current_pane.get_list_name(), current_pane.get_single_tweet(), current_pane.get_conversation())


    def on_account_changed(self, widget, new_account):
        if not (widget.get_active() and self.accounts.has_key(new_account)):
            return

        self.username = new_account
        self.api = self.accounts[self.username]
        self.db['active_user'] = self.username
        self.update_account_label()
        for i in range(0, self.tweet_notebook.get_n_pages()):
            pane = self.tweet_notebook.get_nth_page(i)
            if re.match(r'user: ', pane.get_list_name()):
                user = re.sub(r'user: ', r'', pane.get_list_name())
                apithreads.GetFollowing(api=self.api, pane=pane, user=user).start()


    def on_lists_ready(self, widget, username, list_names):
        # Setup the new sub-menu
        outer_menu_item = gtk.MenuItem(username, False)
        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, False)
            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


    def on_update_posted(self, widget, success):
        if success:
            self.reply_id = None
            self.update_entry.set_text("")
            self.update_status_bar('Tweet Posted')
        else:
            self.update_status_bar('Failed to post tweet')

        self.update_entry.set_sensitive(True)


    def on_retweet_posted(self, widget, success):
        if success:
            self.update_status_bar('Retweet Posted')
        else:
            self.update_status_bar('Failed to retweet')

        self.update_entry.set_sensitive(True)


    def debug_show_threads(self, widget):
        print 'debug_show_threads()'
        for thread in enumerate():
            print 'debug: thread: ' + thread.name


    def on_file_add_account(self, widget):
        token = apithreads.get_access_token(self.window)
        if token is None:
            return

        api = apithreads.CustomApi(token)

        if not self.accounts.has_key(api.username):
            tokens = self.db['tokens']
            tokens.append(token)
            self.db['tokens'] = tokens
            self.add_account(api)


    def add_account(self, api):
        username = api.username
        self.accounts[username] = api
        self.accounts[username].sig_proxy.connect('lists-ready', self.on_lists_ready)

        # Add account's menu item
        menu_item = gtk.RadioMenuItem(self.first_account_item, label=username, use_underline=False)

        if not self.first_account_item:
            self.first_account_item = menu_item

        menu_item.set_draw_as_radio(False)

        if not self.db.has_key('active_user'):
            self.db['active_user'] = username
        elif username == self.db['active_user']:
            menu_item.set_active(True)

        menu_item.connect('activate', self.on_account_changed, username)
        self.accounts_menu.append(menu_item)
        menu_item.show()


    def on_tray_icon_clicked(self, event):
        if self.minimized:
            self.window.deiconify()
        else:
            self.window.iconify()


    def on_tray_icon_popup(self, icon, button, activate_time):
        self.tray_menu.popup(None, None, gtk.status_icon_position_menu, button, activate_time, icon)

    
    def on_window_state_changed(self, window, event):
        if event.changed_mask & gtk.gdk.WINDOW_STATE_ICONIFIED:
            if event.new_window_state & gtk.gdk.WINDOW_STATE_ICONIFIED:
                self.minimized = True
                if not self.taskbar_min:
                    self.window.set_property('skip-taskbar-hint', True)
            else:
                self.minimized = False
                self.window.set_property('skip-taskbar-hint', False)


    def _check_config(self, config, config_file):
        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', '5')
            new_data = True
        if not config.has_option('global', 'trayicon'):
            config.set('global', 'trayicon', '1')
            new_data = True
        if not config.has_option('global', 'taskbar_when_minimized'):
            config.set('global', 'taskbar_when_minimized', '0')
            new_data = True
        if not config.has_option('global', 'debug'):
            config.set('global', 'debug', '0')
            new_data = True
        if not config.has_option('global', 'dbfile'):
            config.set('global', 'dbfile', '~/.hrafn.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()


    def on_read_tweets_changed(self, widget):
        unread_tweets = 0
        for i in range(self.tweet_notebook.get_n_pages()):
            pane = self.tweet_notebook.get_nth_page(i)
            unread_tweets += pane.num_new_tweets
            
        if unread_tweets > 0:
            self.tray_icon.set_property('blinking', True)
        else:
            self.tray_icon.set_property('blinking', False)

### end class Hrafn


# main
debug = False

parser = optparse.OptionParser()
parser.add_option('-c' ,'--config', dest="filename", default="~/.hrafn.conf", help="read configuration from FILENAME instead of the default ~/.hrafn.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()

base_icon = gtk.gdk.pixbuf_new_from_file('ui/icon.svg')
icon = base_icon.scale_simple(128, 128, gtk.gdk.INTERP_BILINEAR)
gtk.window_set_default_icon(icon)
my_twitter = Hrafn(options.filename, options.resize)

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