#!/usr/bin/env python
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import (unicode_literals, division, absolute_import, print_function)

from collections import namedtuple
from PyQt5.Qt import (
    Qt, QAbstractItemView, QCheckBox, QComboBox, QDialogButtonBox, QEvent, QGroupBox, QHBoxLayout, QIcon,
    QLabel, QObject, QSpinBox, QTableWidget, QTableWidgetItem, QTextEdit, QToolButton, QVBoxLayout, QWidget)

from calibre.constants import (DEBUG, numeric_version)
from calibre.utils.config import JSONConfig

from calibre_plugins.overdrive_link import ActionOverdriveLink
from calibre_plugins.overdrive_link.formats import (
        ALL_EPUB_FORMATS, ALL_KINDLE_FORMATS, ALL_PDF_FORMATS, ALL_BLIO_FORMATS, ALL_OTHER_EBOOK_FORMATS,
        ALL_APP_ONLINE_EBOOK_FORMATS, ALL_MP3_FORMATS, ALL_WMA_FORMATS, ALL_OTHER_AUDIOBOOK_FORMATS)
from calibre_plugins.overdrive_link.book import author_sort_key
from calibre_plugins.overdrive_link.author_prep import author_search_prep
from calibre_plugins.overdrive_link.match import (primary_author, author_match_prep)
from calibre_plugins.overdrive_link.jobs import worker_limit
from calibre_plugins.overdrive_link.language import LANGUAGES
from calibre_plugins.overdrive_link.library import (LendingLibrary, SearchableLibrary, full_library_id)

from .python_transition import (IS_PYTHON2)
if IS_PYTHON2:
    from .python_transition import (repr, str)


__license__ = 'GPL v3'
__copyright__ = '2012-2025, John Howell <jhowell@acm.org>'


ConfigStoreLocation = 'plugins/' + ActionOverdriveLink.name

# required for calibre 6 (QT6), actually works with calibre 4 or later
QTextEdit_LineWrapMode_NoWrap = QTextEdit.LineWrapMode.NoWrap if numeric_version >= (5, 99, 0) else QTextEdit.NoWrap


# Plugin preference dict keys
Settings = 'Settings'   # all configuration settings
DiscoveredBooksDialogState = 'DiscoveredBooksDialogState'
ReleaseNoteVersion = 'ReleaseNoteVersion'
IncrementalSearchSequence = 'IncrementalSearchSequence'


# Individual Setting names
OverdriveLibraries = 'OverdriveLibraries'
AuthorNameVariants = 'AuthorNameVariants'
MarkChangedBooks = 'MarkChangedBooks'
DisableMetadataPlugin = 'DisableMetadataPlugin'
DisableConfirmDlgs = 'DisableConfirmDlgs'
DisableJobCompletionDlgs = 'DisableJobCompletionDlgs'
AllowSimultaneousJobs = 'AllowSimultaneousJobs'
SplitSearchAuthorCount = 'SplitSearchAuthorCount'
SearchLanguage = 'SearchLanguage'
CheckRecommendable = 'CheckRecommendable'
UpdateAmazonIdents = 'UpdateAmazonIdents'
DiscoverKeywords = 'DiscoverKeywords'
SearchEPUB = 'SearchEPUB'
SearchMOBI = 'SearchMOBI'
SearchPDF = 'SearchPDF'
SearchBlio = 'SearchBlio'
SearchOtherBook = 'SearchOtherBook'
SearchAppOnlineBook = 'SearchAppOnlineBook'
SearchMP3 = 'SearchMP3'
SearchWMA = 'SearchWMA'
SearchOtherAudio = 'SearchOtherAudio'
NumAuthorsToSearch = 'NumAuthorsToSearch'
ReportWarnings = 'ReportWarnings'
CacheDaysToKeep = 'CacheDaysToKeep'
ShowDetailsAtCompletion = 'ShowDetailsAtCompletion'
CheckAvailabilityOfNewLinks = 'CheckAvailabilityOfNewLinks'

PLUGIN_ICONS = ['images/lending_library_link.png', 'images/link.png', 'images/unlink.png']
HELP_FILE = 'help.htm'


LibraryConfig = namedtuple('LibraryConfig', 'library_id name enabled card_number card_pin branch_id provider_id')
LIBRARY_ID_INDEX = LibraryConfig._fields.index('library_id')
NAME_INDEX = LibraryConfig._fields.index('name')
ENABLED_INDEX = LibraryConfig._fields.index('enabled')
PROVIDER_ID_INDEX = LibraryConfig._fields.index('provider_id')

BLANK_LIBRARY = ('', '', False, '', '', '', '')     # default values for missing data


DEFAULT_SETTINGS = {
    # Lending libraries. Organized as per LibraryConfig in priority order from high to low.
    OverdriveLibraries: [
        # Defaults provided as examples to first-time users
        ('brooklyn.overdrive.com', 'BPL', False, '', '', '', ''),       # Brooklyn Public Library (NY)
        ('fairfax.overdrive.com', 'FCPL', False, '', '', '', ''),       # Fairfax County Public Library (VA)
        ('OCLS', 'OCLS-CloudLibrary', False, '', '', '', '3m'),         # Orange County Library System (FL) - cloudLibrary
        ('freelibrary', 'FLP-Freading', False, '', '', '', 'fr'),       # Free Library of Philadelphia (PA) - Freading
        ('', 'InternetArchive', False, '', '', '', 'ia'),               # Internet Archive
        ('unlimited', 'KindleUnlimited', False, '', '', '', 'ak'),      # Kindle Unlimited
        ],

    MarkChangedBooks: False,
    DisableMetadataPlugin: False,
    DisableConfirmDlgs: False,
    DisableJobCompletionDlgs: False,
    ReportWarnings: False,
    ShowDetailsAtCompletion: False,
    AllowSimultaneousJobs: False,
    SplitSearchAuthorCount: 0,
    SearchLanguage: '',    # Any
    CheckRecommendable: False,
    UpdateAmazonIdents: False,
    CheckAvailabilityOfNewLinks: False,
    DiscoverKeywords: [],
    AuthorNameVariants: [],
    NumAuthorsToSearch: 1,
    CacheDaysToKeep: 90,

    SearchEPUB: True,
    SearchMOBI: True,
    SearchPDF: True,
    SearchBlio: True,
    SearchOtherBook: True,
    SearchAppOnlineBook: True,

    SearchMP3: True,
    SearchWMA: True,
    SearchOtherAudio: True,
}

DEFAULT_RELEASE_NOTE_VERSION = (0, 0, 0)

# Obsolete Setting Names
DisableSearchConfirmDlg = 'DisableSearchConfirmDlg'     # Migrate to DisableConfirmDlgs
ShowChangedAfterUpdate = 'ShowChangedAfterUpdate'       # Migrate to MarkChangedBooks
MinSearchConfidence = 'MinSearchConfidence'
MinMatchConfidence = 'MinMatchConfidence'
SearchAudio = 'SearchAudio'                             # Migrate to SearchMP3, SearchWMA, SearchOtherAudio
CheckPurchasable = 'CheckPurchasable'

# old providers to be removed from configuration
obsolete_provider_ids = [
    'oy',                       # Oyster
    'ocd',                      # RBDigital
    ]

obsolete_library_ids = [
    ('prime', 'ak'),            # KOLL
    ]

disabled_provider_ids = [
    'ol',                       # Open Library
    ]

# Set location where all preferences for this plugin will be stored
plugin_config = JSONConfig(ConfigStoreLocation)


def set_defaults():
    # Set default values for settings as a whole
    plugin_config.defaults[Settings] = DEFAULT_SETTINGS
    plugin_config.defaults[ReleaseNoteVersion] = DEFAULT_RELEASE_NOTE_VERSION
    plugin_config.defaults[IncrementalSearchSequence] = 0

    # Handle migration for any settings that have changed name or allowed values
    migrate_setting_name(DisableSearchConfirmDlg, DisableConfirmDlgs)
    migrate_setting_name(ShowChangedAfterUpdate, MarkChangedBooks)
    migrate_setting_name(SearchAudio, SearchMP3, keep=True)
    migrate_setting_name(SearchAudio, SearchWMA, keep=True)
    migrate_setting_name(SearchAudio, SearchOtherAudio)
    migrate_setting_name(SearchOtherBook, SearchAppOnlineBook, keep=True)

    # Handle any newly added individual settings
    for setting, value in DEFAULT_SETTINGS.items():
        if setting not in plugin_config[Settings]:
            plugin_config[Settings][setting] = value

    # Add default values for missing library fields and remove extras
    new_library_config = []
    fixed = False
    for library_info in plugin_config[Settings][OverdriveLibraries]:
        if len(library_info) < len(BLANK_LIBRARY):
            library_info.extend(list(BLANK_LIBRARY)[len(library_info):])
            fixed = True

        if len(library_info) > len(BLANK_LIBRARY):
            del library_info[len(BLANK_LIBRARY):]
            fixed = True

        if library_info[PROVIDER_ID_INDEX] in disabled_provider_ids:
            # disable Open Library
            library_info[ENABLED_INDEX] = False
            fixed = True

        if (library_info[PROVIDER_ID_INDEX] in obsolete_provider_ids or
                (library_info[LIBRARY_ID_INDEX], library_info[PROVIDER_ID_INDEX]) in obsolete_library_ids):
            # discard this library
            fixed = True
        else:
            new_library_config.append(library_info)

    if fixed:
        plugin_config[Settings][OverdriveLibraries] = new_library_config

    return Configuration(plugin_config[Settings])   # Use new settings


def migrate_setting_name(old_name, new_name, keep=False):
    if old_name in plugin_config[Settings]:
        if new_name not in plugin_config[Settings]:
            plugin_config[Settings][new_name] = plugin_config[Settings][old_name]

        if not keep:
            del plugin_config[Settings][old_name]


def parse_author_variant(author):
    # returns author name, disabled (no search by this author), no discovery, excluded (any books with this author are rejected)

    if author.startswith('{') and author.endswith('}'):
        return author[1:-1], True, True, True

    if author.startswith('[') and author.endswith(']'):
        return author[1:-1], True, True, False

    if author.startswith('<') and author.endswith('>'):
        return author[1:-1], False, True, False

    return author, False, False, False


def author_variants_sort_key(authors):
    return author_sort_key(parse_author_variant(primary_author(authors))[0])


class Configuration:
    '''
    The current configuration parameters as a python object
    '''

    def __init__(self, prefs):
        self.plugin_version = ActionOverdriveLink.version
        self.configuration_error = False

        self.mark_changed_books = prefs[MarkChangedBooks]
        self.disable_metadata_plugin = prefs[DisableMetadataPlugin]
        self.disable_confirm_dlgs = prefs[DisableConfirmDlgs]
        self.disable_job_completion_dlgs = prefs[DisableJobCompletionDlgs]
        self.report_warnings = prefs[ReportWarnings]
        self.split_search_author_count = prefs[SplitSearchAuthorCount]
        self.allow_simultaneous_jobs = prefs[AllowSimultaneousJobs]
        self.search_language = prefs[SearchLanguage]
        self.check_recommendable = prefs[CheckRecommendable]
        self.update_amazon_idents = prefs[UpdateAmazonIdents]
        self.check_availability_of_new_links = prefs[CheckAvailabilityOfNewLinks]
        self.discover_keywords = prefs[DiscoverKeywords]
        self.num_authors_to_search = prefs[NumAuthorsToSearch]
        self.cache_days_to_keep = prefs[CacheDaysToKeep]
        self.show_details_at_completion = prefs[ShowDetailsAtCompletion]
        self.search_formats = set()

        if prefs[SearchEPUB]:
            self.search_formats |= ALL_EPUB_FORMATS
        if prefs[SearchMOBI]:
            self.search_formats |= ALL_KINDLE_FORMATS
        if prefs[SearchPDF]:
            self.search_formats |= ALL_PDF_FORMATS
        if prefs[SearchBlio]:
            self.search_formats |= ALL_BLIO_FORMATS
        if prefs[SearchOtherBook]:
            self.search_formats |= ALL_OTHER_EBOOK_FORMATS
        if prefs[SearchAppOnlineBook]:
            self.search_formats |= ALL_APP_ONLINE_EBOOK_FORMATS

        if prefs[SearchMP3]:
            self.search_formats |= ALL_MP3_FORMATS
        if prefs[SearchWMA]:
            self.search_formats |= ALL_WMA_FORMATS
        if prefs[SearchOtherAudio]:
            self.search_formats |= ALL_OTHER_AUDIOBOOK_FORMATS

        self.have_project_gutenberg = False
        self.libraries = []

        self.library_names = set()
        self.enabled_library_names = set()

        self.libraries_by_full_id = {}

        lib_configs = []
        for library_info in prefs[OverdriveLibraries]:
            lib_configs.append(LibraryConfig._make(library_info))   # Convert library info into object via namedtuple

        lib_configs, libs, errors = check_libraries(lib_configs, migrate=True)  # convert "good" libs to internal format

        if errors:
            self.configuration_error = True
            if DEBUG:
                print('\n'.join(errors))

        for lib in libs:
            self.libraries.append(lib)
            self.library_names.add(lib.name)
            self.libraries_by_full_id[lib.full_library_id] = lib

            if lib.enabled:
                self.enabled_library_names.add(lib.name)

            if lib.provider.is_project_gutenberg:
                self.have_project_gutenberg = True

        self.author_name_variants = prefs[AuthorNameVariants]
        self.author_search_equivalents = {}
        self.author_match_equivalents = {}
        self.excluded_authors = set()
        self.no_discovery_authors = set()

        for equivalents in self.author_name_variants:
            for i, name in enumerate(equivalents):
                name, disabled, no_discovery, excluded = parse_author_variant(name)
                match_name = author_match_prep(name)
                search_name = author_search_prep(name)

                if i == 0:
                    # primary name for group
                    match_primary = match_name
                    if match_primary not in self.author_match_equivalents:
                        self.author_match_equivalents[match_primary] = set()

                    search_primary = search_name
                    if search_primary not in self.author_search_equivalents:
                        self.author_search_equivalents[search_primary] = set()

                self.author_match_equivalents[match_primary].add(match_name)

                if not disabled:
                    self.author_search_equivalents[search_primary].add(search_name)

                if excluded:
                    self.excluded_authors.add(match_name)

                if no_discovery:
                    self.no_discovery_authors.add(match_name)

    def library(self, provider_id, library_id):
        return self.libraries_by_full_id.get(full_library_id(provider_id, library_id), None)

    def library_name(self, provider_id, library_id):
        '''
        Derive a short human readable library name from a library_id using configured information about libraries
        '''
        lib = self.library(provider_id, library_id)

        if lib:
            return lib.name

        # This library is unknown. Attempt to generate a reasonable name on-the-fly.
        return make_library_name(provider_id, library_id)

    def priority(self, provider_id, library_id):
        '''
        Relative priority of a library host (low number = higher priority)
        '''
        lib = self.library(provider_id, library_id)

        if lib:
            return lib.priority

        # This library_id is unknown. Use lower priority than any configured host.
        return len(self.libraries)

    def configured(self, provider_id, library_id):
        return self.library(provider_id, library_id) is not None

    def enabled(self, provider_id, library_id):
        lib = self.library(provider_id, library_id)
        if lib:
            return lib.enabled

        return False

    def disabled(self, provider_id, library_id):
        lib = self.library(provider_id, library_id)
        if lib:
            return not lib.enabled

        return False

    def any_enabled(self):
        '''
        Is there at least one enabled library configured?
        '''
        for lib in self.libraries:
            if lib.enabled:
                return True

        return False

    def provider_ids_enabled(self):
        provider_ids = set()
        for lib in self.libraries:
            if lib.enabled:
                provider_ids.add(lib.provider_id)

        return provider_ids


def make_library_name(provider_id, library_id):
    lib_name = library_id.replace('.lib.overdrive.com', '').replace('.overdrive.com', '').replace('.lib.us', '')
    lib_name = lib_name.replace('.org', '').replace('overdrive.', '').replace('downloads.', '')

    try:
        provider_name = SearchableLibrary.provider(provider_id).name
    except Exception:
        provider_name = provider_id

    return '.'.join([n for n in [lib_name, provider_name] if n]).replace(' ', '')


def check_libraries(libraries, migrate=False):
    # provider id must be known
    # Library ids and names must be non-empty
    # library_id + branch must be unique
    # name must be unique

    lib_configs = []
    lending_libs = []
    errors = []

    branch_names = set()
    library_names = set()

    for row, li in enumerate(libraries):
        try:
            # Check for invalid ids and repeated or missing names (config=None required to prevent change to OverDrive libs)
            SearchableLibrary.validate_provider_id(li.provider_id)

            li = li._replace(library_id=SearchableLibrary.provider(li.provider_id).validate_library_id(
                    li.library_id, migrate=migrate, config=None))

            li = li._replace(branch_id=SearchableLibrary.provider(li.provider_id).validate_branch_id(li.branch_id))

            if not li.name:
                new_name = make_library_name(li.provider_id, li.library_id)     # Generate a reasonable name if none given
                if new_name in library_names:
                    raise ValueError('Library name missing')    # can't generate missing name automatically

                li = li._replace(name=new_name)

            if li.name in library_names:
                raise ValueError('Library name "%s" not unique' % li.name)

            library_names.add(li.name)

            branch_name = '%s/%s/%s' % (li.provider_id, li.library_id, li.branch_id)

            if branch_name in branch_names:
                raise ValueError('Provider/Library/Branch ids "%s" not unique' % branch_name)

            branch_names.add(branch_name)

            lending_lib = LendingLibrary(priority=row, **li._asdict())  # pass namedtuple as kwargs, may raise exception
            lending_libs.append(lending_lib)    # "good" library config

        except Exception as e:
            errors.append('Library %d configuration error: %s' % (row + 1, repr(e)))

        lib_configs.append(li)   # save (possible updated) whether good or has errors

    return (lib_configs, lending_libs, errors)


class MaintainLibrariesTableWidget(QTableWidget):
    ENABLED_COL = 0
    NAME_COL = 1
    PROVIDER_ID_COL = 2
    LIBRARY_ID_COL = 3
    BRANCH_ID_COL = 4
    CARD_NUMBER_COL = 5
    CARD_PIN_COL = 6

    COLUMN_HEADER_LABELS = ['Enabled', 'Name', 'Provider', 'Library ID', 'Branch ID*', 'Card Number*', 'Card PIN*']
    COLUMN_WIDTHS = [90, 150, 150, 250, 80, 120, 90]

    def __init__(self, parent, gui, change_handler):
        QTableWidget.__init__(self, parent)
        self.gui = gui
        self.change_handler = change_handler

    def populate_table(self, lending_libraries):
        self.clear()
        self.setAlternatingRowColors(True)
        self.setColumnCount(len(MaintainLibrariesTableWidget.COLUMN_HEADER_LABELS))
        self.setHorizontalHeaderLabels(MaintainLibrariesTableWidget.COLUMN_HEADER_LABELS)
        self.verticalHeader().setDefaultSectionSize(24)
        self.horizontalHeader().setStretchLastSection(True)

        self.setRowCount(len(lending_libraries))

        for row, library_info in enumerate(lending_libraries):
            self.populate_table_row(row, library_info)

        for col, width in enumerate(MaintainLibrariesTableWidget.COLUMN_WIDTHS):
            self.setColumnWidth(col, width)

        self.setSortingEnabled(False)
        self.setMinimumSize(sum(MaintainLibrariesTableWidget.COLUMN_WIDTHS)+50, 24*6+1)
        self.setSelectionBehavior(QAbstractItemView.SelectRows)
        if lending_libraries:
            self.selectRow(0)

    def populate_table_row(self, row, library_info):
        li = LibraryConfig._make(library_info)

        item = QTableWidgetItem('')
        item.setToolTip('Enable searching at this lending library?')
        item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled)
        item.setCheckState(Qt.Checked if li.enabled else Qt.Unchecked)
        self.setItem(row, MaintainLibrariesTableWidget.ENABLED_COL, item)

        item = QTableWidgetItem(li.name)
        item.setToolTip('Short, readable name for display. eg: FLP')
        self.setItem(row, MaintainLibrariesTableWidget.NAME_COL, item)

        widget = ProviderComboBox(self, li.provider_id)
        widget.setToolTip('Provider of ebook lending library services: OverDrive, cloudLibrary, Freading, etc.')
        widget.currentIndexChanged.connect(self.change_handler)
        self.setCellWidget(row, MaintainLibrariesTableWidget.PROVIDER_ID_COL, widget)

        library_id = li.library_id
        try:
            # attempt fix up of values from previous plugin versions
            library_id = SearchableLibrary.provider(li.provider_id).validate_library_id(library_id, config=None)
        except Exception:
            pass    # allows handling of corrupted or obsolete library configuration

        branch_id = li.branch_id
        try:
            # attempt fix up of values from previous plugin versions
            branch_id = SearchableLibrary.provider(li.provider_id).validate_branch_id(branch_id)
        except Exception:
            pass    # allows handling of corrupted or obsolete library configuration

        item = QTableWidgetItem(library_id)
        item.setToolTip('Identifier for a lending library. Format depends on provider selected.')
        self.setItem(row, MaintainLibrariesTableWidget.LIBRARY_ID_COL, item)

        item = QTableWidgetItem(branch_id)
        item.setToolTip('Branch ID number for OverDrive sign in/Authentication for EBSCOhost/Collection name for Internet Archive (optional)')
        self.setItem(row, MaintainLibrariesTableWidget.BRANCH_ID_COL, item)

        item = QTableWidgetItem(li.card_number)
        item.setToolTip('Library card number/user name for library sign in (optional)')
        self.setItem(row, MaintainLibrariesTableWidget.CARD_NUMBER_COL, item)

        item = QTableWidgetItem(li.card_pin)
        item.setToolTip('Library card PIN/password for library sign in (optional)')
        self.setItem(row, MaintainLibrariesTableWidget.CARD_PIN_COL, item)

    def add_row(self):
        self.setFocus()
        # We will insert a blank row at the end
        row = self.rowCount()
        self.insertRow(row)
        self.populate_table_row(row, self.create_blank_row_data())
        self.select_and_scroll_to_row(row)

    def delete_rows(self):
        self.setFocus()
        rows = self.selectionModel().selectedRows()

        if not rows:
            return

        first_sel_row = self.currentRow()

        for selrow in reversed(rows):
            self.removeRow(selrow.row())

        if first_sel_row < self.rowCount():
            self.select_and_scroll_to_row(first_sel_row)
        elif self.rowCount() > 0:
            self.select_and_scroll_to_row(first_sel_row - 1)

        self.change_handler(None)   # force update of error messages

    def raise_row(self):
        self.setFocus()
        if len(self.selectionModel().selectedRows()) != 1 or self.currentRow() == 0:
            return

        # Swap data with row above and move selection up
        current_row = self.currentRow()
        previous_row = current_row - 1
        lending_libraries = self.get_data()
        self.populate_table_row(previous_row, lending_libraries[current_row])
        self.populate_table_row(current_row, lending_libraries[previous_row])
        self.select_and_scroll_to_row(previous_row)

    def lower_row(self):
        self.setFocus()
        if len(self.selectionModel().selectedRows()) != 1 or self.currentRow() == self.rowCount()-1:
            return

        # Swap data with row below and move selection down
        current_row = self.currentRow()
        next_row = current_row + 1
        lending_libraries = self.get_data()
        self.populate_table_row(next_row, lending_libraries[current_row])
        self.populate_table_row(current_row, lending_libraries[next_row])
        self.select_and_scroll_to_row(next_row)

    def select_and_scroll_to_row(self, row):
        self.selectRow(row)
        self.setCurrentItem(self.item(row, 0))
        self.scrollToItem(self.currentItem())

    def create_blank_row_data(self):
        return LibraryConfig._make(BLANK_LIBRARY)

    def get_data(self):
        # get data without any error correction

        libraries = []

        for row in range(self.rowCount()):
            for col in range(len(MaintainLibrariesTableWidget.COLUMN_WIDTHS)):
                if (self.item(row, col) is None) and (self.cellWidget(row, col) is None):
                    #print('not ready (%d, %d)' % (row, col))
                    library = LibraryConfig._make(BLANK_LIBRARY)
                    break
            else:
                provider_id = str(self.cellWidget(row, MaintainLibrariesTableWidget.PROVIDER_ID_COL).selected_value())
                enabled = self.item(row, MaintainLibrariesTableWidget.ENABLED_COL).checkState() == Qt.Checked

                #if provider_id in disabled_provider_ids:
                #enabled = False

                library = LibraryConfig(
                    enabled=enabled,
                    name=str(self.item(row, MaintainLibrariesTableWidget.NAME_COL).text()).strip(),
                    provider_id=provider_id,
                    library_id=str(self.item(row, MaintainLibrariesTableWidget.LIBRARY_ID_COL).text()).strip(),
                    branch_id=str(self.item(row, MaintainLibrariesTableWidget.BRANCH_ID_COL).text()).strip(),
                    card_number=str(self.item(row, MaintainLibrariesTableWidget.CARD_NUMBER_COL).text()).strip(),
                    card_pin=str(self.item(row, MaintainLibrariesTableWidget.CARD_PIN_COL).text()).strip())

            libraries.append(library)
        return libraries


class LanguageComboBox(QComboBox):

    def __init__(self, parent, value):
        QComboBox.__init__(self, parent)
        self.values = sorted(LANGUAGES)
        self.populate_combo(value)

        self.filter = WheelFilter()
        self.installEventFilter(self.filter)

    def populate_combo(self, selected_value):
        self.clear()
        selected_idx = idx = 0

        for value in self.values:
            self.addItem(value)
            if value == selected_value:
                selected_idx = idx

            idx = idx + 1

        self.setCurrentIndex(selected_idx)  # defaults for first

    def selected_value(self):
        for value in self.values:
            if value == str(self.currentText()).strip():
                return value

        return ''   # unexpected value


class ProviderComboBox(QComboBox):

    def __init__(self, parent, selected_provider_id):
        QComboBox.__init__(self, parent)

        self.clear()

        self.ids = []
        for name in sorted(SearchableLibrary.provider_names()):
            self.addItem(name)
            self.ids.append(SearchableLibrary.provider_by_name(name).id)

        for idx, id in enumerate(self.ids):
            if id == selected_provider_id:
                self.setCurrentIndex(idx)
                break
        else:
            # add unknown id as a choice
            self.addItem("Unknown-%s" % selected_provider_id)
            self.setCurrentIndex(len(self.ids))
            self.ids.append(selected_provider_id)

        self.setFocusPolicy(Qt.StrongFocus)
        self.filter = WheelFilter()
        self.installEventFilter(self.filter)

    def selected_value(self):
        return self.ids[self.currentIndex()]


class ConfigWidget(QWidget):

    def __init__(self, plugin_action):
        QWidget.__init__(self)
        self.plugin_action = plugin_action

        self.settings = plugin_config[Settings]

        layout = QVBoxLayout(self)

        heading_label = QLabel('<b>Lending libraries in priority order:</b>', self)
        layout.addWidget(heading_label)

        layout.addLayout(self.table_layout(), stretch=10)

        note_label = QLabel('<i>* - Optional fields, allow sign-in to libraries for more accurate search results (See Help)</i>', self)
        layout.addWidget(note_label)

        self.library_error_label = QLabel('')  # text changed dynamically
        self.library_error_label.setStyleSheet("QLabel { color : red; font-weight: bold; }")
        layout.addWidget(self.library_error_label)

        layout.addSpacing(20)

        layout.addLayout(self.options_layout())

        layout.addLayout(self.bottom_layout())

        self.setLayout(layout)
        self.lending_libraries_table.populate_table(check_libraries(self.settings[OverdriveLibraries], migrate=True)[0])

    def table_layout(self):
        # Horizontal layout containing the table of lending libraries and the buttons next to it
        layout = QHBoxLayout()

        # Create a table the user can edit the data values in
        self.lending_libraries_table = MaintainLibrariesTableWidget(
            self, self.plugin_action.gui, self.lending_libraries_table_currentIndexChanged)
        self.lending_libraries_table.itemChanged.connect(self.lending_libraries_table_itemChanged)
        self.lending_libraries_table.currentItemChanged.connect(self.lending_libraries_table_currentItemChanged)

        layout.addWidget(self.lending_libraries_table)

        layout.addLayout(self.table_button_layout())
        return layout

    def table_button_layout(self):
        # Vertical layout containing the the buttons to add/remove libraries.
        layout = QVBoxLayout()

        add_button = QToolButton(self)
        add_button.setToolTip('Add row')
        add_button.setIcon(QIcon(I('plus.png')))
        add_button.clicked.connect(self.lending_libraries_table.add_row)
        layout.addWidget(add_button)

        delete_button = QToolButton(self)
        delete_button.setToolTip('Delete row')
        delete_button.setIcon(QIcon(I('minus.png')))
        delete_button.clicked.connect(self.lending_libraries_table.delete_rows)
        layout.addWidget(delete_button)

        raise_button = QToolButton(self)
        raise_button.setToolTip('Move selected row up')
        raise_button.setIcon(QIcon(I('arrow-up.png')))
        raise_button.clicked.connect(self.lending_libraries_table.raise_row)
        layout.addWidget(raise_button)

        lower_button = QToolButton(self)
        lower_button.setToolTip('Move selected row down')
        lower_button.setIcon(QIcon(I('arrow-down.png')))
        lower_button.clicked.connect(self.lending_libraries_table.lower_row)
        layout.addWidget(lower_button)

        layout.addStretch()
        return layout

    def options_layout(self):
        layout = QVBoxLayout()

        hor1_layout = QHBoxLayout()
        hor1_layout.addWidget(self.general_options_group_box())
        hor1_layout.addWidget(self.search_options_group_box())
        hor1_layout.addWidget(self.ebook_formats_group_box())
        hor1_layout.addWidget(self.audiobook_formats_group_box())
        layout.addLayout(hor1_layout)

        hor2_layout = QHBoxLayout()
        hor2_layout.addWidget(self.keywords_group_box())
        hor2_layout.addWidget(self.authors_group_box())
        layout.addLayout(hor2_layout)

        return layout

    def general_options_group_box(self):
        group_box = QGroupBox('General Options:', self)
        layout = QVBoxLayout()
        group_box.setLayout(layout)

        self.ShowDetailsAtCompletion = QCheckBox('Show result summary in popup dialogs')
        self.ShowDetailsAtCompletion.setToolTip('Show a summary of search results in the completion popup by default')
        self.ShowDetailsAtCompletion.setChecked(self.settings[ShowDetailsAtCompletion])
        layout.addWidget(self.ShowDetailsAtCompletion)

        self.ReportWarnings = QCheckBox('Report warnings from search')
        self.ReportWarnings.setToolTip('Include diagnostic warning messages in search results')
        self.ReportWarnings.setChecked(self.settings[ReportWarnings])
        layout.addWidget(self.ReportWarnings)

        self.DisableConfirmDlgs = QCheckBox('Disable confirmation dialogs')
        self.DisableConfirmDlgs.setToolTip('Perform actions immediatly without asking for confirmation')
        self.DisableConfirmDlgs.setChecked(self.settings[DisableConfirmDlgs])
        layout.addWidget(self.DisableConfirmDlgs)

        self.DisableJobCompletionDlgs = QCheckBox('Disable job completion popup dialogs')
        self.DisableJobCompletionDlgs.setToolTip('Accept changes without displaying job completion popup dialogs')
        self.DisableJobCompletionDlgs.setChecked(self.settings[DisableJobCompletionDlgs])
        layout.addWidget(self.DisableJobCompletionDlgs)

        self.DisableMetadataPlugin = QCheckBox('Disable links in book details panel')
        self.DisableMetadataPlugin.setToolTip('Disable clickable links in book details.'
                                              '\n(Restart is required for change to take effect.)')
        self.DisableMetadataPlugin.setChecked(self.settings[DisableMetadataPlugin])
        layout.addWidget(self.DisableMetadataPlugin)

        self.MarkChangedBooks = QCheckBox('Mark changed books')
        self.MarkChangedBooks.setToolTip('Mark changed or added books whenever the calibre library is modified.')
        self.MarkChangedBooks.setChecked(self.settings[MarkChangedBooks])
        layout.addWidget(self.MarkChangedBooks)

        self.UpdateAmazonIdents = QCheckBox("Set 'amazon' identifier for Kindle e-books")
        self.UpdateAmazonIdents.setToolTip('When a link is created to a kindle e-book at Amazon then also update the amazon identifier')
        self.UpdateAmazonIdents.setChecked(self.settings[UpdateAmazonIdents])
        layout.addWidget(self.UpdateAmazonIdents)

        layout.addStretch()
        return group_box

    def search_options_group_box(self):
        group_box = QGroupBox('Search Options:', self)
        layout = QVBoxLayout()
        group_box.setLayout(layout)

        split_count_layout = QHBoxLayout()
        split_count_layout.addWidget(QLabel('Maximum authors per search job:'))
        self.SplitSearchAuthorCount = QSpinBox()
        self.SplitSearchAuthorCount.setToolTip('Automatically split jobs having more authors. Zero for no limit.')
        self.SplitSearchAuthorCount.setRange(0, 1000)
        self.SplitSearchAuthorCount.setSingleStep(10)
        self.SplitSearchAuthorCount.setValue(self.settings[SplitSearchAuthorCount])
        split_count_layout.addWidget(self.SplitSearchAuthorCount)
        split_count_layout.addStretch()
        layout.addLayout(split_count_layout)

        self.AllowSimultaneousJobs = QCheckBox('Allow %d search jobs to run simultaneously' % worker_limit())
        self.AllowSimultaneousJobs.setToolTip(
            'Speeds up searching, but can affect lending library servers.'
            '\nActual number set by Preferences, Miscellaneous, Max. simultaneous...')
        self.AllowSimultaneousJobs.setChecked(self.settings[AllowSimultaneousJobs])
        layout.addWidget(self.AllowSimultaneousJobs)

        num_authors_to_search_layout = QHBoxLayout()
        num_authors_to_search_layout.addWidget(QLabel('Max authors to search per book:'))
        self.NumAuthorsToSearch = QSpinBox()
        self.NumAuthorsToSearch.setToolTip('Limits the number of authors to search for books with multiple authors.')
        self.NumAuthorsToSearch.setRange(1, 100)
        self.NumAuthorsToSearch.setSingleStep(1)
        self.NumAuthorsToSearch.setValue(self.settings[NumAuthorsToSearch])
        num_authors_to_search_layout.addWidget(self.NumAuthorsToSearch)
        num_authors_to_search_layout.addStretch()
        layout.addLayout(num_authors_to_search_layout)

        language_layout = QHBoxLayout()
        language_layout.addWidget(QLabel('Language:'))
        self.SearchLanguage = LanguageComboBox(self, self.settings[SearchLanguage])
        self.SearchLanguage.setToolTip('Optional language to limit searches. Leave blank for any.')
        self.SearchLanguage.setMaximumWidth(150)
        language_layout.addWidget(self.SearchLanguage, stretch=4)
        language_layout.addStretch(1)
        layout.addLayout(language_layout)

        self.CheckRecommendable = QCheckBox('Check for books that can be recommended for acquisition')
        self.CheckRecommendable.setToolTip(
            'During search perform an additional check for books recommendable, but not currently available. '
            'Applies only to Cloud Library.')
        self.CheckRecommendable.setChecked(self.settings[CheckRecommendable])
        layout.addWidget(self.CheckRecommendable)

        cache_days_to_keep_layout = QHBoxLayout()
        cache_days_to_keep_layout.addWidget(QLabel('Days to cache library book info:'))
        self.CacheDaysToKeep = QSpinBox()
        self.CacheDaysToKeep.setToolTip('Limits the time library books info will be cached (0 for no caching).')
        self.CacheDaysToKeep.setRange(0, 10000)
        self.CacheDaysToKeep.setSingleStep(30)
        self.CacheDaysToKeep.setValue(self.settings[CacheDaysToKeep])
        cache_days_to_keep_layout.addWidget(self.CacheDaysToKeep)
        cache_days_to_keep_layout.addStretch()
        layout.addLayout(cache_days_to_keep_layout)

        self.CheckAvailabilityOfNewLinks = QCheckBox("Update current availability of books with new links (slow)")
        self.CheckAvailabilityOfNewLinks.setToolTip('Update the current wait time to borrow when a link is created to an available book')
        self.CheckAvailabilityOfNewLinks.setChecked(self.settings[CheckAvailabilityOfNewLinks])
        layout.addWidget(self.CheckAvailabilityOfNewLinks)

        layout.addStretch()
        return group_box

    def ebook_formats_group_box(self):
        group_box = QGroupBox('eBook formats:', self)
        layout = QVBoxLayout()
        group_box.setLayout(layout)

        self.SearchBlio = QCheckBox('Blio')
        layout.addWidget(self.SearchBlio)
        self.SearchBlio.setChecked(self.settings[SearchBlio])

        self.SearchEPUB = QCheckBox('EPUB')
        layout.addWidget(self.SearchEPUB)
        self.SearchEPUB.setChecked(self.settings[SearchEPUB])

        self.SearchMOBI = QCheckBox('Mobi/Kindle')
        layout.addWidget(self.SearchMOBI)
        self.SearchMOBI.setChecked(self.settings[SearchMOBI])

        self.SearchPDF = QCheckBox('PDF')
        layout.addWidget(self.SearchPDF)
        self.SearchPDF.setChecked(self.settings[SearchPDF])

        self.SearchOtherBook = QCheckBox('Other ebook')
        layout.addWidget(self.SearchOtherBook)
        self.SearchOtherBook.setChecked(self.settings[SearchOtherBook])

        self.SearchAppOnlineBook = QCheckBox('App/Online viewer')
        layout.addWidget(self.SearchAppOnlineBook)
        self.SearchAppOnlineBook.setChecked(self.settings[SearchAppOnlineBook])

        layout.addStretch()
        return group_box

    def audiobook_formats_group_box(self):
        group_box = QGroupBox('Audiobook formats:', self)
        layout = QVBoxLayout()
        group_box.setLayout(layout)

        self.SearchMP3 = QCheckBox('MP3')
        layout.addWidget(self.SearchMP3)
        self.SearchMP3.setChecked(self.settings[SearchMP3])

        self.SearchWMA = QCheckBox('WMA')
        layout.addWidget(self.SearchWMA)
        self.SearchWMA.setChecked(self.settings[SearchWMA])

        self.SearchOtherAudio = QCheckBox('Other/app/player')   # M4B/Acoustik
        layout.addWidget(self.SearchOtherAudio)
        self.SearchOtherAudio.setChecked(self.settings[SearchOtherAudio])

        layout.addStretch()
        return group_box

    def keywords_group_box(self):
        group_box = QGroupBox('Keywords for book discovery:', self)
        layout = QVBoxLayout()
        group_box.setLayout(layout)
        self.DiscoverKeywords = QTextEdit()
        self.DiscoverKeywords.setToolTip('Keywords for discovery search, one set per line.')
        self.DiscoverKeywords.setLineWrapMode(QTextEdit_LineWrapMode_NoWrap)
        #self.DiscoverKeywords.setMaximumWidth(300)
        self.DiscoverKeywords.setFixedHeight(100)
        self.DiscoverKeywords.setText('\n'.join(self.settings[DiscoverKeywords]))
        layout.addWidget(self.DiscoverKeywords)
        layout.addStretch()
        return group_box

    def authors_group_box(self):
        group_box = QGroupBox('Author name variants:', self)
        layout = QVBoxLayout()
        group_box.setLayout(layout)
        self.AuthorNameVariants = QTextEdit()
        self.AuthorNameVariants.setToolTip('One author per line with name variants separated by "&".')
        self.AuthorNameVariants.setLineWrapMode(QTextEdit_LineWrapMode_NoWrap)
        self.AuthorNameVariants.setFixedHeight(100)
        self.AuthorNameVariants.setText('\n'.join([
                ' & '.join(a) for a in sorted(self.settings[AuthorNameVariants], key=author_variants_sort_key)]))
        layout.addWidget(self.AuthorNameVariants)
        layout.addStretch()
        return group_box

    def bottom_layout(self):
        '''
        This should really be part of the dialog box that includes this config widget,
        but the calibre plugin architecture doesn't allow for that.
        '''
        layout = QHBoxLayout()

        layout.addStretch()

        button_box = QDialogButtonBox(QDialogButtonBox.Help)
        button_box.setToolTip('Display help for this plugin')
        button_box.helpRequested.connect(self.show_help)
        layout.addWidget(button_box)
        return layout

    def show_help(self):
        self.plugin_action.show_help()

    def lending_libraries_table_itemChanged(self, item):
        self.update_library_error_label()

    def lending_libraries_table_currentItemChanged(self, old_item, new_item):
        self.update_library_error_label()

    def lending_libraries_table_currentIndexChanged(self, index):
        self.update_library_error_label()

    def update_library_error_label(self):
        errors = check_libraries(self.lending_libraries_table.get_data(), migrate=False)[2]
        self.library_error_label.setText(errors[0] if errors else '')   # show first error

    def save_settings(self):
        '''
        Called by calibre when the configuration dialog has been accepted
        '''
        new_prefs = {}
        new_prefs[OverdriveLibraries] = [list(li) for li in check_libraries(self.lending_libraries_table.get_data(), migrate=False)[0]]
        new_prefs[MarkChangedBooks] = self.MarkChangedBooks.isChecked()
        new_prefs[DisableMetadataPlugin] = self.DisableMetadataPlugin.isChecked()
        new_prefs[DisableConfirmDlgs] = self.DisableConfirmDlgs.isChecked()
        new_prefs[DisableJobCompletionDlgs] = self.DisableJobCompletionDlgs.isChecked()
        new_prefs[ReportWarnings] = self.ReportWarnings.isChecked()
        new_prefs[ShowDetailsAtCompletion] = self.ShowDetailsAtCompletion.isChecked()
        new_prefs[SplitSearchAuthorCount] = self.SplitSearchAuthorCount.value()
        new_prefs[AllowSimultaneousJobs] = self.AllowSimultaneousJobs.isChecked()
        new_prefs[NumAuthorsToSearch] = self.NumAuthorsToSearch.value()
        new_prefs[CacheDaysToKeep] = self.CacheDaysToKeep.value()
        new_prefs[SearchLanguage] = self.SearchLanguage.selected_value()
        new_prefs[CheckRecommendable] = self.CheckRecommendable.isChecked()
        new_prefs[UpdateAmazonIdents] = self.UpdateAmazonIdents.isChecked()
        new_prefs[CheckAvailabilityOfNewLinks] = self.CheckAvailabilityOfNewLinks.isChecked()
        new_prefs[DiscoverKeywords] = [k.strip() for k in str(self.DiscoverKeywords.toPlainText()).split('\n') if k.strip()]

        new_prefs[AuthorNameVariants] = [
                [a.strip() for a in ka.split('&') if a.strip()]
                for ka in str(self.AuthorNameVariants.toPlainText()).split('\n') if ka.strip()]

        new_prefs[SearchEPUB] = self.SearchEPUB.isChecked()
        new_prefs[SearchMOBI] = self.SearchMOBI.isChecked()
        new_prefs[SearchPDF] = self.SearchPDF.isChecked()
        new_prefs[SearchBlio] = self.SearchBlio.isChecked()
        new_prefs[SearchOtherBook] = self.SearchOtherBook.isChecked()
        new_prefs[SearchAppOnlineBook] = self.SearchAppOnlineBook.isChecked()
        new_prefs[SearchMP3] = self.SearchMP3.isChecked()
        new_prefs[SearchWMA] = self.SearchWMA.isChecked()
        new_prefs[SearchOtherAudio] = self.SearchOtherAudio.isChecked()

        self.plugin_action.config = Configuration(new_prefs)   # Use new settings
        plugin_config[Settings] = new_prefs         # Save permanently


class WheelFilter(QObject):

    def eventFilter(self, obj, event):
        '''
        Event filter to keep wheelEvents away from combo boxes.
        This prevents accidental value changes when scrolling by wheel.
        '''
        if event.type() == QEvent.Wheel and isinstance(obj, QComboBox):
            event.ignore()
            return True

        return False


def config_set_library_group(lib_names):
    prefs = plugin_config[Settings]
    library_config = prefs[OverdriveLibraries]

    for library_info in library_config:
        library_info[ENABLED_INDEX] = (library_info[NAME_INDEX] in lib_names)

    prefs[OverdriveLibraries] = library_config
    plugin_config[Settings] = prefs     # save

    return Configuration(prefs)         # Use new settings
