import glob
import logging
import os
import sys
import traceback
from copy import deepcopy

from PyQt5.QtCore import Qt, pyqtRemoveInputHook, pyqtSignal
from PyQt5.QtWidgets import QAbstractItemView, QApplication, QCheckBox, QComboBox, QDialog, QGroupBox, QHBoxLayout, \
    QInputDialog, QLabel, QLineEdit, QPushButton, QSpinBox, QTextEdit, QToolButton, QVBoxLayout, \
    QWidget

from .. import audioinfo, version_string
from ..constants import (TEXT, COMBO, SPINBOX,
                        CHECKBOX, RIGHTDOCK, CONFIGDIR)
from ..findfunc import FuncError
from ..functions import replace_regex
from ..puddleobjects import (create_buddy, get_icon, winsettings,
                            ListBox, ListButtons, OKCancel, PuddleConfig, PuddleThread)
from ..tagsources import (tagsources, status_obj, set_useragent,
                         write_log, RetrievalError, mp3tag, SubmissionError)
from ..util import (isempty, pprint_tag,
                   split_by_field, to_string, translate)

from .releasewidget import ReleaseWidget

pyqtRemoveInputHook()

TAGSOURCE_CONFIG = os.path.join(CONFIGDIR, 'tagsources.conf')
MTAG_SOURCE_DIR = os.path.join(CONFIGDIR, 'mp3tag_sources')

DEFAULT_SEARCH_TIP = translate("Tag Sources",
                               "Enter search parameters here. If empty, the selected "
                               "files are used. <ul><li><b>artist;album</b> searches "
                               "for a specific album/artist combination.</li><li>To "
                               "list the albums by an artist leave off the album "
                               "part, but keep the semicolon (eg. <b>Ratatat;</b>). "
                               "For a album only leave the artist part as in "
                               "<b>;Resurrection.</li></ul>")

FIELDLIST_TIP = translate("Tag Sources",
                          'Enter a comma seperated list of fields to write. '
                          '<br /><br />Eg. <b>artist, album, title</b> will only '
                          'write the artist, album and title fields of the '
                          'retrieved tags. <br /><br />If you want to '
                          'exclude some fields, but write all others start the '
                          'list the tilde (~) character. Eg <b>~composer, '
                          '__image</b> will write all fields but the '
                          'composer and __image fields.')

DEFAULT_REGEXP = {'album': [r'(.*?)([\(\[\{].*[\)\]\}])', '$1']}


def apply_regexps(audio, regexps=None):
    if regexps is None:
        regexps = DEFAULT_REGEXP
    audio = deepcopy(audio)
    changed = False
    for field, (regexp, output) in regexps.items():
        if field not in audio:
            continue
        text = to_string(audio[field])
        try:
            val = replace_regex(audio, text, regexp, output)
            if val:
                audio[field] = val
                if not changed and val != text:
                    changed = val
        except FuncError:
            continue
    return changed, audio


def display_tag(tag):
    """Used to display tags in in a human parseable format."""
    tag = dict((k, v) for k, v in tag.items() if
               not k.startswith('#') and not isempty(v))

    if not tag:
        return translate("Tag Sources", "<b>Nothing to display.</b>")
    fmt = "<b>%s</b>: %s<br />"
    text = pprint_tag(tag, fmt, True)
    if text.endswith('<br />'):
        text = text[:-len('<br />')]
    return text


def load_mp3tag_sources(dirpath=MTAG_SOURCE_DIR):
    "Loads Mp3tag tag sources from dirpath and return the tag source classes."
    files = glob.glob(os.path.join(dirpath, '*.src'))
    classes = []
    for f in files:
        try:
            idents, search, album = mp3tag.open_script(f)
            classes.append(mp3tag.Mp3TagSource(idents, search, album))
        except:
            logging.exception(translate("Tag Sources", "Couldn't load Mp3tag Tag Source {}").format(f))
            continue
    return classes


def strip(audio, field_list, reverse=False, leave_exact=False):
    '''Returns dict of key/values from audio where the key is in field_list.

    If reverse is True then the dict will consist of all
    fields found in audio where the key is NOT IN in field_list.

    If the first field in field_list starts with '~' then reverse will be
    set to True.

    Any fields starting with '#' will be removed.
    '''
    if not field_list:
        ret = dict([(key, audio[key]) for key in audio if
                    not key.startswith('#')])
        if leave_exact and '#exact' in audio:
            ret['#exact'] = audio['#exact']
        return ret
    tags = field_list[::]
    if tags and tags[0].startswith('~'):
        reverse = True
        tags[0] = tags[0][1:]
    else:
        reverse = False
    if reverse:
        ret = dict([(key, audio[key]) for key in audio if key not in
                    tags and not key.startswith('#')])
    else:
        ret = dict([(key, audio[key]) for key in field_list
                    if not key.startswith('#') and key in audio])

    if leave_exact and '#exact' in audio:
        ret['#exact'] = audio['#exact']
    return ret


def split_strip(stringlist):
    '''Splits and strips each comma-delimited string in a list of strings.

    >>> split_strip(['artist, title', 'album,genre'])
    [['artist', 'title'], ['album', 'genre']]
    '''
    return [[field.strip() for field in s.split(',')] for s in stringlist]


class FieldsEdit(QWidget):
    fieldsChanged = pyqtSignal(list, name='fieldsChanged')

    def __init__(self, tags=None, parent=None):
        QWidget.__init__(self, parent)
        if not tags:
            tags = []
        label = QLabel()
        label.setText(translate("Defaults", '&Fields'))
        self._text = QLineEdit(', '.join(tags))
        label.setBuddy(self._text)

        layout = QHBoxLayout()
        layout.setContentsMargins(0, 0, 0, 0)
        layout.addWidget(label, 0)
        layout.addWidget(self._text, 1)

        self._text.textChanged.connect(
            self.emitTags)

        self.setLayout(layout)

    def emitTags(self, text=None):
        self.fieldsChanged.emit(self.tags(text))

    def setTags(self, tags):
        self._text.setText(', '.join(tags))

    def setToolTip(self, value):
        QWidget.setToolTip(self, value)
        self._text.setToolTip(value)

    def tags(self, text=None):
        if not text:
            return [_f for _f in [z.strip() for z in
                                  str(self._text.text()).split(',')] if _f]
        else:
            return [_f for _f in [z.strip() for z in str(text).split(',')] if _f]


class SimpleDialog(QDialog):
    """Class for simple dialog creation."""
    editingFinished = pyqtSignal(list, name='editingFinished')

    def __init__(self, title, controls, parent=None):
        """title => Dialog's title.
        controls is a list of 3-element-lists.

        The three 3-element lists consist of:
            description => Descriptive label for the control.
            control_type => One of TEXT, COMBO, CHECKBOX corresponding
                to a QLineEdit, QComboBox and QCheckBox being created
                respectively.
            default => Default arguments.
                Can be any string for TEXT,
                Must be a list of strings for COMBO as these will form the
                    items selectable by the combo box.
                Can be either True or False for CheckBox.

        A dialog will be created with vertical layout. Like so:
            <label>
            <control>
            <label>
            <control>

        When the user has finished editing an 'editingFinished' signal
        will be emitted containing a list with the new value.

        The list will consist of the value for each control in the order
        given. For TEXT it'll a string. COMBO an integer corresponding to
        the selected index. True or False for CHECK.
        """

        QDialog.__init__(self, parent)
        vbox = QVBoxLayout()
        self._controls = []
        winsettings(title, self)
        self.setWindowTitle(translate("Tag Sources", "Configure: {}").format(title))
        for desc, ctype, default in controls:
            if ctype == TEXT:
                control = QLineEdit(default)
                vbox.addLayout(create_buddy(desc, control))
            elif ctype == COMBO:
                control = QComboBox()
                control.addItems(default[0])
                control.setCurrentIndex(default[1])
                label = QLabel(desc)
                label.setBuddy(control)
                vbox.addWidget(label)
                vbox.addWidget(control)
            elif ctype == CHECKBOX:
                control = QCheckBox(desc)
                if default:
                    control.setCheckState(Qt.CheckState.Checked)
                else:
                    control.setCheckState(Qt.CheckState.Unchecked)
                vbox.addWidget(control)
            elif ctype == SPINBOX:
                control = QSpinBox()
                control.setMinimum(default[0])
                control.setMaximum(default[1])
                control.setValue(default[2])
                vbox.addLayout(create_buddy(desc, control))

            self._controls.append(control)
        okcancel = OKCancel()
        okcancel.ok.connect(self.okClicked)
        okcancel.cancel.connect(self.close)
        vbox.addLayout(okcancel)
        vbox.addStretch()
        self.setLayout(vbox)

    def okClicked(self):
        values = []
        for control in self._controls:
            if isinstance(control, QLineEdit):
                values.append(str(control.text()))
            elif isinstance(control, QComboBox):
                values.append(control.currentIndex())
            elif isinstance(control, QCheckBox):
                values.append(control.isChecked())
            elif isinstance(control, QSpinBox):
                values.append(control.value())
        self.editingFinished.emit(values)
        self.close()


class SortOptionEditor(QDialog):
    options = pyqtSignal(list, name='options')

    def __init__(self, options, parent=None):
        """options is a list of strings. Each a comma-delimited field list.

        Eg. ['artist, title', 'album, genre']
        """

        QDialog.__init__(self, parent)
        connect = lambda c, signal, s: getattr(c, signal).connect(s)
        self.listbox = ListBox()
        self.listbox.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)

        buttons = ListButtons()

        self.listbox.addItems(options)
        hbox = QHBoxLayout()
        hbox.addWidget(self.listbox, 1)

        hbox.addLayout(buttons)

        okcancel = OKCancel()
        vbox = QVBoxLayout()
        vbox.addLayout(hbox)
        vbox.addLayout(okcancel)
        self.setLayout(vbox)

        connect(buttons, "add", self.addPattern)
        connect(buttons, "edit", self.editItem)
        buttons.duplicateButton.setVisible(False)
        okcancel.ok.connect(self.applySettings)
        okcancel.cancel.connect(self.applySettings)
        self.listbox.connectToListButtons(buttons)
        self.listbox.editButton = buttons.editButton
        connect(self.listbox, "itemDoubleClicked", self._doubleClicked)

    def addPattern(self):
        l = self.listbox.item
        patterns = [str(l(z).text()) for z in range(self.listbox.count())]
        row = self.listbox.currentRow()
        if row < 0:
            row = 0
        (text, ok) = QInputDialog().getItem(self, translate("Tag Sources",
                                                            'Add sort option'),
                                            translate("Tag Sources",
                                                      'Enter a sorting option (a comma-separated list of fields. '
                                                      'Eg. "artist, title")'), patterns, row)
        if ok:
            self.listbox.clearSelection()
            self.listbox.addItem(text)
            self.listbox.setCurrentRow(self.listbox.count() - 1)

    def _doubleClicked(self, item):
        self.editItem()

    def editItem(self, row=None):
        if row is None:
            row = self.listbox.currentRow()
        l = self.listbox.item
        patterns = [str(l(z).text()) for z in range(self.listbox.count())]
        (text, ok) = QInputDialog().getItem(self,
                                            translate("Tag Sources", 'Edit sort option'),
                                            translate("Tag Sources",
                                                      'Enter a sorting option (a comma-separated list of fields. '
                                                      'Eg. "artist, title")'), patterns, row)
        if ok:
            item = l(row)
            item.setText(text)
            item.setSelected(True)

    def applySettings(self):
        item = self.listbox.item
        options = [str(item(row).text())
                   for row in range(self.listbox.count())]
        self.close()
        self.options.emit(options)


class SettingsDialog(QWidget):

    def __init__(self, parent=None, status=None):
        QWidget.__init__(self, parent)
        self.title = translate('Settings', 'Tag Sources')

        label = QLabel(translate("Tag Sources",
                                 '&Display format for individual tracks.'))
        self._text = QLineEdit()
        label.setBuddy(self._text)

        albumlabel = QLabel(translate("Tag Sources",
                                      'Display format for &retrieved albums'))
        self._albumdisp = QLineEdit()
        albumlabel.setBuddy(self._albumdisp)

        sortlabel = QLabel(translate("Tag Sources",
                                     'Sort retrieved albums using order:'))
        self._sortoptions = QComboBox()
        sortlabel.setBuddy(self._sortoptions)
        editoptions = QPushButton(translate("Defaults", '&Edit'))
        editoptions.clicked.connect(self._editOptions)

        ua_label = QLabel(translate("Tag Sources",
                                    'User-Agent to when accessing web sites.'))
        self._ua = QTextEdit()

        self.jfdi = QCheckBox(translate('Profile Editor',
                                        'Brute force unmatched files.'))
        self.jfdi.setToolTip(translate('Profile Editor',
                                       "<p>If a proper match isn't found for a file, the files "
                                       "will get sorted by filename, the retrieved tag sources "
                                       "by filename and corresponding (unmatched) tracks will "
                                       "matched.</p>"))

        self.matchFields = QLineEdit('artist, title')
        self.matchFields.setToolTip(translate('Profile Editor',
                                              "<p>The fields listed here will be used in determining "
                                              "whether a track matches the retrieved track. Each "
                                              "field will be compared using a fuzzy matching algorithm. "
                                              "If the resulting average match percentage is greater "
                                              'than the "Minimum Percentage" it\'ll be considered to '
                                              "match.</p>"))

        self.albumBound = QSpinBox()
        self.albumBound.setToolTip(translate('Profile Editor',
                                             "<p>The artist and album fields will be used in "
                                             "determining whether an album matches the retrieved one. "
                                             "Each field will be compared using a fuzzy matching "
                                             "algorithm. If the resulting average match percentage "
                                             "is greater or equal than what you specify here "
                                             "it'll be considered to match.</p>"))

        self.albumBound.setRange(0, 100)
        self.albumBound.setValue(70)

        self.trackBound = QSpinBox()
        self.trackBound.setRange(0, 100)
        self.trackBound.setValue(80)

        vbox = QVBoxLayout()
        vbox.addWidget(label)
        vbox.addWidget(self._text)

        vbox.addWidget(albumlabel)
        vbox.addWidget(self._albumdisp)

        vbox.addWidget(sortlabel)
        sortbox = QHBoxLayout()
        sortbox.addWidget(self._sortoptions, 1)
        sortbox.addWidget(editoptions)
        vbox.addLayout(sortbox)

        vbox.addWidget(ua_label)
        vbox.addWidget(self._ua)

        frame = QGroupBox(translate(
            'WebDB', 'Automatic retrieval options'))

        auto_box = QVBoxLayout()
        frame.setLayout(auto_box)

        auto_box.addLayout(create_buddy(translate('Profile Editor',
                                                  'Minimum &percentage required for album matches.'),
                                        self.albumBound))
        auto_box.addLayout(create_buddy(translate('Profile Editor',
                                                  'Match tracks using &fields: '), self.matchFields))
        auto_box.addLayout(create_buddy(translate('Profile Editor',
                                                  'Minimum percentage required for track match.'),
                                        self.trackBound))
        auto_box.addWidget(self.jfdi)

        vbox.addWidget(frame)

        vbox.addStretch()
        self.setLayout(vbox)
        self.loadSettings()

    def applySettings(self, control):
        listbox = control.listbox
        text = str(self._text.text())
        listbox.trackPattern = text

        albumdisp = str(self._albumdisp.text())
        listbox.albumPattern = albumdisp

        sort_combo = self._sortoptions
        sort_options_text = [str(sort_combo.itemText(i)) for i in
                             range(sort_combo.count())]
        sort_options = split_strip(sort_options_text)
        listbox.setSortOptions(sort_options)

        listbox.sort(sort_options[sort_combo.currentIndex()])

        useragent = str(self._ua.toPlainText())
        set_useragent(useragent)

        listbox.jfdi = self.jfdi.isChecked()
        listbox.matchFields = [z.strip() for z in
                               str(self.matchFields.text()).split(',')]
        listbox.albumBound = self.albumBound.value() / 100.0
        listbox.trackBound = self.trackBound.value() / 100.0

        cparser = PuddleConfig(os.path.join(CONFIGDIR, 'tagsources.conf'))
        set_value = lambda s, v: cparser.set('tagsources', s, v)
        set_value('trackpattern', text)
        set_value('albumpattern', albumdisp)
        set_value('sortoptions', sort_options_text)
        set_value('useragent', useragent)
        set_value('album_bound', self.albumBound.value())
        set_value('track_bound', self.trackBound.value())
        set_value('jfdi', listbox.jfdi)
        set_value('match_fields', listbox.matchFields)

    def loadSettings(self):
        cparser = PuddleConfig(os.path.join(CONFIGDIR, 'tagsources.conf'))

        trackpattern = cparser.get('tagsources', 'trackpattern',
                                   '%track% - %title%')

        self._text.setText(trackpattern)

        sortoptions = cparser.get('tagsources', 'sortoptions',
                                  ['artist, album', 'album, artist'])
        self._sortoptions.clear()
        self._sortoptions.addItems(sortoptions)

        albumformat = cparser.get('tagsources', 'albumpattern',
                                  '%artist% - %album% $if(%__numtracks%, [%__numtracks%], "")')
        self._albumdisp.setText(albumformat)

        self._ua.setText(cparser.get('tagsources',
                                     'useragent', 'puddletag/' + version_string))

        self.albumBound.setValue(
            cparser.get('tagsources', 'album_bound', 70, True))
        self.trackBound.setValue(
            cparser.get('tagsources', 'track_bound', 80, True))
        self.jfdi.setChecked(
            bool(cparser.get('tagsources', 'jfdi', True, True)))

        fields = cparser.get('tagsources', 'match_fields',
                             ['artist', 'title'])
        fields = ', '.join(z.strip() for z in fields)
        self.matchFields.setText(fields)

    def _editOptions(self):
        text = self._sortoptions.itemText
        win = SortOptionEditor([text(i) for i in
                                range(self._sortoptions.count())], self)
        win.options.connect(self._setSortOptions)
        win.setModal(True)
        win.show()

    def _setSortOptions(self, items):
        current = self._sortoptions.currentText()
        self._sortoptions.clear()
        self._sortoptions.addItems(items)
        index = self._sortoptions.findText(current)
        if index == -1:
            index = 0
        self._sortoptions.setCurrentIndex(index)


def load_source_prefs(name, preferences):
    cparser = PuddleConfig(TAGSOURCE_CONFIG)
    ret = []
    for option in preferences:
        if option[1] == COMBO:
            ret.append(cparser.get(name, option[0], option[2][1]))
        elif option[1] == SPINBOX:
            ret.append(cparser.get(name, option[0], option[2][2]))
        else:
            ret.append(cparser.get(name, option[0], option[2]))
    return ret


def tag_source_search(ts, group, files):
    """Helper method for tag source searches."""

    if not ts.group_by:
        return ts.search(files), files

    ret = []

    for primary in group:
        albums = ts.search(primary, group[primary])
        if albums:
            ret.extend(albums)
            continue

        audio = {'album': primary}
        changed, audio = apply_regexps(audio)
        if changed:
            audio['album'] = audio['album'].strip()
            write_log(translate("Tag Sources", "Retrying search with {}").format(audio['album']))
            ret.extend(ts.search(audio['album'], group[primary]))

    return ret, files


#############################################################################
# The Main Tag Sources Window
class MainWin(QWidget):
    writepreview = pyqtSignal(name='writepreview')
    setpreview = pyqtSignal(object, name='setpreview')
    clearpreview = pyqtSignal(name='clearpreview')
    enable_preview_mode = pyqtSignal(name='enable_preview_mode')
    logappend = pyqtSignal(str, name='logappend')
    disable_preview_mode = pyqtSignal(name='disable_preview_mode')

    def __init__(self, status, parent=None):
        QWidget.__init__(self, parent)
        self.settingsdialog = SettingsDialog

        connect = lambda obj, sig, slot: getattr(obj, sig).connect(slot)

        self.setWindowTitle("Tag Sources")

        self.receives = []
        self.emits = ['writepreview', 'setpreview', 'clearpreview',
                      'enable_preview_mode', 'logappend', 'disable_preview_mode']

        self.fieldMapping = audioinfo.mapping

        self._status = status
        self.__sources = [z() for z in tagsources]
        self.__sources.extend(load_mp3tag_sources())

        for ts in self.__sources:
            if hasattr(ts, 'preferences') and not isinstance(ts, QWidget):
                try:
                    ts.applyPrefs(load_source_prefs(ts.name, ts.preferences))
                except:
                    continue

        status['initialized_tagsources'] = self.__sources

        self.curSource = self.__sources[0]
        self.__sourceFields = [[] for z in self.__sources]

        ##################################################################
        # Add widgets to the window

        # The Tag Source selector and its label
        self.sourcelist = QComboBox()
        self.sourcelist.addItems([ts.name for ts in self.__sources])
        connect(self.sourcelist, 'currentIndexChanged', self.changeSource)

        sourcelabel = QLabel(translate("Tag Sources", 'Sour&ce: '))
        sourcelabel.setBuddy(self.sourcelist)

        # The Preferences/Configuration button (beside the Tag Source selector)
        preferences = QToolButton()
        preferences.setIcon(get_icon('configure'))
        preferences.setToolTip(translate("Tag Sources", 'Configure'))
        self.__preferencesButton = preferences
        connect(preferences, 'clicked', self.configure)

        # The Search text box
        self.searchEdit = QLineEdit()
        self.searchEdit.setToolTip(DEFAULT_SEARCH_TIP)
        connect(self.searchEdit, 'returnPressed', self.search)

        # The Search button
        self.searchButton = QPushButton(translate("Tag Sources", "&Search"))
        self.searchButton.setDefault(True)
        self.searchButton.setAutoDefault(True)
        connect(self.searchButton, "clicked", self.search)

        # The instruction/feedback label (between Search box and the results)
        self.label = QLabel(translate("Tag Sources",
                                      "Select files and click on Search to retrieve metadata."))

        # The results box (a tree view of album releases)
        self.listbox = ReleaseWidget(status, self.curSource)
        connect(self.listbox, 'statusChanged', self.label.setText)
        connect(self.listbox, 'preview', self.emit_preview)
        connect(self.listbox, 'exactMatches', self.emitExact)

        # An link to the web page sourced from
        linklabel = QLabel()
        linklabel.setOpenExternalLinks(True)
        connect(self.listbox, 'infoChanged', linklabel.setText)

        # The Submit button (optional, if the tag source support submission)
        self.submitButton = QPushButton(translate("Tag Sources",
                                                  "S&ubmit Tags"))
        connect(self.submitButton, "clicked", self.submit)

        # The Write button (beneath the Search results)
        write_preview = QPushButton(translate("Tag Sources", '&Write'))
        connect(write_preview, "clicked", self.writePreview)

        # The Clear preview button (beneath the Search results)
        clear = QPushButton(translate("Previews", "Clea&r preview"))
        connect(clear, "clicked", self.disable_preview_mode.emit)

        # The Fields row
        self.__fieldsEdit = FieldsEdit()
        self.__fieldsEdit.setToolTip(FIELDLIST_TIP)
        connect(self.__fieldsEdit, 'fieldsChanged', self.__changeFields)

        # The first fields options
        # If checked, only fields that aren’t already in the file will get updated.
        self.__updateEmpty = QCheckBox(translate("Tag Sources",
                                                 'Update empty fields only.'))

        # The Second fields option (Automatically retrieve matches)
        # If checked, the album that best matches the retrieved albums will be automatically retrieved.
        # Uses configured preferences to automate thinsg:
        #    https://docs.puddletag.net/source/preferences.html#auto-retrieval-prefs
        self.__autoRetrieve = QCheckBox(translate("Tag Sources",
                                                  'Automatically retrieve matches.'))

        # Layout Tag Source row
        sourcebox = QHBoxLayout()
        sourcebox.addWidget(sourcelabel)
        sourcebox.addWidget(self.sourcelist, 1)
        sourcebox.addWidget(preferences)

        # Layout the Search row
        searchbox = QHBoxLayout()
        searchbox.addWidget(self.searchButton, 0)
        searchbox.addWidget(self.searchEdit, 1)

        # Layout the Response row
        responsebox = QHBoxLayout()
        responsebox.addWidget(linklabel, 1)
        responsebox.addStretch()
        responsebox.addWidget(self.submitButton)
        responsebox.addWidget(write_preview)
        responsebox.addWidget(clear)

        # Stack the rows
        vbox = QVBoxLayout()
        vbox.addLayout(sourcebox)
        vbox.addLayout(searchbox)
        vbox.addWidget(self.label)
        vbox.addWidget(self.listbox, 1)
        vbox.addLayout(responsebox)
        vbox.addWidget(self.__fieldsEdit)
        vbox.addWidget(self.__updateEmpty)
        vbox.addWidget(self.__autoRetrieve)

        self.setLayout(vbox)

        connect(status_obj, 'statusChanged', self.label.setText)
        connect(status_obj, 'logappend', self.logappend)

        self.changeSource(0)

    def _applyPrefs(self, prefs):
        self.curSource.applyPrefs(prefs)
        cparser = PuddleConfig(TAGSOURCE_CONFIG)
        name = self.curSource.name
        for section, value in zip(self.curSource.preferences, prefs):
            cparser.set(name, section[0], value)

    def __changeFields(self, fields):
        self.listbox.tagsToWrite = fields
        self.__sourceFields[self.sourcelist.currentIndex()] = fields

    def changeSource(self, index):
        self.curSource = self.__sources[index]
        self.searchEdit.setToolTip(
            getattr(self.curSource, 'tooltip', DEFAULT_SEARCH_TIP))
        self.listbox.tagSource = self.curSource

        self.__preferencesButton.setVisible(
            not (getattr(self.curSource, 'preferences', False) is False))

        self.__fieldsEdit.setTags(self.__sourceFields[index])

        self.listbox.setMapping(self.fieldMapping.get(self.curSource.name, {}))

        self.searchEdit.setEnabled(hasattr(self.curSource, 'keyword_search'))

        self.submitButton.setVisible(hasattr(self.curSource, 'submit'))

    def configure(self):
        config = getattr(self.curSource, 'preferences', None)
        if config is None:
            return

        if isinstance(config, QWidget):
            win = config(parent=self)
        else:
            defaults = load_source_prefs(self.curSource.name, config)
            prefs = deepcopy(config)
            for pref, value in zip(prefs, defaults):
                if pref[1] == SPINBOX:
                    try:
                        pref[2][2] = int(value)
                    except ValueError:
                        pass
                elif pref[1] == COMBO:
                    pref[2][1] = value
                else:
                    pref[2] = value
            win = SimpleDialog(self.curSource.name, prefs, self)
        win.setModal(True)
        win.editingFinished.connect(self._applyPrefs)
        win.show()

    def emit_preview(self, tags):
        if not self.__updateEmpty.isChecked():
            self.enable_preview_mode.emit()
            self.setpreview.emit(tags)
        else:
            files = self._status['selectedfiles']
            previews = []
            for f, r in zip(files, tags):
                temp = {}
                for field in r:
                    if field not in f:
                        temp[field] = r[field]
                previews.append(temp)
            self.enable_preview_mode.emit()
            self.setpreview.emit(previews)

    def emitExact(self, d):
        if not self.__updateEmpty.isChecked():
            self.enable_preview_mode.emit()
            self.setpreview.emit(d)
        else:
            previews = []
            for f, r in d.items():
                temp = {}
                for field in r:
                    if field not in f:
                        temp[field] = r[field]
                previews.append(temp)
            self.enable_preview_mode.emit()
            self.setpreview.emit(previews)

    def loadSettings(self):
        settings = PuddleConfig(os.path.join(CONFIGDIR, 'tagsources.conf'))
        get = lambda s, k, i = False: settings.get('tagsources', s, k, i)

        source = get('lastsource', 'Musicbrainz')
        self.__sourceFields = [settings.get('tagsourcetags', ts.name, [])
                               for ts in self.__sources]

        index = self.sourcelist.findText(source)
        if index == -1:
            index = 0
        self.sourcelist.setCurrentIndex(index)
        self.__fieldsEdit.setTags(self.__sourceFields[index])
        df = get('trackpattern', '%track% - %title%')
        self.listbox.trackPattern = df

        albumformat = get('albumpattern',
                          '%artist% - %album%$if(%__numtracks%, [%__numtracks%], "")')
        self.listbox.albumPattern = albumformat

        sort_options = get('sortoptions',
                           ['artist, album', 'album, artist'])
        sort_options = split_strip(sort_options)
        self.listbox.setSortOptions(sort_options)

        sortindex = get('lastsort', 0)
        self.listbox.sort(sort_options[sortindex])

        filepath = os.path.join(CONFIGDIR, 'mappings')
        self.setMapping(audioinfo.loadmapping(filepath))

        useragent = get('useragent', '')
        if useragent:
            set_useragent(useragent)

        checkstate = get('existing', False)
        self.__updateEmpty.setChecked(checkstate)

        checkstate = get('autoretrieve', False)
        self.__autoRetrieve.setChecked(checkstate)

        self.listbox.albumBound = get('album_bound', 70, True) / 100.0
        self.listbox.trackBound = get('track_bound', 80, True) / 100.0
        self.listbox.jfdi = bool(get('jfdi', True, True))
        self.listbox.matchFields = get('match_fields', ['artist', 'title'])

    def setResults(self, retval):
        self.searchButton.setEnabled(True)
        if isinstance(retval, str):
            self.label.setText(retval)
        else:
            releases, files = retval
            if releases:
                self.label.setText(translate("Tag Sources",
                                             'Searching complete.'))
            else:
                self.label.setText(translate("Tag Sources",
                                             'No matching albums were found.'))
            if files and self.__autoRetrieve.isChecked():
                self.listbox.setReleases(releases, files)
            else:
                self.listbox.setReleases(releases)
            self.listbox.infoChanged.emit('')

    def search(self):
        if not self.searchButton.isEnabled():
            return
        files = self._status['selectedfiles']
        if self.curSource.group_by:
            group = split_by_field(files, *self.curSource.group_by)
        self.label.setText(translate("Tag Sources", 'Searching...'))
        text = None
        if self.searchEdit.text() and self.searchEdit.isEnabled():
            text = str(self.searchEdit.text())
        elif not files:
            self.label.setText(translate("Tag Sources",
                                         '<b>Select some files or enter search paramaters.</b>'))
            return

        def search():
            try:
                ret = []
                if text:
                    return self.curSource.keyword_search(text), None
                else:
                    return tag_source_search(self.curSource, group, files)
            except RetrievalError as e:
                return translate("Tag Sources",
                                 "An error occured: {}").format(str(e))
            except Exception as e:
                traceback.print_exc()
                return translate("Tag Sources",
                                 "An unhandled error occurred: {}").format(str(e))

        self.searchButton.setEnabled(False)
        t = PuddleThread(search, self)
        t.threadfinished.connect(self.setResults)
        t.start()

    def submit(self):
        files = self._status['selectedfiles']
        self.submitButton.setEnabled(False)

        def end(text):
            self.submitButton.setEnabled(True)
            self.label.setText(text)

        def submit():
            try:
                self.curSource.submit(files)
            except SubmissionError as e:
                traceback.print_exc()
                return translate("Tag Sources",
                                 "An error occured: {}").format(str(e))
            except Exception as e:
                traceback.print_exc()
                return translate("Tag Sources",
                                 "An unhandled error occurred: {}").format(str(e))

            return translate("Tag Sources", "Submission completed.")

        t = PuddleThread(submit, self)
        t.threadfinished.connect(end)
        t.start()

    def saveSettings(self):
        settings = PuddleConfig()
        settings.filename = os.path.join(CONFIGDIR, 'tagsources.conf')
        settings.set('tagsources', 'lastsource', self.sourcelist.currentText())
        for i, ts in enumerate(self.__sources):
            settings.set('tagsourcetags', ts.name, self.__sourceFields[i])
        settings.set('tagsources', 'lastsort', self.listbox.lastSortIndex)
        settings.set('tagsources', 'existing', self.__updateEmpty.isChecked())
        settings.set('tagsources', 'autoretrieve',
                     self.__autoRetrieve.isChecked())

    def setMapping(self, mapping):
        self.fieldMapping = mapping
        if self.curSource.name in mapping:
            self.listbox.setMapping(mapping[self.curSource.name])
        else:
            self.listbox.setMapping({})

    def writePreview(self):
        self.writepreview.emit()
        self.label.setText(translate("Tag Sources", "<b>Tags were written.</b>"))


control = ('Tag Sources', MainWin, RIGHTDOCK, False)

if __name__ == '__main__':
    app = QApplication(sys.argv)
    status = {}
    status['selectedfiles'] = exampletags.tags
    win = MainWin(status)
    win.show()
    app.exec_()
