# Copyright 2019-2020 Joschua Gandert
#                2022 Nick Boultbee
#
# 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.
from enum import IntEnum

from gi.repository import Gtk

from quodlibet import _, app, config, qltk, const
from quodlibet.qltk import Icons, ErrorMessage, Message
from quodlibet.plugins import PluginConfig, BoolConfProp, IntConfProp
from quodlibet.plugins.events import EventPlugin
from quodlibet.plugins.songshelpers import is_writable
from quodlibet.util import print_e
from quodlibet.util.songwrapper import SongWrapper


class UpdateStrategy(IntEnum):
    AFTER_PLAY_NOT_SKIP = 0
    AFTER_PLAY_OR_SKIP = 1
    ONCE_ALBUM_RATED = 2


class StrategyText:
    def __init__(self, name, description):
        self.name = name
        self.description = description


STRATEGY_TEXTS = {  #
    UpdateStrategy.AFTER_PLAY_NOT_SKIP: StrategyText(
        _("After every play (default)"),
        _(
            "Whenever a song was played but not skipped, "
            "the plugin will write the tags to the file. "
            "Skip counts aren't stored in files at all, "
            "so this avoids unnecessary writes."
        ),
    ),
    UpdateStrategy.AFTER_PLAY_OR_SKIP: StrategyText(
        _("After every play or skip"),
        _(
            "Whenever a song was played or skipped, "
            "the plugin will write the tags to the file. "
            "Can be useful if you want to make sure that ratings of songs "
            "you dislike and thus skipped are written to the files."
        ),
    ),
    UpdateStrategy.ONCE_ALBUM_RATED: StrategyText(
        _("Once, when album fully rated"),
        _(
            "When a song was played or skipped, the album of that song will "
            "be checked. If every song in the album has been rated and at "
            "least one has no ratings or play counts stored in its file, "
            "the plugin will write the tags to the songs' files.\n\n"
            "Use this to avoid constant file updates, "
            "but be aware that once an album was updated, "
            "you'll have to use the 'Update Tags in Files' plugin "
            "whenever you want modified ratings and play counts "
            "to be written to the files."
        ),
    ),
}

PLAY_COUNT_ABOVE_ZERO_TOOLTIP = _(
    "When the plugin writes the tags of an album, "
    "it will first set the play count of the songs which are zero to one.\n"
    "Sometimes you already know that you don't like a song, "
    "so setting it to one when saving can be useful later on, "
    "when searching for albums you have fully listened to (%s)."
)

WRITE_ERROR_FMT = _("Couldn't write '%s'")


class Config:
    _config = PluginConfig("autoupdatetagsinfiles")

    update_strategy = IntConfProp(
        _config, "update_strategy", UpdateStrategy.AFTER_PLAY_NOT_SKIP.value
    )
    ensure_play_counts_above_zero = BoolConfProp(  # useful for searching
        _config, "ensure_play_counts_above_zero", False
    )


CONFIG = Config()


class AutoUpdateTagsInFiles(EventPlugin):
    PLUGIN_ID = "AutoUpdateTagsInFiles"
    PLUGIN_NAME = _("Auto Update Tags in Files")
    PLUGIN_DESC = _(
        "When songs were played, update the tags in their files. "
        "This will ensure play counts and ratings are up to date."
    )
    PLUGIN_ICON = Icons.DOCUMENT_SAVE

    def PluginPreferences(self, _):
        return AutoUpdateTagsPrefs()

    def enabled(self):
        if not config.getboolean("editing", "save_to_songs"):
            config.set("editing", "save_to_songs", True)

            warning_text = _(
                "The following setting was enabled as it's "
                "required for this plugin to work:\n\n%s"
            )
            setting_name = _("Save ratings and play _counts in tags")

            Message(
                Gtk.MessageType.INFO,
                app.window,
                _("Settings updated"),
                warning_text % setting_name.replace("_", ""),
            ).run()

    def plugin_on_song_ended(self, song, skipped):
        if song is None or not is_writable(song):
            return

        strategy = CONFIG.update_strategy
        if strategy == UpdateStrategy.AFTER_PLAY_NOT_SKIP:
            if not skipped:
                self._try_to_update_song(song)
            return
        if strategy == UpdateStrategy.AFTER_PLAY_OR_SKIP:
            self._try_to_update_song(song)
            return

        self._update_album_if_fully_rated(song.album_key)

    def _try_to_update_song(self, song_wrapper):
        try:
            song_wrapper._needs_write = True
        except Exception as e:
            print_e(e)
            self._error_msg(WRITE_ERROR_FMT % song_wrapper._song)

    def _error_msg(self, message):
        title = _("Error in %s") % self.PLUGIN_NAME
        ErrorMessage(app.window, title, message).run()

    def _update_album_if_fully_rated(self, album_key):
        album = app.library.albums.get(album_key, None)
        if album is None:
            return

        songs = album.songs
        # first check for ratings to avoid costly file checks
        if not songs or not all(song.has_rating for song in songs):
            return

        songs = [s for s in songs if s.supports_rating_and_play_count_in_file]
        if not songs:
            return

        email = config.get("editing", "save_email", const.EMAIL).strip()
        if all(s.has_rating_and_playcount_in_file(email) for s in songs):
            return

        # at least one song has no ratings or play counts stored in files
        try:
            self._update_album(songs)
        except Exception as e:
            print_e(e)
            self._error_msg(WRITE_ERROR_FMT % album_key)

    def _update_album(self, songs):
        song_wrappers = []

        req_play_counts_above_zero = CONFIG.ensure_play_counts_above_zero
        for song in songs:
            if req_play_counts_above_zero and not song.get("~#playcount", 0):
                song["~#playcount"] = 1

            wrapper = SongWrapper(song)
            wrapper._needs_write = True
            song_wrappers.append(wrapper)


class AutoUpdateTagsPrefs(Gtk.Box):
    def __init__(self):
        super().__init__(orientation=Gtk.Orientation.VERTICAL)

        strategy_boxes = []
        for strategy in UpdateStrategy:
            desc = STRATEGY_TEXTS[strategy].description
            desc_label = Gtk.Label(label=desc, wrap=True)

            box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
            box.pack_start(desc_label, False, False, 0)

            strategy_boxes.append(box)

        def ensure_play_count_toggled(button, *args):
            CONFIG.ensure_play_counts_above_zero = button.get_active()

        ensure_play_count_checkbutton = Gtk.CheckButton(
            label=_("Ensure play counts are above zero when saving"),
            valign=Gtk.Align.START,
        )
        ensure_play_count_checkbutton.set_tooltip_text(
            PLAY_COUNT_ABOVE_ZERO_TOOLTIP % "#(playcount:min = 1)"
        )
        ensure_play_count_checkbutton.set_active(CONFIG.ensure_play_counts_above_zero)
        ensure_play_count_checkbutton.connect("toggled", ensure_play_count_toggled)

        album_box = strategy_boxes[UpdateStrategy.ONCE_ALBUM_RATED]
        album_box.pack_start(ensure_play_count_checkbutton, False, False, 0)

        def show_only_current_box():
            current = CONFIG.update_strategy
            for n, box in enumerate(strategy_boxes):
                box.set_visible(n == current)

        grid = Gtk.Grid(column_spacing=6, row_spacing=6)

        def grid_add(x, y, child):
            grid.attach(child, x, y, 1, 1)

        def change_strategy(button):
            new_strategy = button.get_active()
            CONFIG.update_strategy = new_strategy
            show_only_current_box()

        update_combobox = Gtk.ComboBoxText()
        for strategy in UpdateStrategy:
            update_combobox.append_text(STRATEGY_TEXTS[strategy].name)

        update_combobox.set_active(CONFIG.update_strategy)
        update_combobox.connect("changed", change_strategy)

        update_lbl = ConfigLabel(_("_Update strategy:"), update_combobox)

        grid_add(0, 0, update_lbl)
        grid_add(1, 0, update_combobox)
        for box in strategy_boxes:
            # show_all will be called on the plugin preference interface, so
            # without the following that would result in overlapping text
            box.show_all()
            box.props.no_show_all = True

            grid_add(1, 1, box)

        show_only_current_box()

        frame = qltk.Frame(label=_("Preferences"), child=grid)
        self.pack_start(frame, False, False, 0)


class ConfigLabel(Gtk.Label):
    """Customised Label for configuration, tied to a widget"""

    def __init__(self, text, widget):
        super().__init__(label=text, use_underline=True)
        self.set_mnemonic_widget(widget)
        self.set_alignment(0.0, 0.5)
