# This file is part of beets.
# Copyright 2016, Heinz Wiesinger.
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.

"""Synchronize information from music player libraries
"""


from abc import abstractmethod, ABCMeta
from importlib import import_module

from confuse import ConfigValueError
from beets import ui
from beets.plugins import BeetsPlugin


METASYNC_MODULE = 'beetsplug.metasync'

# Dictionary to map the MODULE and the CLASS NAME of meta sources
SOURCES = {
    'amarok': 'Amarok',
    'itunes': 'Itunes',
}


class MetaSource(metaclass=ABCMeta):
    def __init__(self, config, log):
        self.item_types = {}
        self.config = config
        self._log = log

    @abstractmethod
    def sync_from_source(self, item):
        pass


def load_meta_sources():
    """ Returns a dictionary of all the MetaSources
    E.g., {'itunes': Itunes} with isinstance(Itunes, MetaSource) true
    """
    meta_sources = {}

    for module_path, class_name in SOURCES.items():
        module = import_module(METASYNC_MODULE + '.' + module_path)
        meta_sources[class_name.lower()] = getattr(module, class_name)

    return meta_sources


META_SOURCES = load_meta_sources()


def load_item_types():
    """ Returns a dictionary containing the item_types of all the MetaSources
    """
    item_types = {}
    for meta_source in META_SOURCES.values():
        item_types.update(meta_source.item_types)
    return item_types


class MetaSyncPlugin(BeetsPlugin):

    item_types = load_item_types()

    def __init__(self):
        super().__init__()

    def commands(self):
        cmd = ui.Subcommand('metasync',
                            help='update metadata from music player libraries')
        cmd.parser.add_option('-p', '--pretend', action='store_true',
                              help='show all changes but do nothing')
        cmd.parser.add_option('-s', '--source', default=[],
                              action='append', dest='sources',
                              help='comma-separated list of sources to sync')
        cmd.parser.add_format_option()
        cmd.func = self.func
        return [cmd]

    def func(self, lib, opts, args):
        """Command handler for the metasync function.
        """
        pretend = opts.pretend
        query = ui.decargs(args)

        sources = []
        for source in opts.sources:
            sources.extend(source.split(','))

        sources = sources or self.config['source'].as_str_seq()

        meta_source_instances = {}
        items = lib.items(query)

        # Avoid needlessly instantiating meta sources (can be expensive)
        if not items:
            self._log.info('No items found matching query')
            return

        # Instantiate the meta sources
        for player in sources:
            try:
                cls = META_SOURCES[player]
            except KeyError:
                self._log.error('Unknown metadata source \'{}\''.format(
                    player))

            try:
                meta_source_instances[player] = cls(self.config, self._log)
            except (ImportError, ConfigValueError) as e:
                self._log.error('Failed to instantiate metadata source '
                                '\'{}\': {}'.format(player, e))

        # Avoid needlessly iterating over items
        if not meta_source_instances:
            self._log.error('No valid metadata sources found')
            return

        # Sync the items with all of the meta sources
        for item in items:
            for meta_source in meta_source_instances.values():
                meta_source.sync_from_source(item)

            changed = ui.show_model_changes(item)

            if changed and not pretend:
                item.store()
