#!/usr/bin/python

#Audio Tools, a module and set of tools for manipulating audio data
#Copyright (C) 2007-2012  Brian Langenberger

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

#This program 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 this program; if not, write to the Free Software
#Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA

import audiotools
import audiotools.player
import sys
import gettext
import time
import select
import os
import tty
import termios
import cStringIO

gettext.install("audiotools", unicode=True)

try:
    import urwid

    class MappedButton(urwid.Button):
        def __init__(self, label, on_press=None, user_data=None,
                     key_map={}):
            urwid.Button.__init__(self, label=label,
                                  on_press=on_press,
                                  user_data=user_data)
            self.__key_map__ = key_map

        def keypress(self, size, key):
            return urwid.Button.keypress(self,
                                         size,
                                         self.__key_map__.get(key, key))

    class MappedRadioButton(urwid.RadioButton):
        def __init__(self, group, label, state='first True',
                     on_state_change=None, user_data=None,
                     key_map={}):
            urwid.RadioButton.__init__(self, group=group,
                                       label=label,
                                       state=state,
                                       on_state_change=on_state_change,
                                       user_data=user_data)
            self.__key_map__ = key_map

        def keypress(self, size, key):
            return urwid.RadioButton.keypress(self,
                                              size,
                                              self.__key_map__.get(key, key))

    class AudioProgressBar(urwid.ProgressBar):
        def __init__(self, normal, complete, sample_rate, current=0, done=100,
                     satt=None):
            urwid.ProgressBar.__init__(self,
                                       normal=normal,
                                       complete=complete,
                                       current=current,
                                       done=done,
                                       satt=satt)
            self.sample_rate = sample_rate

        def render(self, size, focus=False):
            """
            Render the progress bar.
            """

            #leeched from the orignal implementation
            #only the txt formatting differs

            (maxcol,) = size

            try:
                txt = urwid.Text("%d:%2.2d" % \
                                     ((self.current / self.sample_rate) / 60,
                                      (self.current / self.sample_rate) % 60),
                                 'center', 'clip')
            except ZeroDivisionError:
                txt = urwid.Text("0:00", 'center', 'clip')
            c = txt.render((maxcol,))

            cf = float(self.current) * maxcol / self.done
            ccol = int(cf)
            cs = 0
            if self.satt is not None:
                cs = int((cf - ccol) * 8)
            if ccol < 0 or (ccol == 0 and cs == 0):
                c._attr = [[(self.normal, maxcol)]]
            elif ccol >= maxcol:
                c._attr = [[(self.complete, maxcol)]]
            elif cs and c._text[0][ccol] == " ":
                t = c._text[0]
                cenc = self.eighths[cs].encode("utf-8")
                c._text[0] = t[:ccol] + cenc + t[ccol + 1:]
                a = []
                if ccol > 0:
                    a.append((self.complete, ccol))
                a.append((self.satt, len(cenc)))
                if maxcol - ccol - 1 > 0:
                    a.append((self.normal, maxcol - ccol - 1))
                c._attr = [a]
                c._cs = [[(None, len(c._text[0]))]]
            else:
                c._attr = [[(self.complete, ccol),
                    (self.normal, maxcol - ccol)]]
            return c

    class CDplayGUI(urwid.Frame):
        def __init__(self, cdda, metadata, track_list, audio_output):
            self.cdda = cdda
            self.metadata = metadata
            self.track_list = track_list
            self.audio_output = audio_output
            self.player = audiotools.player.CDPlayer(
                cdda=cdda,
                audio_output=audio_output,
                next_track_callback=self.next_track)

            self.track_name = urwid.Text("")
            self.album_name = urwid.Text("")
            self.artist_name = urwid.Text("")
            self.tracknum = urwid.Text("")
            self.length = urwid.Text("", align='right')
            self.channels = urwid.Text("2ch", align='right')
            self.sample_rate = urwid.Text("44100Hz", align='right')
            self.bits_per_sample = urwid.Text("16bps", align='right')
            self.play_pause_button = MappedButton("Play",
                                                  on_press=self.play_pause,
                                                  key_map={'tab': 'right'})
            self.progress = AudioProgressBar(normal='pg normal',
                                             complete='pg complete',
                                             sample_rate=0,
                                             current=0,
                                             done=100)
            self.progress.sample_rate = 44100

            self.track_group = []

            track_len = cdda.length() / 75

            self.status = urwid.Text(
                "%(count)d tracks, %(min)d:%(sec)2.2d minutes" % {
                    "count": len(self.track_list),
                    "min": int(track_len) / 60,
                    "sec": int(track_len) % 60},
                align='center')

            header = urwid.Pile([
                    urwid.Columns([
                            ('fixed', 9, urwid.Text(('header', u"Name : "),
                                                    align='right')),
                            ('weight', 1, self.track_name)]),
                    urwid.Columns([
                            ('fixed', 9, urwid.Text(('header', u"Album : "),
                                                    align='right')),
                            ('weight', 1, self.album_name)]),
                    urwid.Columns([
                            ('fixed', 9, urwid.Text(('header', u"Artist : "),
                                                    align='right')),
                            ('weight', 1, self.artist_name)]),
                    urwid.Columns([
                            ('fixed', 9, urwid.Text(('header', u"Track : "),
                                                    align='right')),
                            ('fixed', 7, self.tracknum),
                            ('fixed', 7, self.length),
                            ('fixed', 5, self.channels),
                            ('fixed', 10, self.sample_rate),
                            ('fixed', 7, self.bits_per_sample)]),
                    self.progress])

            controls = urwid.GridFlow([
                    MappedButton("Prev", on_press=self.previous_track,
                                 key_map={'tab': 'right'}),
                    self.play_pause_button,
                    MappedButton("Next", on_press=self.next_track,
                                 key_map={'tab': 'down'})],
                                      9, 4, 1, 'center')
            controls.set_focus(1)

            def track_label(label, track_number):
                if (len(label) > 0):
                    return label
                else:
                    return "track %2.2d" % (track_number)

            track_list = urwid.ListBox([
                    MappedRadioButton(
                        group=self.track_group,
                        label=track_label(
                            metadata[track.track_number].track_name,
                            track.track_number),
                        user_data=(track,
                                   metadata[track.track_number].track_name,
                                   metadata[track.track_number].artist_name,
                                   metadata[track.track_number].album_name),
                        on_state_change=self.select_track,
                        key_map={'tab': 'down'})
                    for track in self.track_list])
            body = urwid.Pile([('fixed', 2, urwid.Filler(urwid.Pile([
                                controls, urwid.Divider(div_char='-')]))),
                               ('weight', 1, track_list)])
            footer = urwid.Pile([urwid.Divider(div_char='-'),
                                 self.status])

            urwid.Frame.__init__(self, body=body, header=header, footer=footer)

        def next_track(self, user_data=None):
            track_index = [g.state for g in self.track_group].index(True)
            try:
                self.track_group[track_index + 1].set_state(True)
            except IndexError:
                pass

        def previous_track(self, user_data=None):
            track_index = [g.state for g in self.track_group].index(True)
            try:
                if (track_index == 0):
                    raise IndexError()
                else:
                    self.track_group[track_index - 1].set_state(True)
            except IndexError:
                pass

        def select_track(self, radio_button, new_state, user_data,
                         auto_play=True):
            if (new_state == True):
                (track,
                 track_name,
                 artist_name,
                 album_name) = user_data
                self.track_name.set_text(track_name)
                self.album_name.set_text(album_name)
                self.artist_name.set_text(artist_name)
                self.tracknum.set_text(u"%d / %d" % \
                                           (track.track_number,
                                            len(self.cdda)))

                track_length = track.length() / 75
                self.length.set_text("%d:%2.2d" % (int(track_length) / 60,
                                                  int(track_length) % 60))

                self.progress.current = 0
                self.progress.done = track.length() * (44100 / 75)

                self.player.open(track.track_number)
                if (auto_play):
                    self.player.play()
                    self.play_pause_button.set_label("Pause")

        def update_status(self):
            self.progress.set_completion(self.player.progress()[0])

        def play_pause(self, user_data):
            self.player.toggle_play_pause()
            if (self.play_pause_button.get_label() == "Play"):
                self.play_pause_button.set_label("Pause")
            elif (self.play_pause_button.get_label() == "Pause"):
                self.play_pause_button.set_label("Play")

        def handle_text(self, i):
            if ((i == 'esc') or (i == 'q') or (i == 'Q')):
                self.perform_exit()
            elif ((i == 'n') or (i == 'N')):
                self.next_track()
            elif ((i == 'p') or (i == 'P')):
                self.previous_track()

        def perform_exit(self, *args):
            self.player.close()
            raise urwid.ExitMainLoop()

    def timer(main_loop, cdplay):
        cdplay.update_status()
        loop.set_alarm_at(tm=time.time() + 1,
                          callback=timer,
                          user_data=cdplay)

    interactive_available = True
except ImportError:
    interactive_available = False


class CDplayTTY:
    OUTPUT_FORMAT = (u"%(track_number)d/%(track_total)d " +
                     u"[%(sent_minutes)d:%(sent_seconds)2.2d / " +
                     u"%(total_minutes)d:%(total_seconds)2.2d] " +
                     u"%(channels)dch %(sample_rate)dHz " +
                     u"%(bits_per_sample)d-bit")

    def __init__(self, cdda, track_list, audio_output):
        self.track_list = track_list
        self.player = audiotools.player.CDPlayer(
            cdda=cdda,
            audio_output=audio_output,
            next_track_callback=self.next_track)
        self.track_index = -1
        self.current_track = None
        self.seconds_total = 0
        self.track_number = 0
        self.track_total = len(self.track_list)

    def quit(self):
        if (self.current_track is not None):
            self.current_track = None
            self.player.close()

    def toggle_play_pause(self):
        if (self.current_track is not None):
            self.player.toggle_play_pause()

    def play(self):
        self.next_track()

    def previous_track(self):
        if (self.track_index > 0):
            self.track_index -= 1
            self.current_track = self.track_list[self.track_index]
            self.track_number = self.track_index + 1
            self.player.open(self.current_track.track_number)
            self.player.play()

    def next_track(self):
        try:
            self.track_index += 1
            self.current_track = self.track_list[self.track_index]
            self.track_number = self.track_index + 1
            self.player.open(self.current_track.track_number)
            self.player.play()
        except IndexError:
            self.current_track = None
            self.player.close()

    def progress(self):
        return self.player.progress()

    def progress_line(self):
        return (self.OUTPUT_FORMAT %
                {"track_number": self.track_number,
                 "track_total": self.track_total,
                 "sent_minutes":
                     (frames_sent / 44100) / 60,
                 "sent_seconds":
                     (frames_sent / 44100) % 60,
                 "total_minutes":
                     (frames_total / 44100) / 60,
                 "total_seconds":
                     (frames_total / 44100) % 60,
                 "channels": 2,
                 "sample_rate": 44100,
                 "bits_per_sample": 16})

if (__name__ == '__main__'):
    parser = audiotools.OptionParser(
        usage=_(u"%prog [track 1] [track 2] ..."),
        version="Python Audio Tools %s" % (audiotools.VERSION))

    parser.add_option(
        '-V', '--verbose',
        action='store',
        dest='verbosity',
        choices=audiotools.VERBOSITY_LEVELS,
        default=audiotools.DEFAULT_VERBOSITY,
        help=_(u'the verbosity level to execute at'))

    parser.add_option(
        '-I', '--interactive',
        action='store_true',
        default=False,
        dest='interactive',
        help=_(u'run in interactive mode'))

    players_map = dict([(player.NAME, player)
                        for player in audiotools.player.AUDIO_OUTPUT])

    parser.add_option(
        '-o', '--output',
        action='store',
        dest='output',
        choices=players_map.keys(),
        default=[player.NAME for player in audiotools.player.AUDIO_OUTPUT
                 if player.available()][0],
        help=_(u"the method to play audio (choose from: %s)") % \
            u", ".join([u"\"%s\"" % (player.NAME.decode('ascii'))
                        for player in audiotools.player.AUDIO_OUTPUT
                        if player.available()]))

    parser.add_option(
        '-c', '--cdrom', action='store',
        type='string', dest='cdrom',
        default=audiotools.DEFAULT_CDROM)

    parser.add_option(
        '--shuffle', action='store_true', dest='shuffle', default=False,
        help='shuffle tracks')

    lookup = audiotools.OptionGroup(parser, _(u"CD Lookup Options"))

    lookup.add_option(
        '--musicbrainz-server', action='store',
        type='string', dest='musicbrainz_server',
        default=audiotools.MUSICBRAINZ_SERVER,
        metavar='HOSTNAME')
    lookup.add_option(
        '--musicbrainz-port', action='store',
        type='int', dest='musicbrainz_port',
        default=audiotools.MUSICBRAINZ_PORT,
        metavar='PORT')
    lookup.add_option(
        '--no-musicbrainz', action='store_false',
        dest='use_musicbrainz',
        default=audiotools.MUSICBRAINZ_SERVICE,
        help='do not query MusicBrainz for metadata')

    lookup.add_option(
        '--freedb-server', action='store',
        type='string', dest='freedb_server',
        default=audiotools.FREEDB_SERVER,
        metavar='HOSTNAME')
    lookup.add_option(
        '--freedb-port', action='store',
        type='int', dest='freedb_port',
        default=audiotools.FREEDB_PORT,
        metavar='PORT')
    lookup.add_option(
        '--no-freedb', action='store_false',
        dest='use_freedb',
        default=audiotools.FREEDB_SERVICE,
        help='do not query FreeDB for metadata')

    parser.add_option_group(lookup)

    (options, args) = parser.parse_args()
    msg = audiotools.Messenger("trackplay", options)

    if (options.interactive and (not interactive_available)):
        msg.error(_(u"urwid is required for interactive mode"))
        msg.output(_(u"Please download and install urwid " +
                     u"from http://excess.org/urwid/"))
        msg.output(_(u"or your system's package manager."))
        sys.exit(1)

    try:
        cdda = audiotools.CDDA(options.cdrom, perform_logging=False)
    except IOError, err:
        msg.error(unicode(err) + _(u". Is that an audio cd ?"))
        sys.exit(-1)

    if (len(cdda) == 0):
        msg.error(_(u"No CD in drive"))
        sys.exit(1)

    if (len(args) == 0):
        tracks = [cdda[i] for i in xrange(1, len(cdda) + 1)]
    else:
        tracks = []
        for tracknum in args:
            try:
                tracks.append(cdda[int(tracknum)])
            except (ValueError, IndexError):
                pass

    if (options.shuffle):
        import random

        random.shuffle(tracks)

    if (options.interactive):
        #a track_number -> MetaData dictionary
        #where track_number typically starts from 1
        metadata = dict([(m.track_number, m) for m in
                         cdda.metadata_lookup(
                    musicbrainz_server=options.musicbrainz_server,
                    musicbrainz_port=options.musicbrainz_port,
                    freedb_server=options.freedb_server,
                    freedb_port=options.freedb_port,
                    use_musicbrainz=options.use_musicbrainz,
                    use_freedb=options.use_freedb)[0]])

        cdplay = CDplayGUI(cdda=cdda,
                           metadata=metadata,
                           track_list=tracks,
                           audio_output=players_map[options.output]())
        if (len(cdda) > 0):
            cdplay.select_track(None, True,
                                (tracks[0],
                                 metadata[1].track_name,
                                 metadata[1].artist_name,
                                 metadata[1].album_name),
                                False)

        loop = urwid.MainLoop(cdplay,
                              [('header', 'default,bold', 'default', ''),
                               ('pg normal', 'white', 'black', 'standout'),
                               ('pg complete', 'white', 'dark blue'),
                               ('pg smooth', 'dark blue', 'black')],
                              unhandled_input=cdplay.handle_text)
        loop.set_alarm_at(tm=time.time() + 1,
                          callback=timer,
                          user_data=cdplay)

        loop.run()
    else:
        trackplay = CDplayTTY(cdda=cdda,
                              track_list=tracks,
                              audio_output=players_map[options.output]())
        trackplay.play()
        output_line_len = 0

        original_terminal_settings = termios.tcgetattr(0)
        try:
            tty.setcbreak(sys.stdin.fileno())
            while (trackplay.current_track is not None):
                (frames_sent, frames_total) = trackplay.progress()
                output_line = trackplay.progress_line()
                msg.ansi_clearline()
                if (len(output_line) > output_line_len):
                    output_line_len = len(output_line)
                    msg.partial_output(output_line)
                else:
                    msg.partial_output(output_line +
                                       (u" " * (output_line_len -
                                                len(output_line))))

                (r_list, w_list, x_list) = select.select([sys.stdin.fileno()],
                                                         [], [], 1)
                if (len(r_list) > 0):
                    char = os.read(sys.stdin.fileno(), 1)
                    if ((char == 'q') or
                        (char == 'Q') or
                        (char == '\x1B')):
                        trackplay.quit()
                    elif (char == ' '):
                        trackplay.toggle_play_pause()
                    elif ((char == 'n') or
                          (char == 'N')):
                        trackplay.next_track()
                    elif ((char == 'p') or
                          (char == 'P')):
                        trackplay.previous_track()
                    else:
                        pass

            msg.ansi_clearline()
        finally:
            termios.tcsetattr(0, termios.TCSADRAIN, original_terminal_settings)
