# Copyright 2010 Steven Robertson
#           2012 Christoph Reiter
#           2017 Nick Boultbee
#           2018 Olli Helin
#
# 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 gi.repository import Gtk, Gdk

from quodlibet import _
from quodlibet import app
from quodlibet import config
from quodlibet.qltk import Button, Icons
from quodlibet.plugins.events import EventPlugin
from quodlibet.util import print_e, print_w

import ast

# Presets (roughly) taken from Pulseaudio equalizer
PRESET_BANDS = [50, 100, 156, 220, 311, 440, 622, 880, 1250, 1750, 2500,
                3500, 5000, 10000, 20000]
PRESETS = {
    "flat": (_("Flat"), [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]),
    "live": (_("Live"), [-9.0, -5.5, 0.0, 1.5, 2.0, 3.5, 3.5, 3.5, 3.5,
                         3.5, 3.5, 3.5, 3.0, 1.5, 2.0]),
    "full_bass_treble": (_("Full Bass & Treble"),
                         [5.0, 5.0, 3.5, 2.5, 0.0, -7.0, -14.0, -10.0, -10.0,
                          -8.0, 1.0, 1.0, 5.0, 7.5, 9.5]),
    "club": (_("Club"), [0.0, 0.0, 0.0, 0.0, 3.5, 3.5, 3.5, 3.5, 3.5,
                         3.5, 3.5, 2.5, 2.5, 0.0, 0.0]),
    "large_hall": (_("Large Hall"), [7.0, 7.0, 7.0, 3.5, 3.0, 3.0, 3.0, 1.5,
                                     0.0, -2.0, -3.5, -6.0, -9.0, -1.0, 0.0]),
    "party": (_("Party"), [5.0, 5.0, 5.0, 3.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
                           0.0, 0.0, 0.0, 2.5, 5.0]),
    "rock": (_("Rock"), [5.5, 2.5, 2.5, -8.5, -10.5, -11.0, -16.0, -14.5,
                         -6.5, -5.5, -3.0, 3.0, 6.5, 7.0, 7.0]),
    "soft": (_("Soft"), [3.0, 3.0, 1.0, 1.0, 0.0, -2.5, -5.0, 1.5, 0.0, 1.0,
                         3.0, 3.0, 6.0, 8.0, 8.0]),
    "full_bass": (_("Full Bass"),
                  [-16.0, -16.0, 6.5, 6.5, 6.0, 5.5, 4.5, 1.0, 1.0, 1.0, -8.0,
                   -10.0, -16.0, -16.0, -20.5]),
    "classical": (_("Classical"),
                  [0, 0, 0, 0, 0, 0, 0, 0, 0,
                   0, 0, 0, -21.0, -21.0, -27.0]),
    "reggae": (_("Reggae"), [0.0, 0.0, 0.0, 0.0, 0.0, -4.5, -10.0, -6.0, 0.5,
                             1.0, 2.0, 4.0, 4.0, 0.0, 0.0]),
    "headphones": (_("Headphones"),
                   [3.0, 3.0, 7.0, 7.0, 3.0, -1.0, -6.5, -6.0, -4.5, -4.0,
                    1.0, 1.0, 6.0, 8.0, 9.0]),
    "soft_rock": (_("Soft Rock"), [3.0, 3.0, 3.0, 1.5, 1.5, 1.5, 0.0, -3.5,
                                   -8.0, -7.0, -10, -9.0, -6.5, 1.5, 6.0]),
    "full_treble": (_("Full Treble"),
                    [5.0, -18.5, -18.5, -18.5, -18.5, -10.0, -8.0, -6.5, 1.5,
                     1.5, 1.5, 8.5, 10.5, 10.5, 10.5]),
    "dance": (_("Dance"), [6.0, 4.0, 4.0, 1.5, 1.5, 1.5, 0.0, 0.0, 0.0,
                           1.0, -10.5, -14.0, -15.0, -7.0, 0.0]),
    "pop": (_("Pop"), [-3.5, 1.0, 2.0, 3.0, 5.0, 5.5, 6.5, 5.0, 3.0, 1.5, 0.0,
                       -2.5, -5.0, -5.0, -3.0]),
    "techno": (_("Techno"), [5.0, 4.0, 4.0, 3.0, 0.0, -4.5, -10.0, -9.0, -8.0,
                             -5.5, -1.5, 3.0, 6.0, 6.0, 6.0]),
    "ska": (_("Ska"), [-4.5, -8.0, -9.0, -8.5, -8.0, -6.0, 0.0, 1.5, 2.5, 2.5,
                       3.0, 3.0, 6.0, 6.0, 6.0]),
    "laptop": (_("Laptop"), [-1, -1, -1, -1, -5, -10, -18, -15, -10, -5, -5,
                             -5, -5, 0, 0]),
}


def interp_bands(src_band, target_band, src_gain):
    """Linear interp from one band to another. All must be sorted."""
    gain = []
    for i, b in enumerate(target_band):
        if b in src_band:
            gain.append(src_gain[i])
            continue
        idx = sorted(src_band + [b]).index(b)
        idx = min(max(idx, 1), len(src_band) - 1)
        x1, x2 = src_band[idx - 1:idx + 1]
        y1, y2 = src_gain[idx - 1:idx + 1]
        g = y1 + ((y2 - y1) * (b - x1)) / float(x2 - x1)
        gain.append(min(12.0, g))
    return gain


def get_config():
    try:
        config_str = config.get("plugins", "equalizer_levels", "[]")
        config_dict = ast.literal_eval(config_str)

        if isinstance(config_dict, list):
            print_w("Converting old EQ config to new format.")
            config_dict = {"Current": config_dict}
        if not isinstance(config_dict, dict):
            raise ValueError("Saved config is of wrong type.")
        if not "Current" in config_dict.keys():
            raise ValueError("Saved config was malformed.")

        # Run through the values to check everything is of correct type.
        for key in config_dict.keys():
            [float(s) for s in config_dict[key]]

        return config_dict
    except (config.Error, ValueError) as e:
        print_e(str(e))
        return {"Current": []}


class Equalizer(EventPlugin):
    PLUGIN_ID = "Equalizer"
    PLUGIN_NAME = _("Equalizer")
    PLUGIN_DESC = _("Controls the tone of your music with an equalizer.\n"
                    "Click or use keys to customise levels "
                    "(right-click resets the band).")
    PLUGIN_ICON = Icons.AUDIO_CARD

    @property
    def player_has_eq(self):
        return hasattr(app.player, 'eq_bands') and app.player.eq_bands

    def __init__(self):
        super().__init__()
        self._enabled = False
        self._config = {}

    def apply(self):
        if not self.player_has_eq:
            return
        levels = self._enabled and get_config()["Current"] or []
        lbands = len(app.player.eq_bands)
        if len(levels) != lbands:
            print_w("Number of bands didn't match current. Using flat EQ.")
            levels = [0.] * lbands
        app.player.eq_values = levels

    def enabled(self):
        self._enabled = True
        self.apply()

    def disabled(self):
        self._enabled = False
        self.apply()

    def PluginPreferences(self, win):
        main_vbox = Gtk.VBox(spacing=12)
        if not self.player_has_eq:
            l = Gtk.Label()
            l.set_markup(
                _('The current backend does not support equalization.'))
            main_vbox.pack_start(l, False, True, 0)
            return main_vbox

        def format_hertz(band):
            if band >= 1000:
                return _('%.1f kHz') % (band / 1000.)
            return _('%d Hz') % band

        bands = [format_hertz(band) for band in app.player.eq_bands]
        self._config = get_config()
        levels = self._config["Current"]

        # This fixes possible old corrupt config files with extra level values.
        if len(levels) != len(bands):
            print_w("Number of bands didn't match current. Using flat EQ.")
            levels = [0.] * len(bands)

        table = Gtk.Table(rows=len(bands), columns=3)
        table.set_col_spacings(6)

        def set_band(adj, idx):
            rounded = int(adj.get_value() * 2) / 2.0
            adj.set_value(rounded)
            levels[idx] = rounded

            self._config["Current"] = levels
            config.set('plugins', 'equalizer_levels', str(self._config))
            self.apply()

        adjustments = []

        for i, band in enumerate(bands):
            # align numbers and suffixes in separate rows for great justice
            lbl = Gtk.Label(label=band.split()[0])
            lbl.set_alignment(1, 0.5)
            lbl.set_padding(0, 4)
            table.attach(lbl, 0, 1, i, i + 1, xoptions=Gtk.AttachOptions.FILL)
            lbl = Gtk.Label(label=band.split()[1])
            lbl.set_alignment(1, 0.5)
            table.attach(lbl, 1, 2, i, i + 1, xoptions=Gtk.AttachOptions.FILL)
            adj = Gtk.Adjustment.new(levels[i], -24., 12., 0.5, 3, 0)
            adj.connect('value-changed', set_band, i)
            adjustments.append(adj)
            hs = Gtk.HScale(adjustment=adj)
            hs.connect('button-press-event', self.__rightclick)
            hs.set_draw_value(True)
            hs.set_value_pos(Gtk.PositionType.RIGHT)
            hs.connect('format-value', lambda s, v: _('%.1f dB') % v)
            table.attach(hs, 2, 3, i, i + 1)
        main_vbox.pack_start(table, True, True, 0)

        # Reset EQ button
        def clicked_rb(button):
            [adj.set_value(0) for adj in adjustments]
            self._combo_default.set_active(0)
            self._combo_custom.set_active(0)

        # Delete custom preset button
        def clicked_db(button):
            selected_index = self._combo_custom.get_active()
            if selected_index < 1:
                return # Select…
            selected = self._combo_custom.get_active_text()
            self._combo_custom.set_active(0)
            self._combo_custom.remove(selected_index)
            del self._config[selected]
            config.set('plugins', 'equalizer_levels', str(self._config))

        # Save custom preset button
        def clicked_sb(button):
            name = self._preset_name_entry.get_text()
            is_new = not name in self._config.keys()

            levels = [adj.get_value() for adj in adjustments]
            self._config[name] = levels
            config.set('plugins', 'equalizer_levels', str(self._config))

            self._preset_name_entry.set_text("")
            if is_new:
                self._combo_custom.append_text(name)

            def find_iter(list_store, text):
                i = list_store.get_iter_first()
                while (i is not None):
                    if list_store.get_value(i, 0) == text:
                        return i
                    i = list_store.iter_next(i)
                return None

            itr = find_iter(self._combo_custom.get_model(), name)
            self._combo_custom.set_active_iter(itr)

        sorted_presets = sorted(PRESETS.items())

        def default_combo_changed(combo):
            if combo.get_active() < 1:
                return # Select…
            self._combo_custom.set_active(0)
            gain = sorted_presets[combo.get_active() - 1][1][1]
            gain = interp_bands(PRESET_BANDS, app.player.eq_bands, gain)
            for (g, a) in zip(gain, adjustments):
                a.set_value(g)

        def custom_combo_changed(combo):
            if combo.get_active() < 1:
                # Case: Select…
                self._delete_button.set_sensitive(False)
                return
            self._combo_default.set_active(0)
            self._delete_button.set_sensitive(True)
            gain = self._config[combo.get_active_text()]
            for (g, a) in zip(gain, adjustments):
                a.set_value(g)

        def save_name_changed(entry):
            name = entry.get_text()
            if not name or name == "Current" or name.isspace():
                self._save_button.set_sensitive(False)
            else:
                self._save_button.set_sensitive(True)

        frame = Gtk.Frame(label=_("Default presets"), label_xalign=0.5)
        main_middle_hbox = Gtk.HBox(spacing=6)

        # Default presets
        combo = Gtk.ComboBoxText()
        self._combo_default = combo
        combo.append_text(_("Select…"))
        combo.set_active(0)
        for key, (name, gain) in sorted_presets:
            combo.append_text(name)
        combo.connect("changed", default_combo_changed)

        # This block is just for padding.
        padboxv = Gtk.VBox()
        padboxv.pack_start(combo, True, True, 6)
        padboxh = Gtk.HBox()
        padboxh.pack_start(padboxv, True, True, 6)
        frame.add(padboxh)

        main_middle_hbox.pack_start(frame, True, True, 0)

        reset = Button(_("_Reset EQ"), Icons.EDIT_UNDO)
        reset.connect('clicked', clicked_rb)
        main_middle_hbox.pack_start(reset, False, False, 0)

        main_vbox.pack_start(main_middle_hbox, False, False, 0)

        frame = Gtk.Frame(label=_("Custom presets"), label_xalign=0.5)
        main_bottom_vbox = Gtk.VBox()

        # Custom presets
        combo = Gtk.ComboBoxText()
        self._combo_custom = combo
        combo.append_text(_("Select…"))
        combo.set_active(0)

        custom_presets = self._config.keys() - {"Current"}
        for key in custom_presets:
            combo.append_text(key)
        combo.connect("changed", custom_combo_changed)
        hb = Gtk.HBox(spacing=6)
        hb.pack_start(combo, True, True, 0)

        delete = Button(_("_Delete selected"), Icons.EDIT_DELETE)
        delete.connect('clicked', clicked_db)
        delete.set_sensitive(False)
        self._delete_button = delete
        hb.pack_start(delete, False, False, 0)

        main_bottom_vbox.pack_start(hb, True, True, 6)
        hs = Gtk.HSeparator()
        main_bottom_vbox.pack_start(hs, True, True, 6)

        hb = Gtk.HBox()
        l = Gtk.Label(label=_("Preset name for saving:"))
        hb.pack_start(l, False, False, 0)
        main_bottom_vbox.pack_start(hb, False, False, 0)

        e = Gtk.Entry()
        e.connect("changed", save_name_changed)
        self._preset_name_entry = e
        hb = Gtk.HBox(spacing=6)
        hb.pack_start(e, True, True, 0)

        save = Button(_("_Save"), Icons.DOCUMENT_SAVE)
        save.connect('clicked', clicked_sb)
        save.set_sensitive(False)
        self._save_button = save
        hb.pack_start(save, False, False, 0)

        main_bottom_vbox.pack_start(hb, True, True, 6)

        # This block is just for padding.
        padboxh = Gtk.HBox()
        padboxh.pack_start(main_bottom_vbox, True, True, 6)
        frame.add(padboxh)

        main_vbox.pack_start(frame, True, True, 0)
        return main_vbox

    def __rightclick(self, hs, event):
        if event.button == Gdk.BUTTON_SECONDARY:
            hs.set_value(0)
