"""
Pympc is a client for Music Player Daemon.
Copyright (C) 2004  Magnus Bjernstad <bjernstad@gmail.com>

This file is part of Pympc.

Pympc is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.

Pympc is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with Pympc; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
"""

import gtk, gtk.glade, mpdclient, gobject, config, playlist, library, sys, os
from xml.sax import saxutils

TABS = {}
COLS = playlist.COLS

class Pympc:
    def __init__(self):
        gui_file = os.path.join(os.path.split(__file__)[0], 'gui.glade')
        if os.path.isfile(gui_file):
            self.gui = gtk.glade.XML(gui_file, 'window')
        else:
            gui_file = os.path.join(sys.prefix, 'share', 'pympc', 'gui.glade')
            if os.path.isfile(gui_file):
                self.gui = gtk.glade.XML(gui_file, 'window')
            else:
                sys.stderr.write('Could not find gui definition file.\n')
                sys.exit()

        self.w = self.gui.get_widget

        self.c = False
        self.config = config.Config()
        conf = self.config.v
        self.status_string = conf['global.status_format']
        self.conn_info = (conf['global.host'], conf['global.port'], conf['global.password'])
        self.theme_dir = conf['global.theme_dir']
        self.time_format = conf['global.time_format']
        self.poll_flist = []

        self.tooltips = gtk.Tooltips()

        self.setup_accelerators()

        i = 0
        for tab in conf['gui.tabs'].split():
            TABS[tab] = i
            TABS[i] = tab
            if tab == 'playlist':
                self.pl = playlist.Playlist(self, conf, gui_file)
                self.w('notebook').append_page(self.pl.contents, gtk.Label('Playlist'))
            elif tab == 'library':
                self.lib = library.Library(self, gui_file)
                self.w('notebook').append_page(self.lib.contents, gtk.Label('Library'))
            elif tab == 'find':
                import find
                self.find = find.Find(self, gui_file, conf['search.incremental'])
                self.w('notebook').append_page(self.find.contents, gtk.Label('Find'))
            elif tab == 'cover' and conf['global.music_directory'] != '':
                import cover
                self.cover = cover.Cover(gui_file, conf['global.music_directory'])
                self.w('notebook').append_page(self.cover.contents, gtk.Label('Cover'))
            i += 1

        self.add_to_gui(conf['gui.row1'].split(), self.w('hbox_row1'))
        self.add_to_gui(conf['gui.row2'].split(), self.w('hbox_row2'))

        self.gui.signal_autoconnect(self)

        self.w('expander').set_size_request(1,-1)
        self.w('expander').add_events(gtk.gdk.BUTTON_MOTION_MASK)
        self.expander_dragged = None

        self.config.apply_state_file(self)
        gobject.timeout_add(500, self.__update_status)
        self.__update_status()
        self.pl.center()

    def start(self):
        gtk.main()

    def __update_status(self):
        if not self.c:
            if not self.__try_connect():
                return True
        try:
            self.status = self.c.status()
            self.stats = self.c.stats()
        except mpdclient.MpdError:
            self.c = False
            return True

        w = self.w
        for f in self.poll_flist:
            f[0](*f[1])

        state = self.status.stateStr()
        prev_state = self.prev_status.stateStr()
        # Update playlist
        if self.prev_status.playlist != self.status.playlist:
            if self.has_page('playlist'):
                self.pl.update()
            self.prev_status.song = -1

        # Update status string, status bar and window title
        if state in ('play', 'pause'):
            if self.status.song != self.prev_status.song or state != prev_state:
                status_string = self.__get_status_string()
                self.set_status_text(status_string)
                w('window').set_title(status_string)
            if state == 'play':
                s = self.status
                status = 'Playing, %d kbps, %d Hz' % (s.bitrate, s.sampleRate)
                self.w('statusbar').push(1, status)
            elif state == 'pause':
                self.w('statusbar').push(1, 'Paused')
        elif state == 'stop':
            self.set_status_text('Stopped')
            self.w('statusbar').push(1, 'Stopped')
            w('window').set_title('Pympc')

        # New song?
        if self.prev_status.song != self.status.song:
            if len(self.pl.tree) > 0 and self.prev_status.song >= 0:
                self.pl.tree[self.prev_status.song][len(COLS)] = False
            if len(self.pl.tree) > 0 and self.status.song >= 0:
                index = len(COLS)
                self.pl.tree[self.status.song][index] = True
            if self.get_current_page() == 'cover':
                path = self.pl.tree[self.status.song][COLS['file']]
                self.cover.new_song(path)

            # Invalidate!
            self.pl.gui_main.get_widget('view').queue_draw()

        # Database changed?
        if self.prev_stats.db_update != self.stats.db_update:
            self.lib.reload()
        # Updating db?
        if self.status.updating_db > -1:
            w('statusbar').push(1, 'Updating database...')

        self.prev_status = self.status
        self.prev_stats = self.stats
        return True

    def __try_connect(self):
        try:
            try:
                self.c = mpdclient.MpdController(*self.conn_info)
            except:
                raise mpdclient.MpdError("connection failed")
            self.w('statusbar').push(1, 'Connected')
            # connected - must update playlists etc.
            self.prev_status = mpdclient.Status()
            self.prev_stats = self.c.stats()
            self.prev_status.song = -1
            self.prev_status.state = 0
            self.lib.reload()
            return True
        except mpdclient.MpdError:
            self.w('statusbar').push(1, 'Not connected')
            self.set_status_text('Not connected')
            return False

    def seek_to(self, value, Relative=True):
        """
        Note: When seeking absolute, 'value' is a float in [0,1]
              When seeking relative, 'value' is a time in seconds
        """
        if not self.c:
            return
        try:
            s = self.status
            if Relative:
                new_time = max(s.elapsedTime + value, 0)
            else:
                new_time = max(value * float(s.totalTime), 0)
            self.c.seekid(self.status.songid, new_time)
        except mpdclient.MpdError:
            self.c = False
        if float(s.totalTime) > 0:
            self.progress.set_value(new_time / float(s.totalTime))
        else:
            self.progress.set_value(0)

    def change_volume(self, value, widget=None):
        if not self.c:
            return
        try:
            vol = self.c.status().volume
        except mpdclient.MpdError:
            self.c = False
        self.set_volume(vol + value, widget)

    def set_volume(self, value, widget=None):
        if not self.c:
            return
        try:
            self.c.setvol(max(0, int(value)))
            if widget != None:
                widget.set_value(value)
        except mpdclient.MpdError:
            self.c = False

    def cmd_random(self):
        if not self.c:
            return
        try:
            self.c.random()
        except mpdclient.MpdError:
            self.c = False

    def cmd_repeat(self):
        if not self.c:
            return
        try:
            self.c.repeat()
        except mpdclient.MpdError:
            self.c = False

    def cmd_prev(self):
        if not self.c:
            return
        try:
            self.c.prev()
            self.status = self.c.status()
        except mpdclient.MpdError:
            self.c = False

    def cmd_next(self):
        if not self.c:
            return
        try:
            self.c.next()
            self.status = self.c.status()
        except mpdclient.MpdError:
            self.c = False

    def cmd_play(self):
        if not self.c:
            return
        try:
            state = self.status.stateStr()
            if state == 'play':
                self.c.pause(1)
            elif state == 'pause':
                self.c.pause(0)
            elif state == 'stop':
                self.c.play()
        except mpdclient.MpdError:
            self.c = False

    def cmd_stop(self):
        if not self.c:
            return
        try:
            self.c.stop()
        except mpdclient.MpdError:
            self.c = False

    def cmd_crossfade(self, seconds):
        if not self.c:
            return
        try:
            self.c.crossfade(seconds)
        except mpdclient.MpdError:
            self.c = False

    def set_status_text(self, text):
        self.w('expander_label').set_label('<span size="x-large">' + saxutils.escape(text) + '</span>')

    def set_current_page(self, page):
        try:
            self.w('notebook').set_current_page(TABS[page])
        except KeyError:
            pass
    def get_current_page(self):
        try:
            return TABS[self.w('notebook').get_current_page()]
        except KeyError:
            return None
    def has_page(self, page):
        try:
            TABS[page]
            return True
        except KeyError:
            return False
    def on_switch_page(self, notebook, page, page_num):
        if   page_num == TABS.get('playlist', -1): self.pl.gui_main.get_widget('view').grab_focus()
        elif page_num == TABS.get('library', -1): self.lib.gui_main.get_widget('view').grab_focus()
        elif page_num == TABS.get('find', -1): self.find.gui_main.get_widget('entry').grab_focus()
        elif page_num == TABS.get('cover', -1):
            try:
                self.cover.new_song(self.pl.tree[self.status.song][0])
            except:
                pass

    def on_expander_activate(self, expander):
        if expander.get_expanded():
            self.window_size = self.w('window').get_size()
            expander.child.hide_all()
        else:
            expander.child.show_all()
    def on_after_expander_activate(self, expander):
        if not expander.get_expanded():
            self.window_size = self.w('window').get_size()
            self.w('window').resize(self.window_size[0], 1)
            # TODO Find way to stop height resize but allow width resize.
        else:
            self.w('window').resize(self.w('window').get_size()[0], self.window_size[1])

    def on_expander_button_release(self, widget, event):
        if event.button == 1:
            self.w('expander_eventbox').grab_remove()
            if self.expander_dragged == False:
                widget.emit('activate')
            self.expander_dragged = None
    def on_expander_button_press(self, widget, event):
        if event.button == 1:
            self.expander_dragged = False
            self.prev_x = event.x_root
            self.prev_hadj = self.w('expander_vp').get_hadjustment().get_value()
            self.w('expander_eventbox').grab_add()
        return True
    def on_expander_eventbox_motion_notify(self, widget, event):
        dx = event.x_root - self.prev_x
        if abs(dx) > 4:
            self.expander_dragged = True
            vp = self.w('expander_vp')
            hadj = vp.get_hadjustment()
            new_hadj = min(self.prev_hadj - dx, hadj.upper - hadj.page_size)
            hadj.set_value(new_hadj)

    def on_time_button_press(self, widget, event):
        if event.button == 1:
            if self.time_format == 'elapsed':
                self.time_format = 'remaining'
            else:
                self.time_format = 'elapsed'

    def setup_accelerators(self):
        accel = gtk.AccelGroup()
        accelerators = ['<Ctrl>p', '<Ctrl>l', '<Ctrl>f', '<Ctrl>o',
        'Escape', '<Ctrl>q', '<Ctrl>KP_Add', '<Ctrl>KP_Subtract', 'c', '<Ctrl>r',
        '<Ctrl>z', '<Ctrl>Left', '<Ctrl>Right', 'Delete']
        for a in accelerators:
            val, mod = gtk.accelerator_parse(a)
            accel.connect_group(val, mod, gtk.ACCEL_LOCKED, self.accel_callback)
        self.w('window').add_accel_group(accel)

    def add_to_gui(self, items, box):
        base = self.theme_dir
        new_pix = gtk.gdk.pixbuf_new_from_file
        for item in items:
            if item in ('prev', 'stop', 'next'):
                image = gtk.Image()
                image.set_from_file(base + item + '.png')
                button = gtk.Button()
                box.pack_start(button, expand=False)
                button.add(image)
                button.set_relief(gtk.RELIEF_NONE)
                button.connect ('clicked', self.on_button, item)
            elif item == 'play':
                play = gtk.Button()
                play.connect ('clicked', self.on_play)
                image = gtk.Image()
                play_pb = new_pix(base + 'play.png')
                pause_pb = new_pix(base + 'pause.png')
                image.set_from_pixbuf(play_pb)
                box.pack_start(play, expand=False)
                play.add(image)
                play.set_relief(gtk.RELIEF_NONE)
                self.poll_flist.append([self.update_play, [image, play_pb, pause_pb]])
            elif item == 'volume':
                vol = gtk.HScale()
                vol_small = gtk.SpinButton()
                vol.set_draw_value(False)
                vol.set_range(0,100)
                vol_small.set_range(0,100)
                vol_small.set_increments(5, 20)
                vol_small.set_numeric(True)
                vol_small.set_width_chars(3)
                box.pack_start(gtk.Label(' Volume: '), expand=False)
                box.pack_start(vol)
                box.pack_start(vol_small)
                vol_small.connect ('size-allocate', self.on_vol_resize_small, vol)
                vol_small.connect ('value-changed', self.on_vol_small_adjust)
                vol.connect ('adjust-bounds', self.on_vol_adjust)
                vol.connect ('scroll-event', self.on_vol_scroll)
                vol.connect ('size-allocate', self.on_vol_resize, vol_small)
                self.poll_flist.append([self.update_volume, [vol, vol_small]])
            elif item == 'random':
                random = gtk.ToggleButton()
                handler_id = random.connect('toggled', self.on_random)
                image = gtk.Image()
                on_pixbuf = new_pix(base + 'random_on.png')
                off_pixbuf = new_pix(base + 'random_off.png')
                image.set_from_pixbuf(off_pixbuf)
                box.pack_start(random, expand=False)
                random.add(image)
                random.set_relief(gtk.RELIEF_NONE)
                self.tooltips.set_tip(random, 'Random')
                self.poll_flist.append([self.update_random,
                        [random, image, on_pixbuf, off_pixbuf, handler_id]])
            elif item == 'repeat':
                repeat = gtk.ToggleButton()
                repeat_id = repeat.connect('toggled', self.on_repeat)
                repeat_image = gtk.Image()
                repeat_image.set_from_file(base + 'repeat.png')
                box.pack_start(repeat, expand=False)
                repeat.add(repeat_image)
                repeat.set_relief(gtk.RELIEF_NONE)
                self.tooltips.set_tip(repeat, 'Repeat')
                self.poll_flist.append([self.update_repeat, [repeat, repeat_id]])
            elif item == 'time':
                time = gtk.Label('00:00 / 00:00')
                time_eventbox = gtk.EventBox()
                time_eventbox.add(time)
                box.pack_start(time_eventbox, expand=False)
                time_eventbox.connect('button-press-event', self.on_time_button_press)
                self.poll_flist.append([self.update_time, [time]])
            elif item == 'progress':
                self.progress = gtk.HScale()
                self.progress.set_draw_value(False)
                self.progress.set_range(0, 1)
                box.pack_start(self.progress, expand=True)
                self.progress.connect ('adjust-bounds', self.on_progress_adjust)
                self.progress.connect ('scroll-event', self.on_progress_scroll)
                self.poll_flist.append([self.update_progress, [self.progress]])

    def quit(self):
        self.config.save_state(self)
        try:
            self.c.close()
        except:
            pass
        gtk.main_quit()
        return False

    # Polling functions
    def update_progress(self, widget):
        state = self.status.stateStr()
        if state in ('play', 'pause') and float(self.status.totalTime) > 0:
            widget.set_value(self.status.elapsedTime / float(self.status.totalTime))
        else:
            widget.set_value(0)
    def update_volume(self, vol_widget, vol_small_widget):
        if self.prev_status.volume != self.status.volume:
            vol_widget.set_value(self.status.volume)
            vol_small_widget.set_value(self.status.volume)

    def update_play(self, image, play_pixbuf, pause_pixbuf):
        state = self.status.stateStr()
        if state == 'play':
            image.set_from_pixbuf(pause_pixbuf)
        elif state == 'stop':
            image.set_from_pixbuf(play_pixbuf)
        elif state == 'pause':
            image.set_from_pixbuf(play_pixbuf)

    def update_random(self, widget, image, on_pixbuf, off_pixbuf, handler_id):
        state = self.status.random
        widget.handler_block(handler_id)
        widget.set_active(state)
        if state:
            image.set_from_pixbuf(on_pixbuf)
        else:
            image.set_from_pixbuf(off_pixbuf)
        widget.handler_unblock(handler_id)

    def update_repeat(self, widget, handler_id):
        widget.handler_block(handler_id)
        widget.set_active(self.status.repeat)
        widget.handler_unblock(handler_id)

    def update_time(self, widget):
        if self.status.totalTime <= 0: # Most probably http-stream
            widget.set_text('00:00 / 00:00')
        else:
            if self.time_format == 'elapsed':
                first_part = self.status.elapsedTime
            elif self.time_format == 'remaining':
                first_part = self.status.elapsedTime - self.status.totalTime
            widget.set_text(self.__secs_to_mmss(first_part)+\
                ' / ' + self.__secs_to_mmss(self.status.totalTime))

    def __secs_to_mmss(self, secs):
        sign = ''
        if secs < 0:
            sign = '-'
            secs = abs(secs)
        return "%s%s:%s" % (sign, str(secs/60).zfill(2), str(secs%60).zfill(2))


    # Gui callbacks
    def on_button(self, widget, button):
        if   button == 'prev': self.cmd_prev()
        elif button == 'stop': self.cmd_stop()
        elif button == 'next': self.cmd_next()
    def on_play(self, widget):   self.cmd_play()
    def on_random(self, widget): self.cmd_random()
    def on_repeat(self, widget): self.cmd_repeat()
    def on_vol_adjust(self, range, value):
        self.set_volume(value, range)
    def on_vol_small_adjust(self, widget):
        self.set_volume(widget.get_value(), widget)
    def on_vol_scroll(self, widget, event):
        if event.direction == gtk.gdk.SCROLL_UP:
            self.change_volume(2, widget)
        elif event.direction == gtk.gdk.SCROLL_DOWN:
            self.change_volume(-2, widget)
    def on_vol_resize(self, widget, alloc, vol_small_widget): 
        if alloc.width < 55: 
            widget.hide()
            vol_small_widget.show()
    def on_vol_resize_small(self, widget, alloc, vol_widget): 
        if alloc.width > 75:
            widget.hide_all()
            vol_widget.show()
    def on_progress_adjust(self, range, value):
        self.seek_to(value, Relative=False)
    def on_progress_scroll(self, widget, event):
        if event.direction == gtk.gdk.SCROLL_UP:
            self.seek_to(5)
        elif event.direction == gtk.gdk.SCROLL_DOWN:
            self.seek_to(-5)
        #return True
    def on_quit(self, widget, event):
        self.quit()

    def accel_callback(self, accel_group, acceleratable, val, modifier):
        v = gtk.gdk.keyval_from_name
        if   val == v('p'):           self.set_current_page('playlist')
        elif val == v('l'):           self.set_current_page('library')
        elif val == v('f'):           self.set_current_page('find')
        elif val == v('o'):           self.set_current_page('cover')
        elif val == v('Escape'):      self.quit()
        elif val == v('q'):           self.quit()
        elif val == v('KP_Add'):      self.change_volume(2)
        elif val == v('KP_Subtract'): self.change_volume(-2)
        elif val == v('c'):           self.pl.center()
        elif val == v('r'):           self.cmd_random()
        elif val == v('z'):           self.cmd_repeat()
        elif val == v('Left'):        self.seek_to(-5)
        elif val == v('Right'):       self.seek_to(5)
        elif val == v('Delete'):
            if not self.w('notebook').get_property('visible'):
                return
            page = self.w('notebook').get_current_page()
            if page == TABS['playlist']:
                self.pl.gui_popup.get_widget('delete').activate()
            elif page == TABS['library']:
                self.lib.gui_popup.get_widget('delete').activate()
        return False

    def __get_status_string(self):
        if self.status.song >= len(self.pl.tree):
            return
        row = self.pl.tree[self.status.song]
        return self.__rec_get_status_string(row, 0)[0]

    def __rec_get_status_string(self, row, curindex):
        format = self.status_string
        found = False   # Used for required arguments
        ret = ""        # The return string
        temp = ""       # A temporary string

        while curindex < len(format):
            # Our current character
            curchar = format[curindex]

            if curchar == '|':
                curindex += 1
                if not found:
                    # Try next item in the 'or'-list
                    ret = ''
                else:
                    # Already found one valid item - skip the rest
                    curindex = self.__skip_formatting(curindex)
                continue
            if curchar == '&':
                curindex += 1
                if not found:
                    # One argument not found - skip the rest
                    curindex = self.__skip_formatting(curindex)
                else:
                    found = False
                continue

            if curchar == '[':
                curindex += 1
                # Recursively call one self, starting after the '['
                temp, curindex = self.__rec_get_status_string(row, curindex)
                if temp != "":
                    ret += temp
                    found = True
                continue
            if curchar == ']':
                # This group is now done - return
                if not found and ret != "":
                    # But don't return incomplete strings
                    ret = ''
                break;

            if curchar != '#' and curchar != '%':
                # Add all 'normal' characters to the output
                curindex += 1
                ret += curchar
                continue

            if curchar == '#' and curindex+1 < len(format):
                # '#' is escape character - just write next character to output
                ret += format[curindex+1]
                curindex += 2
                continue

            # One '%' found
            temp = ""

            try:
                finalindex = format.index('%', curindex+1)
                # Found the ending '%'
                # Extract the option
                option = format[curindex+1: finalindex]

                if   option == "path":      temp = row[COLS['file']]
                elif option == "artist":    temp = row[COLS['Artist']]
                elif option == "album":     temp = row[COLS['Album']]
                elif option == "title":     temp = row[COLS['Title']]
                elif option == "genre":     temp = row[COLS['Genre']]
                elif option == "date":      temp = row[COLS['Date']]
                elif option == "track":     temp = row[COLS['Track']]
                elif option == "composer":  temp = row[COLS['Composer']]
                elif option == "performer": temp = row[COLS['Performer']]
                elif option == "comment":   temp = row[COLS['Comment']]

                # If we found the metadata...
                if temp != "":
                    # ...append it to the output
                    ret += temp
                    found = True
                else:
                    # Unknown option - add it anyway
                    ret += "%" + option + "%"

                # We're now done up to finalindex+1
                curindex = finalindex + 1

            except ValueError:
                # No ending '%' found - output the '%'
                ret += '%'
                curindex += 1

        # Increment the curindex as maybe we're getting called by ourselves
        curindex += 1
        return ret, curindex

    def __skip_formatting(self, curindex):
        """
        Skip everything inside inside a block.
        """
        stack = 0
        format = self.status_string

        while (curindex < len(format)):
            curchar = format[curindex]

            if curchar == '[':
                stack += 1
            elif curchar == '#' and curindex+1 < len(format):
                curindex += 1
            elif stack > 0 and curchar == ']':
                stack -= 1
            elif curchar == '&' or curchar == '|' or curchar == ']':
                return curindex

            curindex += 1

        return curindex
