#!/usr/bin/python

#Audio Tools, a module and set of tools for manipulating audio data
#Copyright (C) 2007-2010  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 sys
import os.path
import gettext
from audiotools import get_xml_nodes,get_xml_text_node

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

try:
    import gtk
    import gtk.gdk
    import gtk.glade
    import gobject
    import pango
except ImportError:
    audiotools.Messenger("editxmcd",None).error(_(u"PyGTK2 is required"))
    sys.exit(1)


#   columns
(
  COLUMN_NUMBER,
  COLUMN_NAME,
  COLUMN_EDITABLE
) = range(3)

class EditXMCD:
    #track_data is a track_id->(name,artist,extra) dict
    def __init__(self,xml,messenger):
        self.xml = xml
        self.msg = messenger

        # create tree view
        self.track_model = self.__create_model()
        treeview = xml.get_widget("track_names")
        treeview.set_model(self.track_model)
        treeview.set_rules_hint(True)
        treeview.get_selection().set_mode(gtk.SELECTION_SINGLE)
        self.__add_columns(treeview,_(u"Name"),self.on_field_edited)

        self.artist_model = self.__create_model()
        treeview = xml.get_widget("track_artists")
        treeview.set_model(self.artist_model)
        treeview.set_rules_hint(True)
        treeview.get_selection().set_mode(gtk.SELECTION_SINGLE)
        self.__add_columns(treeview,_(u"Artist"),self.on_field_edited)

        self.extra_model = self.__create_model()
        treeview = xml.get_widget("track_extras")
        treeview.set_model(self.extra_model)
        treeview.set_rules_hint(True)
        treeview.get_selection().set_mode(gtk.SELECTION_SINGLE)
        self.__add_columns(treeview,_(u"Extra"),self.on_field_edited)

        self.xmcd = None
        self.filename = None
        self.edited = False

        #a stack of (undo_function,undo_args,redo_functions,redo_args) tuples
        #to undo/redo the the latest actions
        self.undo_stack = []

        #a stack of (undo_function,undo_args,redo_functions,redo_args) tuples
        #to undo/redo the the latest actions
        self.redo_stack = []

        #a dict of widget name->text values corresponding to the
        #current Entry values
        #(so when Editable's "changed" signal is sent,
        # the old value is known, so an undo entry can be made)
        self.current_fields = {}

    def __create_model(self):
        return gtk.ListStore(
            gobject.TYPE_INT,
            gobject.TYPE_STRING,
            gobject.TYPE_BOOLEAN)

    #album_name,artist_name,album_year,album_extra should be unicode strings
    #tracks,artists,extras should be [track_number,unicode] pairs
    def set_data(self,
                 album_name,artist_name,album_year,album_extra,catalog,
                 tracks,artists,extras):
        self.undo_stack = []
        self.redo_Stack = []
        self.current_fields = {}
        self.xml.get_widget("album_name").set_text(album_name)
        self.xml.get_widget("artist_name").set_text(artist_name)
        self.xml.get_widget("album_year").set_text(album_year)
        self.xml.get_widget("album_extra").set_text(album_extra)
        self.xml.get_widget("catalog").set_text(catalog)

        for (model,data) in zip([self.track_model,
                                 self.artist_model,
                                 self.extra_model],
                                [tracks,artists,extras]):
            model.clear()
            for item in data:
                iter = model.append()

                model.set(iter,
                          COLUMN_NUMBER, item[COLUMN_NUMBER],
                          COLUMN_NAME, item[COLUMN_NAME],
                          COLUMN_EDITABLE, True)

    def __add_columns(self, treeview, name_header, edited_method):
        model = treeview.get_model()

        # number column
        renderer = gtk.CellRendererText()
        renderer.set_property('xalign',1.0)
        renderer.set_property('alignment',pango.ALIGN_RIGHT)
        renderer.set_property('family','Monospace')
        renderer.set_property('weight',800)
        renderer.set_data("column", COLUMN_NUMBER)
        column = gtk.TreeViewColumn("#", renderer, text=COLUMN_NUMBER)
        treeview.append_column(column)

        # name column
        renderer = gtk.CellRendererText()
        renderer.connect("edited", edited_method, model)
        renderer.set_data("column", COLUMN_NAME)
        column = gtk.TreeViewColumn(name_header, renderer, text=COLUMN_NAME,
                                    editable=COLUMN_EDITABLE)
        treeview.append_column(column)

    def file_error(self,error_message,error_title):
        self.xml.get_widget("fileerrormessage").set_text(error_message)
        error_dialog = self.xml.get_widget("fileerror")
        error_dialog.set_title(error_title)
        error_dialog.run()

    def on_field_edited(self, cell, path_string, new_text, model):
        iter = model.get_iter_from_string(path_string)
        path = model.get_path(iter)[0]
        column = cell.get_data("column")

        if column == COLUMN_NAME:
            old_text = model.get(iter,column)[0]
            model.set(iter, column, new_text)

        if (old_text != new_text):
            self.undo_stack.append((model.set,
                                    [iter,column,old_text],
                                    model.set,
                                    [iter,column,new_text]))
            self.edited = True
            self.set_title()

    def cut(self,caller):
        widget = self.xml.get_widget("window").get_focus()
        if (hasattr(widget,"cut_clipboard")):
            widget.cut_clipboard()

    def copy(self,caller):
        widget = self.xml.get_widget("window").get_focus()
        if (hasattr(widget,"copy_clipboard")):
            widget.copy_clipboard()

    def paste(self,caller):
        widget = self.xml.get_widget("window").get_focus()
        if (hasattr(widget,"paste_clipboard")):
            widget.paste_clipboard()

    def clear(self,caller):
        widget = self.xml.get_widget("window").get_focus()
        if (hasattr(widget,"delete_selection")):
            widget.delete_selection()

    def xmcd_update(self):
        if (self.xmcd is None):
            return

        self.xmcd['DTITLE'] = u"%s / %s" % \
            (self.xml.get_widget("artist_name").get_text().decode('utf-8'),
             self.xml.get_widget("album_name").get_text().decode('utf-8'))
        self.xmcd['DYEAR'] = self.xml.get_widget("album_year").get_text().decode('utf-8')
        self.xmcd['EXTDD'] = self.xml.get_widget("album_extra").get_text().decode('utf-8')

        for i in xrange(len(self.track_model)):
            track_name = self.track_model[i][1].decode('utf-8')
            artist_name = self.artist_model[i][1].decode('utf-8')
            if (len(artist_name) > 0):
                self.xmcd['TTITLE%s' % (i)] = u"%s / %s" % (artist_name,
                                                            track_name)
            else:
                self.xmcd['TTITLE%s' % (i)] = track_name

        for i in xrange(len(self.extra_model)):
            self.xmcd['EXTT%s' % (i)] = self.extra_model[i][1].decode('utf-8')

    def mbxml_update(self):
        def update_text_node(document, parent, child_tag, new_value):
            for child_node in parent.childNodes:
                if (hasattr(child_node,"tagName") and
                    (child_node.tagName == child_tag)):
                    if (child_node.firstChild is not None):
                        child_node.replaceChild(
                            document.createTextNode(new_value),
                            child_node.firstChild)
                        break
                    else:
                        child_node.appendChild(
                            document.createTextNode(new_value))
                        break
            else:
                child_node = document.createElement(child_tag)
                child_node.appendChild(
                    document.createTextNode(new_value))
                parent.appendChild(child_node)

        if (self.xmcd is None):
            return

        try:
            dom = self.xmcd.dom
            release = self.xmcd.dom.getElementsByTagName(u'release')[0]
        except IndexError:
            return

        update_text_node(
            dom, release, u'title',
            self.xml.get_widget("album_name").get_text().decode('utf-8'))

        try:
            old_artist_node = get_xml_nodes(release,u'artist')[0]
        except IndexError:
            old_artist_node = dom.createElement(u'artist')
            release.appendChild(old_artist_node)

        artist_name_string = self.xml.get_widget("artist_name").get_text().decode('utf-8')
        if (get_xml_text_node(old_artist_node,u'name') != artist_name_string):
            artist_name_text = dom.createTextNode(artist_name_string)
            artist_name = dom.createElement(u'name')
            artist_name.appendChild(artist_name_text)
            new_artist_node = dom.createElement(u'artist')
            new_artist_node.appendChild(artist_name)
            release.replaceChild(new_artist_node,old_artist_node)

        try:
            release_events = get_xml_nodes(release,u'release-event-list')[0]
        except IndexError:
            release_events = dom.createElement(u'release-event-list')
            release.appendChild(release_events)

        try:
            event = get_xml_nodes(release_events,u'event')[-1]
        except IndexError:
            event = dom.createElement('event')
            release_events.appendChild(event)

        event.setAttribute(
            'date',
            self.xml.get_widget("album_year").get_text().decode('utf-8'))
        event.setAttribute(
            'catalog-number',
            self.xml.get_widget("catalog").get_text().decode('utf-8'))


        try:
            for (i,track_node) in enumerate(get_xml_nodes(
                    get_xml_nodes(release,u'track-list')[0],u'track')):
                track_name = self.track_model[i][1].decode('utf-8')
                artist_name = self.artist_model[i][1].decode('utf-8')

                update_text_node(dom, track_node, u'title', track_name)

                if (len(artist_name) > 0):
                    #add/modify a new <artist> child tag of <track>
                    artist_name_text = dom.createTextNode(artist_name)
                    artist_name = dom.createElement(u'name')
                    artist_name.appendChild(artist_name_text)
                    new_artist_node = dom.createElement(u'artist')
                    new_artist_node.appendChild(artist_name)
                    try:
                        track_node.replaceChild(
                            new_artist_node,
                            get_xml_nodes(track_node,u'artist')[0])
                    except IndexError:
                        track_node.appendChild(new_artist_node)
                else:
                    #if <track> has an <artist> child, delete it
                    try:
                        track_node.removeChild(
                            get_xml_nodes(track_node,u'artist')[0])
                    except IndexError:
                        pass

        except IndexError:
            pass

    metadata_update = xmcd_update

    def new_xmcd(self,caller):
        self.xml.get_widget("select_xmcd_tracks").run()

    def new_mbxml(self,caller):
        self.xml.get_widget("select_mbxml_tracks").run()

    def from_xmcd_tracks(self,dialog,signal):
        dialog.hide()
        if (signal == -5):
            audiofiles = audiotools.open_files(dialog.get_filenames())
            if ((len(audiofiles) == 1) and
                audiofiles[0].get_cuesheet() is not None):
                self.read_xmcd(audiotools.XMCD.from_cuesheet(
                        audiofiles[0].get_cuesheet(),
                        audiofiles[0].total_frames(),
                        audiofiles[0].sample_rate(),
                        audiofiles[0].get_metadata()))
            else:
                self.read_xmcd(audiotools.XMCD.from_files(audiofiles))
            self.edited = False
            self.filename = None
            self.set_title()

    def from_mbxml_tracks(self,dialog,signal):
        dialog.hide()
        if (signal == -5):
            audiofiles = audiotools.open_files(dialog.get_filenames())
            if ((len(audiofiles) == 1) and
                audiofiles[0].get_cuesheet() is not None):
                self.read_mbxml(audiotools.MusicBrainzReleaseXML.from_cuesheet(
                        audiofiles[0].get_cuesheet(),
                        audiofiles[0].total_frames(),
                        audiofiles[0].sample_rate(),
                        audiofiles[0].get_metadata()))
            else:
                self.read_mbxml(audiotools.MusicBrainzReleaseXML.from_files(
                        audiofiles))
            self.edited = False
            self.filename = None
            self.set_title()

    def save(self,caller):
        if (self.xmcd is None):
            return
        elif (self.filename is None):
            return self.saveas(caller)

        self.metadata_update()
        try:
            f = open(self.filename,'wb')
            f.write(self.xmcd.build())
            f.close()
            self.edited = False
            self.set_title()
        except IOError:
            self.file_error(_(u"Unable to write \"%s\"") % \
                                (self.msg.filename(self.filename)),
                            _(u"Write Error"))

    def saveas(self,caller):
        if (self.xmcd is not None):
            self.xml.get_widget("filesaver").run()

    def file_savedas(self,dialog,signal):
        dialog.hide()
        if (signal == -5):
            self.filename = dialog.get_filename()
            self.metadata_update()
            try:
                f = open(self.filename,'wb')
                f.write(self.xmcd.build())
                f.close()
                self.edited = False
                self.set_title()
            except IOError:
                self.file_error(_(u"Unable to write \"%s\"") % \
                                    (self.msg.filename(self.filename)),
                                _(u"Write Error"))
                self.filename = None
                self.set_title()

    def open(self,caller):
        self.xml.get_widget("filechooser").run()

    def opened(self,dialog,signal):
        dialog.hide()
        if (signal == -5):
            self.read_file(dialog.get_filename())

    def read_file(self,filename):
        self.filename = filename
        try:
            try:
                self.read_xmcd(audiotools.XMCD.read(filename))
                return
            except audiotools.XMCDException:
                pass
            try:
                self.read_mbxml(audiotools.MusicBrainzReleaseXML.read(filename))
                return
            except audiotools.MBXMLException:
                pass

            raise audiotools.MetaDataFileException(filename)

        except audiotools.MetaDataFileException,err:
            self.file_error(_(u"Error opening \"%s\"") % \
                                (self.msg.filename(filename)),
                            _(u"Read Error"))
        except IOError:
            self.file_error(_(u"Error opening \"%s\"") % \
                                (self.msg.filename(filename)),
                            _(u"Read Error"))

    def read_xmcd(self,xmcd):
        self.xmcd = xmcd
        self.metadata_update = self.xmcd_update
        self.xml.get_widget("album_extra").set_sensitive(True)
        self.xml.get_widget("catalog").set_sensitive(False)

        ttitle = self.xmcd.get('DTITLE',u'')
        if (u' / ' in ttitle):
            (album_artist,album_name) = ttitle.split(u' / ',1)
        else:
            album_name = ttitle
            album_artist = u''
        year = self.xmcd.get('DYEAR',u'')
        extra = self.xmcd.get('EXTD',u'')

        tracks = []
        artists = []
        extras = []

        for key in self.xmcd.keys():
            if (key.startswith('TTITLE')):
                ttitle = self.xmcd[key]
                tracknum = audiotools.XMCD.key_digits(key)
                if (tracknum == -1):
                    continue
                else:
                    tracknum += 1
                if (u' / ' in ttitle):
                    (artist_name,track_name) = ttitle.split(u' / ',1)
                    tracks.append([tracknum,track_name])
                    artists.append([tracknum,artist_name])
                else:
                    tracks.append([tracknum,ttitle])
                    artists.append([tracknum,u''])
            elif (key.startswith('EXTT')):
                extt = self.xmcd[key]
                tracknum = audiotools.XMCD.key_digits(key)
                if (tracknum == -1):
                    continue
                else:
                    tracknum += 1
                extras.append([tracknum,extt])

        tracks.sort()
        artists.sort()
        extras.sort()

        self.set_data(album_name=album_name,
                      artist_name=album_artist,
                      album_year=year,
                      album_extra=extra,
                      catalog=u"",
                      tracks=tracks,
                      artists=artists,
                      extras=extras)

        self.edited = False
        self.set_title()

    def read_mbxml(self,mbxml):
        self.xmcd = mbxml
        self.metadata_update = self.mbxml_update
        self.xml.get_widget("album_extra").set_sensitive(False)
        self.xml.get_widget("catalog").set_sensitive(True)

        try:
            release = mbxml.dom.getElementsByTagName(u'release')[0]
        except IndexError:
            self.set_data(album_name=u"",
                          artist_name=u"",
                          album_year=u"",
                          album_extra=u"",
                          catalog=u"",
                          tracks=[],
                          artists=[],
                          extras=[])
            self.edited = False
            self.set_title()
            return

        album_name = get_xml_text_node(release,u'title')
        try:
            #FIXME - not sure if name or sort-name should take precendence
            album_artist = get_xml_text_node(get_xml_nodes(release,u'artist')[0],
                                             u'name')
        except IndexError:
            album_artist = u""

        try:
            release_events = get_xml_nodes(release,u'release-event-list')[0]
            event = get_xml_nodes(release_events,u'event')[-1]
            year = event.getAttribute('date')[0:4]
            catalog = event.getAttribute('catalog-number')
        except IndexError:
            year = u""
            catalog = u""

        tracks = []
        track_artists = []
        try:
            for (i,track_node) in enumerate(
                get_xml_nodes(get_xml_nodes(release,u'track-list')[0],u'track')):
                tracks.append([i + 1,
                               get_xml_text_node(track_node,u'title')])
                try:
                    track_artists.append([
                            i + 1,
                            get_xml_text_node(
                                get_xml_nodes(track_node,u'artist')[0],u'name')])
                except IndexError:
                    track_artists.append([i + 1,u""])
        except IndexError:
            pass

        self.set_data(album_name=album_name,
                      artist_name=album_artist,
                      album_year=year,
                      album_extra=u"",
                      catalog=catalog,
                      tracks=tracks,
                      artists=track_artists,
                      extras=[])
        self.edited = False
        self.set_title()

    def set_title(self):
        if (self.edited):
            edited = "*"
        else:
            edited = ""

        if (self.filename is None):
            self.xml.get_widget("window").set_title(
                "%s<unnamed> - editxmcd" % (edited))
        else:
            (directory,filename) = os.path.split(self.filename)
            if (len(directory) > 0):
                self.xml.get_widget("window").set_title(
                    "%s%s (%s) - editxmcd" % (edited,filename,directory))
            else:
                self.xml.get_widget("window").set_title(
                    "%s%s - editxmcd" % (edited,filename))

    def field_changed(self,widget):
        try:
            self.undo_stack.append((self.__undo_widget__,
                                    [widget.name,
                                     self.current_fields[widget.name]],
                                    self.__undo_widget__,
                                    [widget.name,
                                     widget.get_text()]))
        except KeyError:
            pass
        self.current_fields[widget.name] = widget.get_text()
        self.edited = True
        self.set_title()

    #sets widget_name to the new value
    #then pops the newly-created undo value
    def __undo_widget__(self, widget_name, value):
        self.xml.get_widget(widget_name).set_text(value)
        self.undo_stack.pop()

    def undo(self,widget):
        if (len(self.undo_stack) == 0):
            return

        (undo_function,
         undo_args,
         redo_function,
         redo_args) = self.undo_stack.pop()
        undo_function(*undo_args)

        self.redo_stack.append((undo_function,
                                undo_args,
                                redo_function,
                                redo_args))

        self.edited = True
        self.set_title()

    def redo(self,widget):
        # print repr(self.undo_stack)
        # print repr(self.redo_stack)
        if (len(self.redo_stack) == 0):
            return

        (undo_function,
         undo_args,
         redo_function,
         redo_args) = self.redo_stack.pop()
        redo_function(*redo_args)

        self.undo_stack.append((undo_function,
                                undo_args,
                                redo_function,
                                redo_args))

        self.edited = True
        self.set_title()

    def error_closed(self,dialog,response):
        dialog.hide()

    def quit(self,caller):
        if (self.edited):
            self.xml.get_widget("confirmquit").run()
        else:
            gtk.main_quit()

    def confirm_quit(self,dialog,response):
        dialog.hide()
        if (response == -5):
            gtk.main_quit()

    def display_about(self,caller):
        self.xml.get_widget("about").run()

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

    parser.add_option('-x','--xmcd',action='store',
                      type='string',dest='xmcd',
                      metavar='FILENAME',
                      help=_(u'FreeDB XMCD file or MusicBrainz XML file'))
    (options,args) = parser.parse_args()
    msg = audiotools.Messenger("editxmcd",options)

    try:
        xml = gtk.glade.XML(audiotools.find_glade_file("editxmcd.glade"),
                            domain="audiotools")
    except IOError:
        msg.error(_(u"editxmcd.glade not found"))
        sys.exit(1)

    editxmcd = EditXMCD(xml,msg)

    xml.signal_connect("gtk_main_quit",editxmcd.quit)
    xml.signal_connect("on_cut1_activate",editxmcd.cut)
    xml.signal_connect("on_copy1_activate",editxmcd.copy)
    xml.signal_connect("on_paste1_activate",editxmcd.paste)
    xml.signal_connect("on_delete1_activate",editxmcd.clear)

    xml.signal_connect("on_new1_activate",editxmcd.new_xmcd)
    xml.signal_connect("on_new2_activate",editxmcd.new_mbxml)
    xml.signal_connect("xmcd_tracks_selected",editxmcd.from_xmcd_tracks)
    xml.signal_connect("mbxml_tracks_selected",editxmcd.from_mbxml_tracks)
    xml.signal_connect("on_save_as1_activate",editxmcd.saveas)

    xml.signal_connect("save_file",editxmcd.save)
    xml.signal_connect("open_file",editxmcd.open)
    xml.signal_connect("file_opened",editxmcd.opened)
    xml.signal_connect("file_saved_as",editxmcd.file_savedas)
    xml.signal_connect("field_changed",editxmcd.field_changed)
    xml.signal_connect("on_undo_activate",editxmcd.undo)
    xml.signal_connect("on_redo_activate",editxmcd.redo)

    xml.signal_connect("filerror_done",editxmcd.error_closed)
    xml.signal_connect("confirmquit",editxmcd.confirm_quit)
    xml.signal_connect("display_about",editxmcd.display_about)

    if (options.xmcd is not None):
        editxmcd.read_file(options.xmcd)
    elif (len(args) > 0):
        editxmcd.read_xmcd(audiotools.XMCD.from_files(
                audiotools.open_files(args)))

    gtk.main()

