#!/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 sys
import os.path
import audiotools
import audiotools.ui
import audiotools.text as _
import termios


if (audiotools.ui.AVAILABLE):
    urwid_present = True
    urwid = audiotools.ui.urwid

    class Tracktag(urwid.Frame):
        def __init__(self, tracks, metadata_choices, final_album=True):
            self.__cancelled__ = True
            status = urwid.Text(u"")

            buttons = urwid.Filler(
                urwid.Columns(
                    widget_list=[('weight', 1,
                                  urwid.Button(_.LAB_CANCEL_BUTTON,
                                               on_press=self.exit)),
                                 ('weight', 2,
                                  urwid.Button(_.LAB_APPLY_BUTTON
                                               if final_album
                                               else _.LAB_TRACK2TRACK_NEXT,
                                               on_press=self.apply))],
                    dividechars=3,
                    focus_column=1))

            self.filler = audiotools.ui.MetaDataFiller(
                [unicode(audiotools.Filename(t.filename).basename())
                 for t in tracks],
                metadata_choices,
                status)

            urwid.Frame.__init__(
                self,
                body=urwid.Pile([("weight", 1, self.filler),
                                 ("fixed", 1, buttons)]),
                footer=status)

        def apply(self, button):
            self.__cancelled__ = False
            raise urwid.ExitMainLoop()

        def exit(self, button):
            self.__cancelled__ = True
            raise urwid.ExitMainLoop()

        def cancelled(self):
            return self.__cancelled__

        def handle_text(self, i):
            if (i == 'esc'):
                self.exit(None)
            elif (i == 'f1'):
                self.filler.selected_match.select_previous_item()
            elif (i == 'f2'):
                self.filler.selected_match.select_next_item()

        def populated_metadata(self):
            """yields a new, populated MetaData object per track
            to be called once Urwid's main loop has completed"""

            for (track_id, metadata) in self.filler.selected_match.metadata():
                yield metadata

else:
    urwid_present = False


UPDATE_OPTIONS = {"track_name": ("--name",
                                 _.LAB_TRACKTAG_UPDATE_TRACK_NAME),
                  "artist_name": ("--artist",
                                  _.LAB_TRACKTAG_UPDATE_ARTIST_NAME),
                  "performer_name": ("--performer",
                                     _.LAB_TRACKTAG_UPDATE_PERFORMER_NAME),
                  "composer_name": ("--composer",
                                    _.LAB_TRACKTAG_UPDATE_COMPOSER_NAME),
                  "conductor_name": ("--conductor",
                                     _.LAB_TRACKTAG_UPDATE_CONDUCTOR_NAME),
                  "album_name": ("--album",
                                 _.LAB_TRACKTAG_UPDATE_ALBUM_NAME),
                  "catalog": ("--catalog",
                              _.LAB_TRACKTAG_UPDATE_CATALOG),
                  "track_number": ("--number",
                                   _.LAB_TRACKTAG_UPDATE_TRACK_NUMBER),
                  "track_total": ("--track-total",
                                  _.LAB_TRACKTAG_UPDATE_TRACK_TOTAL),
                  "album_number": ("--album-number",
                                   _.LAB_TRACKTAG_UPDATE_ALBUM_NUMBER),
                  "album_total": ("--album-total",
                                  _.LAB_TRACKTAG_UPDATE_ALBUM_TOTAL),
                  "ISRC": ("--ISRC",
                           _.LAB_TRACKTAG_UPDATE_ISRC),
                  "publisher": ("--publisher",
                                _.LAB_TRACKTAG_UPDATE_PUBLISHER),
                  "media": ("--media-type",
                            _.LAB_TRACKTAG_UPDATE_MEDIA),
                  "year": ("--year",
                           _.LAB_TRACKTAG_UPDATE_YEAR),
                  "date": ("--date",
                           _.LAB_TRACKTAG_UPDATE_DATE),
                  "copyright": ("--copyright",
                                _.LAB_TRACKTAG_UPDATE_COPYRIGHT),
                  "comment": ("--comment",
                              _.LAB_TRACKTAG_UPDATE_COMMENT)}

REMOVE_OPTIONS = {"track_name": ("--remove-name",
                                 _.LAB_TRACKTAG_REMOVE_TRACK_NAME),
                  "artist_name": ("--remove-artist",
                                  _.LAB_TRACKTAG_REMOVE_ARTIST_NAME),
                  "performer_name": ("--remove-performer",
                                     _.LAB_TRACKTAG_REMOVE_PERFORMER_NAME),
                  "composer_name": ("--remove-composer",
                                    _.LAB_TRACKTAG_REMOVE_COMPOSER_NAME),
                  "conductor_name": ("--remove-conductor",
                                     _.LAB_TRACKTAG_REMOVE_CONDUCTOR_NAME),
                  "album_name": ("--remove-album",
                                 _.LAB_TRACKTAG_REMOVE_ALBUM_NAME),
                  "catalog": ("--remove-catalog",
                              _.LAB_TRACKTAG_REMOVE_CATALOG),
                  "track_number": ("--remove-number",
                                   _.LAB_TRACKTAG_REMOVE_TRACK_NUMBER),
                  "track_total": ("--remove-track-total",
                                  _.LAB_TRACKTAG_REMOVE_TRACK_TOTAL),
                  "album_number": ("--remove-album-number",
                                   _.LAB_TRACKTAG_REMOVE_ALBUM_NUMBER),
                  "album_total": ("--remove-album-total",
                                  _.LAB_TRACKTAG_REMOVE_ALBUM_TOTAL),
                  "ISRC": ("--remove-ISRC",
                           _.LAB_TRACKTAG_REMOVE_ISRC),
                  "publisher": ("--remove-publisher",
                                _.LAB_TRACKTAG_REMOVE_PUBLISHER),
                  "media": ("--remove-media-type",
                            _.LAB_TRACKTAG_REMOVE_MEDIA),
                  "year": ("--remove-year",
                           _.LAB_TRACKTAG_REMOVE_YEAR),
                  "date": ("--remove-date",
                           _.LAB_TRACKTAG_REMOVE_DATE),
                  "copyright": ("--remove-copyright",
                                _.LAB_TRACKTAG_REMOVE_COPYRIGHT),
                  "comment": ("--remove-comment",
                              _.LAB_TRACKTAG_REMOVE_COMMENT)}

if (__name__ == '__main__'):
    #add an enormous number of options to the parser
    #neatly categorized for convenience
    parser = audiotools.OptionParser(
        usage=_.USAGE_TRACKTAG,
        version="Python Audio Tools %s" % (audiotools.VERSION))

    parser.add_option(
        '-I', '--interactive',
        action='store_true',
        default=False,
        dest='interactive',
        help=_.OPT_INTERACTIVE_METADATA)

    text_group = audiotools.OptionGroup(parser, _.OPT_CAT_TEXT)

    for field in audiotools.MetaData.FIELD_ORDER:
        if (field in UPDATE_OPTIONS):
            variable = "update_%s" % (field)
            (option, help_text) = UPDATE_OPTIONS[field]
            text_group.add_option(
                option,
                action='store',
                type='string' if field not in
                audiotools.MetaData.INTEGER_FIELDS else 'int',
                dest=variable,
                metavar='STRING' if field not in
                audiotools.MetaData.INTEGER_FIELDS else 'INT',
                help=help_text)

    text_group.add_option(
        '--comment-file',
        action='store',
        type='string',
        dest='comment_file',
        metavar='FILENAME',
        help=_.OPT_TRACKTAG_COMMENT_FILE)

    parser.add_option_group(text_group)

    parser.add_option(
        '-r', '--replace',
        action='store_true',
        default=False,
        dest='replace',
        help=_.OPT_TRACKTAG_REPLACE)

    remove_group = audiotools.OptionGroup(parser, _.OPT_CAT_REMOVAL)

    for field in audiotools.MetaData.FIELD_ORDER:
        if (field in REMOVE_OPTIONS):
            variable = "remove_%s" % (field)
            (option, help_text) = REMOVE_OPTIONS[field]
            remove_group.add_option(
                option,
                action='store_true',
                default=False,
                dest=variable,
                help=help_text)

    parser.add_option_group(remove_group)

    lookup = audiotools.OptionGroup(parser, _.OPT_CAT_CD_LOOKUP)

    lookup.add_option(
        '-M', '--metadata-lookup', action='store_true',
        default=False, dest='metadata_lookup',
        help=_.OPT_METADATA_LOOKUP)

    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=_.OPT_NO_MUSICBRAINZ)

    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=_.OPT_NO_FREEDB)

    lookup.add_option(
        '-D', '--default',
        dest='use_default', action='store_true', default=False,
        help=_.OPT_DEFAULT)

    parser.add_option_group(lookup)

    parser.add_option(
        '--replay-gain',
        action='store_true',
        default=False,
        dest='add_replay_gain',
        help=_.OPT_REPLAY_GAIN_TRACKTAG)

    parser.add_option(
        '-j', '--joint',
        action='store',
        type='int',
        default=audiotools.MAX_JOBS,
        dest='max_processes',
        help=_.OPT_JOINT)

    parser.add_option(
        '-V', '--verbose',
        action='store',
        dest='verbosity',
        choices=audiotools.VERBOSITY_LEVELS,
        default=audiotools.DEFAULT_VERBOSITY,
        help=_.OPT_VERBOSE)

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

    #ensure interactive mode is available, if selected
    if (options.interactive and (not audiotools.ui.AVAILABLE)):
        audiotools.ui.not_available_message(msg)
        sys.exit(1)

    #open a --comment-file as UTF-8
    if (options.comment_file is not None):
        try:
            comment_file = open(options.comment_file,
                                "rb").read().decode('utf-8', 'replace')
        except IOError:
            msg.error(_.ERR_TRACKTAG_COMMENT_IOERROR %
                      (audiotools.Filename(options.comment_file),))
            sys.exit(1)

        if (((comment_file.count(u"\uFFFD") * 100) /
             len(comment_file)) >= 10):
            msg.error(_.ERR_TRACKTAG_COMMENT_NOT_UTF8 %
                      (audiotools.Filename(options.comment_file),))
            sys.exit(1)
    else:
        comment_file = None

    #open our set of input files for tagging
    try:
        tracks = audiotools.open_files(args,
                                       messenger=msg,
                                       no_duplicates=True)
    except audiotools.DuplicateFile, err:
        msg.error(_.ERR_DUPLICATE_FILE % (err.filename,))
        sys.exit(1)

    if (len(tracks) == 0):
        msg.error(_.ERR_1_FILE_REQUIRED)
        sys.exit(1)

    if (not options.metadata_lookup):
        #if not performing metadata lookup,
        #build fresh MetaData objects
        #and tag them all simultaneously
        input_albums = [(tracks,
                         [t.get_metadata() for t in tracks],
                         [[audiotools.MetaData() for t in tracks]])]
    else:
        #if performining metadata lookup,
        #get initial metadata from CD lookup service
        #for each album in set of tracks
        #and tag them album-by-album
        input_albums = []
        for album_tracks in audiotools.group_tracks(tracks):
            track_metadatas = [t.get_metadata() for t in album_tracks]

            metadata_choices = audiotools.track_metadata_lookup(
                audiofiles=album_tracks,
                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)

            if (not options.interactive):
                #if interactive mode not indicated,
                #have user pick choice right away
                metadata_choices = [
                    audiotools.ui.select_metadata(
                        metadata_choices, msg, options.use_default)]
            else:
                #otherwise, add tracks' original metadatas
                #to list of possibilities
                metadata_choices.insert(0, track_metadatas)

            input_albums.append((album_tracks,
                                 track_metadatas,
                                 metadata_choices))

    #a list of (tracks, old_metadatas, new_metadatas) tuples
    #to apply once all interactive widgets have finished
    to_tag = []

    for (last_album,
         (album_tracks,
          album_track_metadatas,
          album_metadata_choices)) in audiotools.iter_last(iter(input_albums)):
        #if not replacing all metadata
        #merge metadata with that from original files, if any
        #where new values take precedence over old ones
        if (not options.replace):
            for choice in album_metadata_choices:
                for (new_metadata,
                     old_metadata) in zip(choice, album_track_metadatas):
                    if (old_metadata is not None):
                        for (attr, value) in new_metadata.empty_fields():
                            setattr(new_metadata, attr,
                                    getattr(old_metadata, attr))

        for choice in album_metadata_choices:
            for metadata in choice:
                #apply field removal options across all metadata choices
                for attr in audiotools.MetaData.FIELD_ORDER:
                    if (getattr(options, "remove_%s" % (attr))):
                        delattr(metadata, attr)

                #apply field addition options across all metadata choices
                for attr in audiotools.MetaData.FIELD_ORDER:
                    if (getattr(options, "update_%s" % (attr)) is not None):
                        value = getattr(options, "update_%s" % (attr))
                        setattr(metadata,
                                attr,
                                value.decode(audiotools.FS_ENCODING)
                                if (attr not in
                                    audiotools.MetaData.INTEGER_FIELDS)
                                else value)

                #apply comment file across all metadata choices
                if (comment_file is not None):
                    metadata.comment = comment_file

        if (options.interactive):
            #if interactive mode indicated, edit metadata choices in widget
            widget = Tracktag(album_tracks,
                              album_metadata_choices,
                              last_album)
            loop = audiotools.ui.urwid.MainLoop(
                widget,
                audiotools.ui.style(),
                unhandled_input=widget.handle_text)
            try:
                loop.run()
                msg.ansi_clearscreen()
            except (termios.error, IOError):
                msg.error(_.ERR_TERMIOS_ERROR)
                msg.info(_.ERR_TERMIOS_SUGGESTION)
                msg.info(audiotools.ui.xargs_suggestion(sys.argv))
                sys.exit(1)

            if (not widget.cancelled()):
                to_tag.append((album_tracks,
                               album_track_metadatas,
                               list(widget.populated_metadata())))
            else:
                sys.exit(0)
        else:
            #if interactive mode not indicated, use first choice
            #since all other choices have been culled already
            to_tag.append((album_tracks,
                           album_track_metadatas,
                           album_metadata_choices[0]))

    #once all final output metadata is set,
    #perform actual tagging
    for (album_tracks, old_metadatas, new_metadatas) in to_tag:
        if (not options.replace):
            #apply final metadata to tracks using update_metadata
            #if no replacement
            for (old_metadata,
                 (track,
                  new_metadata)) in zip(old_metadatas,
                                        zip(album_tracks, new_metadatas)):
                if (old_metadata is not None):
                    #merge new fields with old fields
                    field_updated = False
                    for (attr, value) in new_metadata.fields():
                        if (getattr(old_metadata,
                                    attr) != getattr(new_metadata,
                                                     attr)):
                            setattr(old_metadata, attr, value)
                            field_updated = True

                    if (field_updated):
                        #update track if at least one field has changed
                        try:
                            track.update_metadata(old_metadata)
                        except IOError, err:
                            msg.error(
                                _.ERR_ENCODING_ERROR %
                                (audiotools.Filename(track.filename),))
                            sys.exit(1)
                else:
                    try:
                        track.set_metadata(new_metadata)
                    except IOError, err:
                        msg.error(_.ERR_ENCODING_ERROR %
                                  (audiotools.Filename(track.filename),))
                        sys.exit(1)
        else:
            #apply final metadata to tracks
            #using set_metadata() if replacement
            for (track, metadata) in zip(album_tracks, new_metadatas):
                try:
                    track.set_metadata(metadata)
                except IOError, err:
                    msg.error(_.ERR_ENCODING_ERROR %
                              (audiotools.Filename(track.filename),))
                    sys.exit(1)

    #add ReplayGain to tracks, if indicated
    queue = audiotools.ExecProgressQueue(
        audiotools.ProgressDisplay(msg))

    for album_tracks in audiotools.group_tracks(tracks):
        if (options.add_replay_gain and (len(album_tracks) > 0)):
            album_number = set([t.album_number() for t in album_tracks]).pop()
            audio_type = album_tracks[0].__class__

            if (audio_type.can_add_replay_gain(album_tracks)):
                #FIXME - should pull ReplayGain text
                #from elsewhere
                queue.execute(
                    audio_type.add_replay_gain,
                    (u"%s ReplayGain%s" %
                     ((u"Adding" if audio_type.lossless_replay_gain() else
                       u"Applying"),
                      (u"" if album_number is None else
                       (u" to album %d" % (album_number))))),
                    (u"ReplayGain %s%s" %
                     ((u"added" if audio_type.lossless_replay_gain() else
                       u"applied"),
                      (u"" if album_number is None else
                       (u" to album %d" % (album_number))))),
                    [a.filename for a in album_tracks])

    #execute ReplayGain addition once all tracks have been tagged
    try:
        queue.run(options.max_processes)
    except ValueError, err:
        msg.error(unicode(err))
        sys.exit(1)
