#   miniplayers.py: a wall of jingles and segment players -- part of IDJC.
#   Copyright 2023 Stephen Fairchild (s-fairchild@users.sourceforge.net)
#
#   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.
#
#   This program is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.
#
#   You should have received a copy of the GNU General Public License
#   along with this program in the file entitled COPYING.
#   If not, see <http://www.gnu.org/licenses/>.

import os
import time
import gettext
import json
import uuid
import itertools
import urllib
import random
from enum import Enum
from functools import wraps

import gi
from gi.repository import Gtk
from gi.repository import Gdk
from gi.repository import GObject
from gi.repository import GdkPixbuf
from gi.repository import Pango

from idjc import *
from .prelims import *
from .gtkstuff import timeout_add, source_remove
from .tooltips import set_tip
from .utils import LinkUUIDRegistry
from .playergui import get_media_metadata, supported

_ = gettext.translation(FGlobs.package_name, FGlobs.localedir,
                                                        fallback=True).gettext

PM = ProfileManager()
link_uuid_reg = LinkUUIDRegistry()


class PostAction(Enum):
    ONCE = "set_once"
    REPEAT = "set_repeat"
    SEQUENCE = "set_sequence"
    PLAY_1 = "set_play_1"
    PLAY_2 = "set_play_2"
    PLAY_3 = "set_play_3"


def indices_decode(f):
    @wraps(f)
    def wrapped(self, string):
        return f(self, *divmod(int(string), 100))
    return wrapped

def indices_encode(page, player):
    return str(page*100 + player)

def setter(mode):
    def wrap(f):
        @wraps(f)
        def wrapped(self):
            if self._mode != mode:
                self._mode = mode
                self._stack.set_visible_child_name(mode.value)
                self.notify("mode")
            f(self)
        return wrapped
    return wrap


class PostActionButton(Gtk.Button):
    @GObject.Property
    def mode(self):
        return self._mode

    def __init__(self):
        Gtk.Button.__init__(self)
        self._mode = None
        self.new_mode = PostAction.ONCE.value

        self._stack = Gtk.Stack.new()
        self.add(self._stack)
        self._stack.add_named(Gtk.Label.new("\u2794"), PostAction.ONCE.value)
        self._stack.add_named(Gtk.Image.new_from_icon_name(
                              "media-playlist-repeat-song-symbolic",
                              Gtk.IconSize.BUTTON), PostAction.REPEAT.value)
        self._stack.add_named(Gtk.Label.new("\u2BA7"), PostAction.SEQUENCE.value)
        self._stack.add_named(Gtk.Label.new("1"), PostAction.PLAY_1.value)
        self._stack.add_named(Gtk.Label.new("2"), PostAction.PLAY_2.value)
        self._stack.add_named(Gtk.Label.new("3"), PostAction.PLAY_3.value)
        self.set_once()
        self.connect("clicked", self.set_next)

    def set_next(self, *_):
        iter_ = itertools.chain((*PostAction, *PostAction))
        try:
            while next(iter_) is not self._mode:
                pass
        except StopIteration as e:
            print(f"{type(e)}: {e} in PostActionButton.set_next: mode not found"
                  f" for {self._mode}")
        else:
            getattr(self, next(iter_).value)()

    @setter(PostAction.ONCE)
    def set_once(self):
        pass

    @setter(PostAction.REPEAT)
    def set_repeat(self):
        pass

    @setter(PostAction.SEQUENCE)
    def set_sequence(self):
        pass

    @setter(PostAction.PLAY_1)
    def set_play_1(self):
        pass

    @setter(PostAction.PLAY_2)
    def set_play_2(self):
        pass

    @setter(PostAction.PLAY_3)
    def set_play_3(self):
        pass

    def get_once(self):
        return self.mode is PostAction.ONCE

    def get_repeat(self):
        return self.mode is PostAction.REPEAT

    def get_sequence(self):
        return self.mode is PostAction.SEQUENCE

    def get_play_x(self):
        return self.mode in (PostAction.PLAY_1,
                             PostAction.PLAY_2,
                             PostAction.PLAY_3)

    def restore_session_pass_2(self):
        getattr(self, self.new_mode)()


class PlayerLink:
    """Players can be linked for consecutive play."""

    def __init__(self):
        self.a = self.b = None
        self._linked = False

    def register_a(self, player):
        assert(self.a is None)
        self.a = player

    def register_b(self, player):
        assert(self.b is None)
        self.b = player

    @property
    def configured_a(self):
        return self.a.is_configured() if self.a else False

    @property
    def configured_b(self):
        return self.b.is_configured() if self.b else False

    @property
    def b_in_repeat_mode(self):
        return self.configured_b and self.b.pa_button.get_repeat()

    @property
    def linked(self):
        if not self.configured_a or not self.configured_b:
            self._linked = False
        return self._linked

    @linked.setter
    def linked(self, linked):
        if self.configured_a and self.configured_b:
            self._linked = bool(linked)
        else:
            self._linked = False


class Trigger(Gtk.Button):
    __gsignals__ = { "ratio" : (GObject.SignalFlags.RUN_LAST,
                                GObject.TYPE_NONE,
                                (GObject.TYPE_DOUBLE,))}
    def __init__(self, num):
        Gtk.Button.__init__(self)
        self.set_border_width(1)

        self.progress = Gtk.ProgressBar()
        self.progress.set_size_request(20, 0)
        self.progress.set_orientation(Gtk.Orientation.HORIZONTAL)

        num_label = Gtk.Label.new(f"{num+1:02d}")
        self.label = Gtk.Label.new()
        self.label.set_ellipsize(Pango.EllipsizeMode.END)
        pad = Gtk.Box()
        sg = Gtk.SizeGroup.new(Gtk.SizeGroupMode.HORIZONTAL)
        sg.add_widget(num_label)
        sg.add_widget(pad)

        label_box = Gtk.HBox()
        label_box.set_spacing(3)
        label_box.pack_start(num_label, False)
        label_box.pack_start(self.label, True)
        label_box.pack_end(pad, False)

        vbox = Gtk.VBox()
        vbox.pack_start(label_box, True)
        vbox.pack_start(self.progress, True)
        self.add(vbox)
        self.connect("button-release-event", self._on_button_press)

    def _on_button_press(self, widget, event):
        ratio = 0.0  # The ratio we want if the progress bar is missed.
        try:
            if event.button != 1:
                return
            parent = self.get_allocation()
            child = self.progress.get_allocation()
            child.x -= parent.x
            child.y -= parent.y
            if child.x < event.x < child.x + child.width and child.y <= event.y:
                ratio = (event.x - child.x) / child.width
        finally:
            self.emit("ratio", ratio)


class MiniPlayer(Gtk.Grid):
    """A miniature player for a jingles or information segments."""

    SOURCE_IS_DEST = Gdk.Atom.intern('application/x-idjc-miniplayer_internal', False)
    PLAYER_IS_SOURCE = Gdk.Atom.intern('application/x-idjc-playlist-export', False)
    PLAYER_IS_DEST = Gdk.Atom.intern('application/x-idjc-miniplayer-export', False)
    OTHER_PLAYER_IS_DEST = Gdk.Atom.intern('application/x-idjc-miniplayer-export-wide', False)

    def __init__(self, num, others, parent, link_above, link_below, pagenum):
        self.num = num
        self.others = others
        self.approot = parent
        self.pathname = None
        self.uuid = str(uuid.uuid4())
        self._pa_enable_f = False
        self.link_above = link_above
        self.link_below = link_below
        self.pagenum = pagenum
        self.ratio = 0.0

        Gtk.Grid.__init__(self, column_homogeneous=True, row_homogeneous=True)
        self.set_border_width(1)

        self.highlight = 0

        image = Gtk.Image.new_from_icon_name("media-playback-stop-symbolic", Gtk.IconSize.BUTTON)
        self.stop = Gtk.Button()
        self.stop.set_border_width(1)
        self.stop.add(image)
        self.attach(self.stop, 0, 0, 3, 1)
        self.stop.connect("clicked", self._on_stop)
        set_tip(self.stop, _('Stop'))

        self.trigger = Trigger(num)
        self.trigger.connect("clicked", self._on_trigger)
        self.trigger.connect("ratio", self._on_ratio)
        self.attach_next_to(self.trigger, self.stop, Gtk.PositionType.RIGHT, 12, 1)
        set_tip(self.trigger, _('Play'))

        self.pa_button = PostActionButton()
        self.pa_button.connect("notify::mode", self._on_pa_mode_changed)
        self.pa_button.set_border_width(1)
        self.attach_next_to(self.pa_button, self.trigger, Gtk.PositionType.RIGHT, 2, 1)
        set_tip(self.pa_button, _('Post-action mode switch: Play then stop; play then repeat; play then next below; play then resume specified playlist.'))

        self.config_image = Gtk.Image.new_from_icon_name("media-tape-symbolic",
                                             Gtk.IconSize.BUTTON)
        self.config_image.set_no_show_all(True)
        self.config_image.connect("notify::visible", self._config_image_visible)
        self.config = Gtk.Button()
        self.config.set_border_width(1)
        self.config.add(self.config_image)
        self.attach_next_to(self.config, self.pa_button, Gtk.PositionType.RIGHT, 2, 1)
        self.config.connect("clicked", self._on_config)
        self.source_targets = Gtk.TargetList()
        self.source_targets.add(self.SOURCE_IS_DEST, Gtk.TargetFlags.SAME_APP, 1)
        self.source_targets.add(self.PLAYER_IS_DEST, Gtk.TargetFlags.SAME_APP, 2)
        self.source_targets.add(self.OTHER_PLAYER_IS_DEST, 0, 3)
        self.config.connect("drag-begin", self._drag_begin)
        self.config.connect("drag-data-get", self._drag_get_data)
        set_tip(self.config, _('Configure'))
        self.config.drag_dest_set(Gtk.DestDefaults.ALL, None, Gdk.DragAction.COPY)
        target_list = Gtk.TargetList()
        target_list.add(self.SOURCE_IS_DEST, Gtk.TargetFlags.SAME_APP, 1)
        target_list.add(self.PLAYER_IS_SOURCE, 0, 2)
        target_list.add_uri_targets(2)
        target_list.add_text_targets(3)
        self.config.drag_dest_set_target_list(target_list)
        self.config.connect("drag-data-received", self._drag_data_received)

        self.dialog = ConfigDialog(self, parent.window)
        self.dialog.connect("response", self._on_dialog_response)
        self.dialog.emit("response", Gtk.ResponseType.NO)
        self.timeout_source_id = None
        self.play_duration = 0.0

        self.link_above.register_b(self)  # a goes to previous MiniPlayer or None
        self.link_below.register_a(self)  # b will go to next MiniPlayer or None

        # Create the widget that will be used in the tab
        self.tabwidget = Gtk.HBox()
        self.tabwidget.set_spacing(3)
        sep = Gtk.VSeparator()
        self.tabwidget.pack_start(sep)
        vb = Gtk.VBox()
        self.tabwidget.pack_start(vb)
        hb = Gtk.HBox()
        hb.set_spacing(3)
        self.tabname = Gtk.Label()
        self.tabtime = Gtk.Label()
        hb.pack_start(self.tabname)
        hb.pack_start(self.tabtime)
        vb.pack_start(hb)
        self.tabprog = Gtk.ProgressBar()
        self.tabprog.set_size_request(0, 5)
        vb.pack_start(self.tabprog)
        self.tabwidget.show_all()

    def _on_pa_mode_changed(self, widget, prop):
        if widget.get_once() or widget.get_play_x():
            self.link_below.linked = False
        elif widget.get_repeat() and self.link_above.linked:
            widget.set_next()
        elif widget.get_repeat() and not self.link_above.linked:
            self.link_below.linked = False
        elif widget.get_sequence():
            if self.link_below.b_in_repeat_mode:
                widget.set_next()
            else:
                self.link_below.linked = True
                if not self.link_below.linked:
                    widget.set_next()

    def is_configured(self):
        return self.trigger.get_sensitive()

    def _config_image_visible(self, widget, _):
        if widget.get_visible():
            self.config.drag_source_set(Gdk.ModifierType.BUTTON1_MASK, None,
                                        Gdk.DragAction.COPY)
            self.config.drag_source_set_target_list(self.source_targets)
        else:
            self.config.drag_source_unset()

    def _drag_begin(self, widget, context):
        widget.drag_source_set_icon_name("media-tape-symbolic")

    def _drag_get_data(self, widget, context, selection, target_id, etime):
        if target_id == 1:
            selection.set(selection.get_target(), 8, (self.pagenum, self.num))
        uri = b"file://" + urllib.request.quote(self.pathname).encode('utf-8') + b"\n"
        if target_id == 2:
            source = f"{indices_encode(self.pagenum, self.num)}\n".encode("utf-8")
            selection.set(selection.get_target(), 8, uri + source)
        if target_id == 3:
            selection.set(selection.get_target(), 8, uri)
        return True

    def _drag_data_received(self, widget, context, x, y, dragged, target_id, etime):
        if target_id == 1:
            otherpagenum, othernum = dragged.get_data()
            if otherpagenum == self.pagenum:
                other = self.others[othernum]
            else:
                other = self.approot.miniplrs.pages[otherpagenum].all_players[othernum]
            if other != self:
                self.stop.clicked()
                other.stop.clicked()
                mask = widget.get_window().get_pointer()[-1]
                if (mask & Gdk.ModifierType.CONTROL_MASK) and not self.is_configured():
                    self._copy(other)
                else:
                    self._swap(other)
                    self._unlink()
                return
        elif target_id in (2, 3):
            if target_id == 2:
                data = dragged.get_data().strip().splitlines()
            else:
                data = dragged.get_text().strip().splitlines()
            if not data or len(data) > 1:
                return
            try:
                data = data[0].decode("utf-8")
            except UnicodeError as e:
                print(e)
                return

            if data.startswith("file://"):
                _, data = data.split("file://")
                pathname = urllib.request.unquote(data)
                title = get_media_metadata(pathname, self.approot).title
                if title:
                    self.stop.clicked()
                    self._set(pathname, title, 0.0)
                    self._unlink()
                    return

    def _copy(self, other):
        self._set(other.pathname, other.trigger.label.get_text() or "",
                    other.level, other.trigger.get_sensitive())

    def _swap(self, other):
        priors = self.others[self.num - 1], self.others[other.num - 1]
        for each in priors:
            if each.pa_button.get_sequence():
                each.pa_button.set_once()
        self.pa_button.set_once()
        other.pa_button.set_once()

        new_pathname = other.pathname
        new_text = other.trigger.label.get_text() or ""
        new_level = other.level
        new_sensitive = other.trigger.get_sensitive()

        other._set(self.pathname, self.trigger.label.get_text() or "",
                   self.level, self.trigger.get_sensitive())
        self._set(new_pathname, new_text, new_level, new_sensitive)

    def _set(self, pathname, button_text, level, sensitive=True):
        if pathname is None:
            self.dialog.audio.filechooser.set_current_folder(os.path.expanduser("~"))
        else:
            try:
                self.dialog.audio.filechooser.set_filename(pathname)
            except:
                self.dialog.audio.filechooser.set_current_folder(os.path.expanduser("~"))

        self.dialog.audio.label_entry.set_text(button_text)
        self.dialog.audio.gain_adj.set_value(level)
        self._on_dialog_response(self.dialog,
                                 Gtk.ResponseType.ACCEPT
                                 if sensitive else
                                 Gtk.ResponseType.NO,
                                 pathname)

    def _on_config(self, widget):
        self.stop.clicked()
        if self.pathname and os.path.isfile(self.pathname):
            self.dialog.audio.filechooser.select_filename(self.pathname)
        self.dialog.audio.label_entry.set_text(self.trigger.label.get_text() or "")
        self.dialog.audio.gain_adj.set_value(self.level)
        if self.link_above.configured_a:
            self.dialog.sequence.link_above.set_sensitive(True)
            self.dialog.sequence.link_above.set_active(self.link_above.linked)
        else:
            self.dialog.sequence.link_above.set_sensitive(False)
            self.dialog.sequence.link_above.set_active(False)
        if self.link_below.configured_b:
            self.dialog.sequence.link_below.set_sensitive(True)
            self.dialog.sequence.link_below.set_active(self.link_below.linked)
        else:
            self.dialog.sequence.link_below.set_sensitive(False)
            self.dialog.sequence.link_below.set_active(False)
        self.dialog.show()

    def _on_trigger(self, widget):
        if not self.trigger.get_sensitive() or not self.pathname:
            return

        self._pa_enable_f = False
        if not self.timeout_source_id:
            self.timeout_source_id = timeout_add(playergui.PROGRESS_TIMEOUT,
                            self._progress_timeout)
            self.ratio = 0.0
            self.tabname.set_text(self.trigger.label.get_text())
            self.tabtime.set_text('0.0')
            self.tabprog.set_fraction(0.0)
            self.approot.miniplrs.nb_plrs_box.add(self.tabwidget)
            self.approot.effect_started(self.trigger.label.get_text(),
                                        self.pathname, self.num)
        self.start_time = time.time() - self.ratio*self.play_duration
        self.approot.mixer_write(f"EFCT={self.num}\n"
                                    f"PLRP={self.pathname}\n"
                                    f"RGDB={self.level}\n"
                                    f"SEEK={self.ratio*self.play_duration:.0f}\n"
                                    f"ACTN=playeffect\nend\n")
        self.ratio = 0.0

    def _on_stop(self, widget):
        self._pa_enable_f = False
        self._stop_progress()
        self.approot.mixer_write(f"EFCT={self.num}\nACTN=stopeffect\nend\n")

    def _on_ratio(self, widget, ratio):
        if self.timeout_source_id:
            self.ratio = ratio
            # Upon button release this value will be used for seeking.
        else:
            # Don't set ratio unless already playing.
            # This allows user to casually click a button and not worry about
            # accidentally hitting the progress bar.
            self.ratio = 0.0

    def pause(self):
        self.approot.mixer_write(f"EFCT={self.num}\nACTN=pauseeffect\nend\n")

    def _progress_timeout(self):
        now = time.time()
        played = now - self.start_time
        try:
            ratio = min(played / self.play_duration, 1.0)
        except ZeroDivisionError:
            pass
        else:
            self.trigger.progress.set_fraction(ratio)
            self.tabprog.set_fraction(ratio)
            remaining = self.play_duration - played
            if remaining < 0.0:
                remaining = 0.0
            self.tabtime.set_text(f"{remaining:4.1f}")
            self._pa_enable_f = True
        return True

    def _stop_progress(self):
        if self.timeout_source_id:
            source_remove(self.timeout_source_id)
            self.timeout_source_id = None
            self.trigger.progress.set_fraction(0.0)
            self.approot.miniplrs.nb_plrs_box.remove(self.tabwidget)
            self.approot.effect_stopped(self.num)

    def _on_dialog_response(self, dialog, response_id, pathname=None):
        if response_id in (Gtk.ResponseType.ACCEPT, Gtk.ResponseType.NO):
            self.pathname = pathname or dialog.audio.filechooser.get_filename()
            text = dialog.audio.label_entry.get_text() if self.pathname and \
                                        os.path.isfile(self.pathname) else ""
            text = text.strip()
            if not text and self.pathname and response_id != Gtk.ResponseType.NO:
                meta = get_media_metadata(self.pathname, self.approot)
                text = meta.title
            else:
                meta = None
            if response_id == Gtk.ResponseType.NO or not text:
                dialog.audio.filechooser.unselect_all()
                dialog.audio.filechooser.set_filename("")
                dialog.audio.filechooser.set_current_folder(os.path.expanduser("~"))
                dialog.audio.label_entry.set_text("")
                dialog.audio.gain_adj.set_value(0.0)
                dialog.audio._stored_filename = None
                self.pathname = ""
                text = ""
                self._unlink()
            sens = bool(text)
            if sens:
                self.play_duration = meta.length if meta else \
                        get_media_metadata(self.pathname, self.approot).length
            else:
                self.play_duration = 0.0
            for each in self.trigger, self.stop, self.pa_button:
                each.set_sensitive(sens)
            self.config_image.set_visible(sens)
            self.trigger.label.set_use_markup(True)
            self.trigger.label.set_label(GLib.markup_escape_text(text))
            self.level = dialog.audio.gain_adj.get_value()
            self.link_above.linked = dialog.sequence.link_above.get_active()
            self.link_below.linked = dialog.sequence.link_below.get_active()
            if response_id == Gtk.ResponseType.ACCEPT and pathname is not None:
                self.uuid = str(uuid.uuid4())

    def _unlink(self):
        self.pa_button.set_once()
        self.link_above.linked = False
        self.link_below.linked = False

    def marshall(self):
        link = link_uuid_reg.get_link_filename(self.uuid)
        if link is not None:
            # Replace orig file abspath with alternate path to a hard link
            # except when link is None as happens when a hard link fails.
            link = PathStr("links") / link
            self.pathname = PM.basedir / link
            if not self.dialog.get_visible():
                self.dialog.audio.filechooser.set_filename(self.pathname)
        return json.dumps([self.trigger.label.get_text(),
                          (link or self.pathname), self.level, self.uuid,
                          self.pa_button._mode.value])

    def unmarshall(self, data):
        try:
            label, pathname, level, self.uuid, self.pa_button.new_mode = json.loads(data)
        except ValueError:
            label = ""
            pathname = None
            level = 0.0

        if pathname is not None and not pathname.startswith(os.path.sep):
            pathname = PM.basedir / pathname
        if pathname is None or not os.path.isfile(pathname):
            self.dialog.audio.filechooser.unselect_all()
            label = ""
        else:
            self.dialog.audio.filechooser.set_filename(pathname)
        self.dialog.audio.label_entry.set_text(label)
        self.dialog.audio.gain_adj.set_value(level)
        self._on_dialog_response(self.dialog, Gtk.ResponseType.ACCEPT, pathname)
        self.pathname = pathname

    def update_highlight(self, highlight):
        if highlight == self.highlight:
            return

        self.highlight = highlight
        if not highlight and self._pa_enable_f and self.pa_button.get_play_x():
            self._stop_progress()
            self.approot.play_on_player_by_index(int(self.pa_button.mode.value[-1]) - 1)
        elif not highlight and self._pa_enable_f and self.pa_button.get_repeat():
            self._stop_progress()
            self.trigger.clicked()
        elif not highlight and self._pa_enable_f and self.pa_button.get_sequence():
            self._stop_progress()
            try:
                self.link_below.b.trigger.clicked()
            except AttributeError:
                pass
        elif not highlight:
            self._stop_progress()

        if self.trigger.label.get_use_markup():
            if highlight:
                self.trigger.label.set_label(
                        f"""<span foreground='red' font_weight='bold'>{
                        self.trigger.label.get_label()}</span>""")
            else:
                self.trigger.label.set_label(GLib.markup_escape_text(
                                             self.trigger.label.get_text()))


class ConfigDialog(Gtk.Dialog):
    def __init__(self, player, window):
        Gtk.Dialog.__init__(self, title=_(f'Player {player.num + 1} Config'))
        self.add_buttons(_("Clear"), Gtk.ResponseType.NO,
                         _("Cancel"), Gtk.ResponseType.REJECT,
                         _("OK"), Gtk.ResponseType.ACCEPT)
        self.set_default_response(Gtk.ResponseType.ACCEPT)
        self.set_modal(True)
        self.set_transient_for(window)
        self.connect("delete-event", lambda w, e: w.hide() or True)
        self.connect("response", self._cb_response)

        ca = self.get_content_area()
        ca.set_orientation(Gtk.Orientation.VERTICAL)
        ca.set_border_width(5)

        self.audio = AudioChooser(player)
        self.audio.set_margin_bottom(20)
        ca.pack_start(self.audio, True, True, 0)

        self.audio.filechooser.connect("file-activated", \
                        lambda w: self.response(Gtk.ResponseType.ACCEPT))
        self.sequence = SequenceChooser(player)
        ca.show_all()

    def _cb_response(self, dialog, response_id):
        dialog.hide()
        if response_id == Gtk.ResponseType.NO:
            self.audio.clear()
            self.sequence.clear()


class SequenceChooser(Gtk.VBox):
    """Used for joining other players in sequence."""

    def __init__(self, player):
        Gtk.VBox.__init__(self)
        self.player = player

        def factory(label):
            button = Gtk.ToggleButton.new_with_label(label)
            box = Gtk.Box()
            box.set_halign(Gtk.Align.CENTER)
            box.add(button)
            return button, box

        self.link_above, box = factory(_('Link Above'))
        self.pack_start(box, False, False, 0)
        self.link_below, box = factory(_('Link Below'))
        self.pack_end(box, False, False, 0)

    def clear(self):
        self.link_above.set_active(False)
        self.link_below.set_active(False)


class AudioChooser(Gtk.VBox):
    """Used for selecting the audio facility of the MiniPlayer."""

    file_filter = Gtk.FileFilter()
    file_filter.set_name(_('Supported media'))
    for each in supported.media:
        if each not in (".cue", ".txt"):
            file_filter.add_pattern("*" + each)
            file_filter.add_pattern("*" + each.upper())

    def __init__(self, player):
        Gtk.VBox.__init__(self)
        self.filechooser = Gtk.FileChooserWidget.new(Gtk.FileChooserAction.OPEN)
        self.filechooser.set_size_request(950, 440)
        self.pack_start(self.filechooser, True, True, 0)

        hbox = Gtk.HBox()
        hbox.set_spacing(3)
        # TC: Labels a text entry for setting a trigger button's text.
        label = Gtk.Label.new(_('Trigger text'))
        # TC: Default label for a trigger button. Needs to not be the name of a song.
        self.label_entry = Gtk.Entry()
        self.label_entry.set_max_length(125)
        self.label_entry.set_width_chars(50)
        self.label_entry.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, "edit-clear-symbolic")
        self.label_entry.set_icon_activatable(Gtk.EntryIconPosition.SECONDARY, True)
        self.label_entry.set_placeholder_text(_('User supplied label'))
        self.label_entry.connect("icon-press", lambda w, p, e: w.set_text(""))
        hbox.pack_start(label, False)
        hbox.pack_start(self.label_entry, False)

        spc = Gtk.HBox()
        hbox.pack_start(spc, False, padding=3)

        label = Gtk.Label.new(_('Level adjustment (dB)'))
        self.gain_adj = Gtk.Adjustment(value=0.0, lower=-32.0, upper=10.0, step_increment=0.5)
        gain = Gtk.SpinButton.new(self.gain_adj, 1.0, 1)
        hbox.pack_start(label, False)
        hbox.pack_start(gain, False)

        self.pack_start(hbox, False)

        self.connect("notify::visible", self._cb_notify_visible)
        self.filechooser.add_filter(self.file_filter)
        self.filechooser.connect("file-activated", lambda w: self.label_entry.set_text(""))

    def set_filename(self, filename):
        self._stored_filename = filename
        Gtk.FileChooserDialog.set_filename(self, filename)

    def clear(self):
        self.filechooser.unselect_all()
        self.filechooser.set_current_folder(os.path.expanduser("~"))
        self.label_entry.set_text("")
        self.gain_adj.set_value(0.0)

    def _cb_notify_visible(self, *args):
        # Make sure filename is shown in the location box.

        if self.get_visible():
            filename = self.filechooser.get_filename()
            if filename is None:
                try:
                    if self._stored_filename is not None:
                        self.filechooser.set_filename(self._stored_filename)
                except AttributeError:
                    pass
        else:
            self._stored_filename = self.filechooser.get_filename()


class PlayerBank(Gtk.Frame):
    """A vertical stack of MiniPlayers with level controls."""

    def __init__(self, qty, base, filename, parent, all_players, pagenum):
        Gtk.Frame.__init__(self)
        self.base = base
        self.session_filename = filename

        hbox = Gtk.HBox()
        hbox.set_spacing(1)
        self.add(hbox)
        vbox = Gtk.VBox()
        hbox.pack_start(vbox)

        self.players = []
        self.all_players = all_players

        count = 0

        if not all_players:
            link_above = PlayerLink()
        else:
            link_above = all_players[-1].link_below

        for row in range(qty):
            link_below = PlayerLink()
            player = MiniPlayer(base + row, self.all_players, parent, link_above, link_below, pagenum)
            link_above = link_below
            self.players.append(player)
            self.all_players.append(player)
            vbox.pack_start(player)
            count += 1

        level_vbox = Gtk.VBox()
        hbox.pack_start(level_vbox, False, padding=3)

        pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(PGlobs.themedir / "volume16.svg",
                                                         16, 16, True)
        vol_image = Gtk.Image.new_from_pixbuf(pixbuf)
        vol_image.set_margin_top(2)

        self.vol_adj = Gtk.Adjustment(value=127.0, lower=0.0, upper=127.0, step_increment=1.0, page_increment=10.0)
        self.vol_adj.connect("value-changed", lambda w: parent.send_new_mixer_stats())
        vol = Gtk.Scale(adjustment=self.vol_adj, orientation=Gtk.Orientation.VERTICAL)
        vol.set_inverted(True)
        vol.set_draw_value(False)
        set_tip(vol, _('Playback level.'))

        pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(PGlobs.themedir / "headroom16.svg",
                                                         16, 16, True)
        headroom_image = Gtk.Image.new_from_pixbuf(pixbuf)
        headroom_image.set_margin_top(2)
        self.mute_adj = Gtk.Adjustment(value=100.0, lower=0.0, upper=127.0, step_increment=1.0, page_increment=10.0)
        self.mute_adj.connect("value-changed", lambda w: parent.send_new_mixer_stats())
        headroom = Gtk.Scale(adjustment=self.mute_adj, orientation=Gtk.Orientation.VERTICAL)
        headroom.set_inverted(True)
        headroom.set_draw_value(False)
        set_tip(headroom, _('Headroom that is applied on the main players when a MiniPlayer is playing.'))

        spc = Gtk.VBox()

        for widget, expand in zip((vol_image, vol, spc, headroom_image, headroom),
                                    (False, True, False, False, True)):
            level_vbox.pack_start(widget, expand, padding=2)

    def marshall(self):
        return json.dumps([self.vol_adj.get_value(), self.mute_adj.get_value()] +
                          [x.marshall() for x in self.players])

    def unmarshall(self, data):
        try:
            iter_= iter(json.loads(data))
            self.vol_adj.set_value(next(iter_))
            self.mute_adj.set_value(next(iter_))
            for per_widget_data, widget in zip(iter_, self.players):
                widget.unmarshall(per_widget_data)
        except json.decoder.JSONDecodeError as e:
            print(f"{type(e)}: {e}")

    def restore_session(self):
        try:
            with open(PM.basedir / self.session_filename, "r") as f:
                self.unmarshall(f.read())
        except IOError:
            print("failed to read mini players session file")

    def save_session(self, where):
        try:
            with open((where or PM.basedir) / self.session_filename, "w") as f:
                f.write(self.marshall())
        except IOError:
            print("failed to write mini players session file")

    def update_highlights(self, bits):
        for bit, each in enumerate(self.players):
            each.update_highlight((1 << bit + self.base) & bits)

    def stop(self):
        for each in self.players:
            each.stop.clicked()

    def uuids(self):
        return (x.uuid for x in self.widgets)

    def pathnames(self):
        return (x.pathname for x in self.widgets)


class PlayerPage(Gtk.HBox):
    def __init__(self, pagenum, parent):
        Gtk.HBox.__init__(self)
        self.pagenum = pagenum
        self.parent = parent
        self.set_border_width(4)
        self.set_spacing(10)
        self.viewlevels = (5,)
        plrs_hbox = Gtk.HBox()
        plrs_hbox.set_homogeneous(True)
        plrs_hbox.set_spacing(6)
        n_miniplrs = PGlobs.num_mini_players
        base = 0
        max_rows = 12
        n_cols = (n_miniplrs + max_rows - 1) // max_rows
        self.all_players = []
        self.player_banks = []

        for col in range(n_cols):
            bank = PlayerBank(min(n_miniplrs - base, max_rows), base,
            f"miniplayers_p{pagenum+1}b{col+1}_session", parent, self.all_players, pagenum)
            parent.label_subst.add_widget(bank, f"miniplayerbank{col}",
                _('Bank {}'.format(col + 1)), title_prefix=_('Page {}: ').format(pagenum + 1))
            self.player_banks.append(bank)
            plrs_hbox.pack_start(bank)
            base += max_rows

        self.pack_start(plrs_hbox)

    def save_session(self, where):
        for each in self.player_banks:
            each.save_session(where)

    def restore_session(self):
        for each in self.player_banks:
            each.restore_session()


class MiniPlayers(Gtk.Notebook):
    def __init__(self, parent):
        self.approot = parent

        sg = Gtk.SizeGroup.new(Gtk.SizeGroupMode.VERTICAL)
        self.nb_label = Gtk.HBox(False, 10)
        vb = Gtk.VBox()
        lbl = Gtk.Label.new(_('Mini Players'))
        sg.add_widget(lbl)
        lbl.set_margin_top(2)
        lbl.set_margin_bottom(2)
        vb.pack_start(lbl)
        vb.show()
        self.nb_label.pack_start(vb)
        self.nb_plrs_box = Gtk.HBox(False, 5)
        self.nb_plrs_box.connect("add", self._on_nb_add, parent.player_nb)
        self.nb_plrs_box.connect("remove", self._on_nb_remove)
        self.nb_label.pack_start(self.nb_plrs_box)
        self.nb_label.show_all()
        self.nb_plrs_box.hide()

        Gtk.Notebook.__init__(self)
        self.pages = []
        for pagenum in range(4):
            label = Gtk.Label.new()
            parent.label_subst.add_widget(label, f"minilabel{pagenum}", _('Page {}').format(pagenum + 1))
            self.pages.append(PlayerPage(pagenum, parent))
            self.append_page(self.pages[-1], label)
            sg.add_widget(self.pages[-1].all_players[0].tabwidget)
        self.top = self.pages[0]
        self.approot.player_nb.connect('switch-page',
                                       self._on_nb_switch_page,
                                       self.nb_plrs_box)
        self.show_all()
        self.connect("switch-page", self._on_switch_page)

    def _on_nb_add(self, container, child, notebook):
        page_widget = notebook.get_nth_page(notebook.get_current_page())
        if not isinstance(page_widget, MiniPlayers):
            container.show()

    def _on_nb_remove(self, container, child):
        if not container.get_children():
            container.hide()

    def _on_nb_switch_page(self, notebook, page, page_num, box):
        page_widget = notebook.get_nth_page(page_num)
        if isinstance(page_widget, MiniPlayers):
            box.hide()
        elif box.get_children():
            box.show()

    def _on_switch_page(self, notebook, page, page_num):
        for each in self.top.all_players:
            each.stop.clicked()
        # Wait for user interface to catch up.
        while 1:
            for player in self.top.all_players:
                if player.timeout_source_id:
                    time.sleep(0.05)
                    self.approot.vu_update()
                    break  # Break for loop and restart while loop.
            else:
                break  # Break out of the while loop if for loop completes.
        for each in self.top.all_players:
            each.update_highlight(False)
        self.top = page
        self.approot.send_new_mixer_stats()

    def restore_session(self):
        for each in self.pages:
            each.restore_session()
        for page in self.pages:
            for player in page.all_players:
                player.pa_button.restore_session_pass_2()

    def save_session(self, where):
        for each in self.pages:
            each.save_session(where)

    def update_highlights(self, ep):
        for each in self.top.player_banks:
            each.update_highlights(ep)

    def cleanup(self):
        pass

    @property
    def playing(self):
        return False

    @property
    def flush(self):
        return 0

    @flush.setter
    def flush(self, value):
        pass

    @indices_decode
    def get_sequence_info(self, page, player):
        plr = self.pages[page].all_players[player]
        if not plr.is_configured():
            return None

        labels = []
        durations = []

        while 1:
            labels.append(plr.trigger.label.get_text())
            durations.append(plr.play_duration)
            if plr.link_below.linked:
                plr = plr.link_below.b
            else:
                break

        return page, player, labels, durations, plr.pa_button.mode

    @indices_decode
    def play(self, page, player):
        self.set_current_page(page)
        self.top.all_players[player].trigger.clicked()


class MiniPlayersWindow(Gtk.Window):
    def __init__(self, notebook, notebook_page):
        Gtk.Window.__init__(self)
        self._notebook = notebook
        self._notebook_page = notebook_page
        self.set_title(_('IDJC Mini Players') + PM.title_extra)
        self.connect("delete-event", lambda w, e: self.hide() or True)
        self.connect("notify::visible", self.cb_visible)

    def cb_visible(self, window, *args):
        if window.props.visible:
            if not window.get_children():
                page = self._notebook.get_current_page()
                pane = self._notebook.get_nth_page(self._notebook_page)
                self._notebook.remove_page(self._notebook_page)
                table = Gtk.Grid()
                table.set_row_homogeneous(True)
                table.set_column_homogeneous(True)
                button = Gtk.Button.new_with_label(_('Restore'))
                button.connect("clicked", lambda w: window.hide())
                table.attach(button, 0, 0, 1, 1)
                table.attach_next_to(Gtk.Box(), button, Gtk.PositionType.LEFT, 1, 1)
                table.attach_next_to(Gtk.Box(), button, Gtk.PositionType.RIGHT, 1, 1)
                table.attach_next_to(Gtk.Box(), button, Gtk.PositionType.TOP, 1, 4)
                table.attach_next_to(Gtk.Box(), button, Gtk.PositionType.BOTTOM, 1, 4)
                table.show_all()
                self._notebook.insert_page(table, pane.nb_label,
                                           self._notebook_page)
                self._notebook.set_current_page(page)
                window.add(pane)
        else:
            try:
                pane = window.get_children()[0]
            except IndexError:
                pass
            else:
                window.remove(pane)
                page = self._notebook.get_current_page()
                self._notebook.remove_page(self._notebook_page)
                self._notebook.insert_page(pane, pane.nb_label,
                                           self._notebook_page)
                self._notebook.set_current_page(page)
