# Copyright (c) 2014-2020 Cedric Bellegarde <cedric.bellegarde@adishatz.org>
# 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 3 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. If not, see <http://www.gnu.org/licenses/>.

from gi.repository import Gtk, GObject, Gdk, GdkPixbuf, GLib

from lollypop.objects_album import Album
from lollypop.define import App, ArtSize, ArtBehaviour, MARGIN
from lollypop.utils import get_round_surface, emit_signal
from lollypop.menu_header import HeaderType
from lollypop.helper_signals import SignalsHelper, signals_map


class MenuBuilder(Gtk.Stack, SignalsHelper):
    """
        Advanced menu model constructor
        Does not support submenus
    """

    __gsignals__ = {
        "hidden": (GObject.SignalFlags.RUN_FIRST, None, (bool,)),
    }

    @signals_map
    def __init__(self, menu, scrolled=False):
        """
            Init menu
            @param menu as Gio.Menu
            @param scrolled as bool
        """
        Gtk.Stack.__init__(self)
        self.__built = False
        self.__boxes = {}
        self.__menu_queue = []
        self.__submenu_queue = []
        self.__widgets_queue = []
        self.__add_menu(menu, "main", False, scrolled)
        return [
            (App().window, "adaptive-changed", "_on_adaptive_changed")
        ]

    def append_widget(self, widget):
        """
            Append widget to menu
            @param widget as Gtk.Widget
        """
        self.__widgets_queue.append(widget)
        if self.__built:
            self.__add_widgets()

#######################
# PROTECTED           #
#######################
    def _on_adaptive_changed(self, window, status):
        """
            Destroy self if adaptive off
            @param window as Window
            @param status as bool
        """
        if not status:
            self.destroy()

#######################
# PRIVATE             #
#######################
    def __add_widgets(self):
        """
            Add pending widget to menu
        """
        while self.__widgets_queue:
            widget = self.__widgets_queue.pop(0)
            if widget.submenu_name is not None:
                self.__add_menu_container(widget.submenu_name, True, True)
                self.__boxes[widget.submenu_name].add(widget)
                button = Gtk.ModelButton.new()
                button.set_label(widget.submenu_name)
                button.set_alignment(0, 0.5)
                button.set_property("menu-name", widget.submenu_name)
                button.show()
                self.__boxes["main"].add(button)
            else:
                main = self.get_child_by_name("main")
                if isinstance(main, Gtk.ScrolledWindow):
                    # scrolled -> viewport -> box
                    main = main.get_child().get_child()
                main.add(widget)

    def __add_menu_container(self, menu_name, submenu, scrolled):
        """
            Add menu container
            @param menu_name as str
            @param submenu as bool
            @param scrolled as bool
        """
        box = self.get_child_by_name(menu_name)
        if box is None:
            box = Gtk.Box.new(Gtk.Orientation.VERTICAL, 0)
            box.connect("map", self.__on_box_map, menu_name)
            self.__boxes[menu_name] = box
            box.set_property("margin", 10)
            box.show()
            if scrolled:
                scrolled = Gtk.ScrolledWindow()
                scrolled.set_policy(Gtk.PolicyType.NEVER,
                                    Gtk.PolicyType.AUTOMATIC)
                scrolled.show()
                scrolled.add(box)
                self.add_named(scrolled, menu_name)
            else:
                self.add_named(box, menu_name)
            if submenu:
                button = Gtk.ModelButton.new()
                button.get_style_context().add_class("padding")
                button.set_property("menu-name", "main")
                button.set_property("inverted", True)
                button.set_label(menu_name)
                button.show()
                box.add(button)

    def __add_menu(self, menu, menu_name, submenu, scrolled):
        """
            Create container and add menu
            @param menu as Gio.Menu
            @param menu_name as str
            @param submenu as bool
            @param scrolled as bool
        """
        self.__add_menu_container(menu_name, submenu, scrolled)
        menu_range = list(range(0, menu.get_n_items()))
        if submenu:
            self.__submenu_queue.append((menu, menu_name, menu_range))
        else:
            self.__add_menu_items(menu, menu_name, menu_range)

    def __add_menu_items(self, menu, menu_name, indexes):
        """
            Add menu items present in indexes
            @param menu as Gio.Menu
            @param menu_name as str
            @param indexes as [int]
        """
        if indexes:
            i = indexes.pop(0)
            header = menu.get_item_attribute_value(i, "header")
            action = menu.get_item_attribute_value(i, "action")
            label = menu.get_item_attribute_value(i, "label")
            tooltip = menu.get_item_attribute_value(i, "tooltip")
            close = menu.get_item_attribute_value(i, "close") is not None
            if header is not None:
                header_type = header[0]
                header_label = header[1]
                if header_type == HeaderType.ALBUM:
                    album_id = header[2]
                    self.__add_album_header(header_label,
                                            album_id,
                                            menu_name)
                elif header_type == HeaderType.ARTIST:
                    artist_id = header[2]
                    self.__add_artist_header(header_label, artist_id,
                                             menu_name)
                elif header_type == HeaderType.ROUNDED:
                    artwork_name = header[2]
                    self.__add_rounded_header(header_label, artwork_name,
                                              menu_name)
                else:
                    icon_name = header[2]
                    self.__add_header(header_label, icon_name, menu_name)
                GLib.idle_add(self.__add_menu_items, menu, menu_name, indexes)
            elif action is None:
                link = menu.get_item_link(i, "section")
                submenu = menu.get_item_link(i, "submenu")
                if link is not None:
                    self.__menu_queue.append((menu, menu_name, indexes))
                    self.__add_section(label, link, menu_name)
                elif submenu is not None:
                    self.__add_submenu(label, submenu, menu_name)
                    GLib.idle_add(self.__add_menu_items, menu,
                                  menu_name, indexes)
            else:
                target = menu.get_item_attribute_value(i, "target")
                self.__add_item(label, action, target,
                                tooltip, close, menu_name)
                GLib.idle_add(self.__add_menu_items, menu, menu_name, indexes)
        # Continue to populate queued menu
        elif self.__menu_queue:
            (menu, menu_name, indexes) = self.__menu_queue.pop(-1)
            GLib.idle_add(self.__add_menu_items, menu, menu_name, indexes)
        # Finish with submenus
        elif self.__submenu_queue:
            (menu, menu_name, indexes) = self.__submenu_queue.pop(-1)
            GLib.idle_add(self.__add_menu_items, menu, menu_name, indexes)
        else:
            self.__built = True
            self.__add_widgets()

    def __add_item(self, text, action, target, tooltip, close, menu_name):
        """
            Add a Menu item
            @param text as GLib.Variant
            @param action as Gio.Action
            @param target as GLib.Variant
            @parmam tooltip as GLib.Variant
            @param close as bool
            @param menu_name as str
        """
        button = Gtk.ModelButton.new()
        button.set_action_name(action.get_string())
        button.set_label(text.get_string())
        button.set_alignment(0, 0.5)
        if close:
            button.connect("clicked",
                           lambda x: emit_signal(self, "hidden", True))
        if tooltip is not None:
            button.set_tooltip_markup(tooltip.get_string())
            button.set_has_tooltip(True)
        if target is not None:
            button.set_action_target_value(target)
        button.show()
        self.__boxes[menu_name].add(button)

    def __add_section(self, text, menu, menu_name):
        """
            Add section to menu
            @param text as as GLib.Variant
            @param menu as Gio.Menu
            @param menu_name as str
        """
        box = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 4)
        sep1 = Gtk.Separator.new(Gtk.Orientation.HORIZONTAL)
        sep1.set_hexpand(True)
        sep1.set_property("valign", Gtk.Align.CENTER)
        box.add(sep1)
        label = Gtk.Label.new(text.get_string())
        label.get_style_context().add_class("dim-label")
        if App().window.is_adaptive:
            label.get_style_context().add_class("padding")
        box.add(label)
        sep2 = Gtk.Separator.new(Gtk.Orientation.HORIZONTAL)
        sep2.set_property("valign", Gtk.Align.CENTER)
        sep2.set_hexpand(True)
        box.add(sep2)
        box.show_all()
        self.__boxes[menu_name].add(box)
        self.__add_menu(menu, menu_name, False, False)

    def __add_submenu(self, text, menu, menu_name):
        """
            Add submenu
            @param text as GLib.Variant
            @param menu as Gio.Menu
            @param menu_name as str
        """
        submenu_name = text.get_string()
        self.__add_menu(menu, submenu_name, True, True)
        button = Gtk.ModelButton.new()
        button.set_property("menu-name", submenu_name)
        button.set_label(text.get_string())
        button.set_alignment(0, 0.5)
        button.show()
        self.__boxes[menu_name].add(button)

    def __add_header(self, text, icon_name, menu_name):
        """
            Add an header for albums to close menu
            @param text as GLib.Variant
            @param icon_name as GLib.Variant
            @param menu_name as str
        """
        button = Gtk.ModelButton.new()
        button.set_alignment(0, 0.5)
        button.connect("clicked", lambda x: emit_signal(self, "hidden", True))
        button.show()
        label = Gtk.Label.new()
        label.set_markup(text)
        label.show()
        artwork = Gtk.Image.new_from_icon_name(icon_name,
                                               Gtk.IconSize.INVALID)
        artwork.set_pixel_size(ArtSize.SMALL)
        artwork.show()
        grid = Gtk.Grid()
        grid.set_column_spacing(MARGIN)
        grid.add(artwork)
        grid.add(label)
        button.set_image(grid)
        button.get_style_context().add_class("padding")
        self.__boxes[menu_name].add(button)

    def __add_album_header(self, text, album_id, menu_name):
        """
            Add an header for album to close menu
            @param text as str
            @param album_id as int
            @param menu_name as str
        """
        button = Gtk.ModelButton.new()
        button.set_alignment(0, 0.5)
        button.connect("clicked", lambda x: emit_signal(self, "hidden", True))
        button.show()
        label = Gtk.Label.new()
        label.set_markup(text)
        label.show()
        artwork = Gtk.Image.new()
        grid = Gtk.Grid()
        grid.set_column_spacing(MARGIN)
        grid.add(artwork)
        grid.add(label)
        button.set_image(grid)
        button.get_style_context().add_class("padding")
        App().art_helper.set_album_artwork(
                Album(album_id),
                ArtSize.SMALL,
                ArtSize.SMALL,
                artwork.get_scale_factor(),
                ArtBehaviour.CACHE | ArtBehaviour.CROP_SQUARE,
                self.__on_artwork,
                artwork)
        self.__boxes[menu_name].add(button)

    def __add_artist_header(self, text, artist_id, menu_name):
        """
            Add an header for artist to close menu
            @param text as str
            @param artist_id as int
            @param menu_name as str
        """
        button = Gtk.ModelButton.new()
        button.set_alignment(0, 0.5)
        button.connect("clicked", lambda x: emit_signal(self, "hidden", True))
        button.show()
        label = Gtk.Label.new()
        label.set_markup(text)
        label.show()
        artwork = Gtk.Image.new()
        grid = Gtk.Grid()
        grid.set_column_spacing(MARGIN)
        grid.add(artwork)
        grid.add(label)
        button.set_image(grid)
        button.get_style_context().add_class("padding")
        artist_name = App().artists.get_name(artist_id)
        App().art_helper.set_artist_artwork(
                artist_name,
                ArtSize.SMALL,
                ArtSize.SMALL,
                artwork.get_scale_factor(),
                ArtBehaviour.CACHE |
                ArtBehaviour.CROP_SQUARE |
                ArtBehaviour.ROUNDED,
                self.__on_artwork,
                artwork)
        self.__boxes[menu_name].add(button)

    def __add_rounded_header(self, text, artwork_name, menu_name):
        """
            Add an header for rounded widgets to close menu
            @param text as str
            @param artwork_name as str
            @param menu_name as str
        """
        def on_load_from_cache(pixbuf, artwork):
            if pixbuf is not None:
                scale_factor = artwork.get_scale_factor()
                surface = Gdk.cairo_surface_create_from_pixbuf(
                    pixbuf.scale_simple(ArtSize.MEDIUM, ArtSize.MEDIUM,
                                        GdkPixbuf.InterpType.BILINEAR),
                    scale_factor, None)
                del pixbuf
                rounded = get_round_surface(surface, scale_factor,
                                            ArtSize.MEDIUM / 4)
                artwork.set_from_surface(rounded)
                del rounded
                artwork.show()
        button = Gtk.ModelButton.new()
        button.set_alignment(0, 0.5)
        button.connect("clicked", lambda x: emit_signal(self, "hidden", True))
        button.show()
        label = Gtk.Label.new()
        label.set_markup(text)
        label.show()
        artwork = Gtk.Image.new()
        artwork.get_style_context().add_class("light-background")
        grid = Gtk.Grid()
        grid.set_column_spacing(MARGIN)
        grid.add(artwork)
        grid.add(label)
        button.set_image(grid)
        button.get_style_context().add_class("padding")
        App().task_helper.run(
                App().art.get_artwork_from_cache,
                artwork_name,
                "ROUNDED",
                ArtSize.BANNER, ArtSize.BANNER,
                callback=(on_load_from_cache, artwork))
        self.__boxes[menu_name].add(button)

    def __on_artwork(self, surface, artwork):
        """
            Set artwork
            @param surface as str
            @param artwork as Gtk.Image
        """
        if surface is None:
            artwork.set_from_icon_name("folder-music-symbolic",
                                       Gtk.IconSize.BUTTON)
        else:
            artwork.set_from_surface(surface)
        artwork.show()

    def __on_box_map(self, widget, menu_name):
        """
            On map, set stack order
            @param widget as Gtk.Widget
            @param menu_name as str
        """
        if menu_name == "main":
            self.set_transition_type(Gtk.StackTransitionType.SLIDE_LEFT)
            self.set_size_request(-1, -1)
        else:
            self.set_size_request(-1, 300)
            self.set_transition_type(Gtk.StackTransitionType.SLIDE_RIGHT)
