"""Widget generators and their signal handlers"""
# Standard Library
# pylint: disable=no-member,too-many-public-methods
import os
from gettext import gettext as _

# Third Party Libraries
from gi.repository import Gdk, Gtk

# Lutris Modules
from lutris import settings, sysoptions
from lutris.gui.widgets.common import EditableGrid, FileChooserEntry, Label, VBox
from lutris.gui.widgets.searchable_combobox import SearchableCombobox
from lutris.runners import InvalidRunner, import_runner
from lutris.util.log import logger


class ConfigBox(VBox):
    """Dynamically generate a vbox built upon on a python dict."""

    config_section = NotImplemented

    def __init__(self, game=None):
        super().__init__()
        self.options = []
        self.game = game
        self.config = None
        self.raw_config = None
        self.option_widget = None
        self.wrapper = None
        self.tooltip_default = None
        self.files = []
        self.files_list_store = None
        self.reset_buttons = {}
        self.wrappers = {}
        self.warning_boxes = {}

    def generate_top_info_box(self, text):
        """Add a top section with general help text for the current tab"""
        help_box = Gtk.Box()
        help_box.set_margin_left(15)
        help_box.set_margin_right(15)
        help_box.set_margin_bottom(5)

        icon = Gtk.Image.new_from_icon_name("dialog-information", Gtk.IconSize.MENU)
        help_box.pack_start(icon, False, False, 5)

        title_label = Gtk.Label("<i>%s</i>" % text)
        title_label.set_line_wrap(True)
        title_label.set_alignment(0, 0.5)
        title_label.set_use_markup(True)
        help_box.pack_start(title_label, False, False, 5)

        self.pack_start(help_box, False, False, 0)
        self.pack_start(Gtk.HSeparator(), False, False, 12)

        help_box.show_all()

    def generate_widgets(self):  # noqa: C901 # pylint: disable=too-many-branches,too-many-statements
        """Parse the config dict and generates widget accordingly."""
        if not self.options:
            no_options_label = Label(_("No options available"), width_request=-1)
            no_options_label.set_halign(Gtk.Align.CENTER)
            no_options_label.set_valign(Gtk.Align.CENTER)
            self.pack_start(no_options_label, True, True, 0)
            self.show_all()
            return

        # Select config section.
        if self.config_section == "game":
            self.config = self.lutris_config.game_config
            self.raw_config = self.lutris_config.raw_game_config
        elif self.config_section == "runner":
            self.config = self.lutris_config.runner_config
            self.raw_config = self.lutris_config.raw_runner_config
        elif self.config_section == "system":
            self.config = self.lutris_config.system_config
            self.raw_config = self.lutris_config.raw_system_config

        current_section = None
        current_vbox = self

        # Go thru all options.
        for option in self.options:
            try:
                if "scope" in option:
                    if self.config_section not in option["scope"]:
                        continue
                option_key = option["option"]
                value = self.config.get(option_key)

                if callable(option.get("choices")) and option["type"] != "choice_with_search":
                    option["choices"] = option["choices"]()
                if callable(option.get("condition")):
                    option["condition"] = option["condition"]()

                if option.get("section") != current_section:
                    current_section = option.get("section")
                    if current_section:
                        frame = ConfigBox.SectionFrame(current_section)
                        current_vbox = frame.vbox
                        self.pack_start(frame, False, False, 0)
                    else:
                        current_vbox = self

                self.wrapper = Gtk.Box()
                self.wrapper.set_spacing(12)
                self.wrapper.set_margin_bottom(6)
                self.wrappers[option_key] = self.wrapper

                # Set tooltip's "Default" part
                default = option.get("default")
                self.tooltip_default = default if isinstance(default, str) else None

                # Generate option widget
                self.option_widget = None
                self.call_widget_generator(option, option_key, value, default)

                # Reset button
                reset_btn = Gtk.Button.new_from_icon_name("edit-undo-symbolic", Gtk.IconSize.MENU)
                reset_btn.set_valign(Gtk.Align.CENTER)
                reset_btn.set_margin_bottom(6)
                reset_btn.set_relief(Gtk.ReliefStyle.NONE)
                reset_btn.set_tooltip_text(_("Reset option to global or default config"))
                reset_btn.connect(
                    "clicked",
                    self.on_reset_button_clicked,
                    option,
                    self.option_widget,
                    self.wrapper,
                )
                self.reset_buttons[option_key] = reset_btn

                placeholder = Gtk.Box()
                placeholder.set_size_request(32, 32)

                if option_key not in self.raw_config:
                    reset_btn.set_visible(False)
                    reset_btn.set_no_show_all(True)
                placeholder.pack_start(reset_btn, False, False, 0)

                # Tooltip
                helptext = option.get("help")
                if isinstance(self.tooltip_default, str):
                    helptext = helptext + "\n\n" if helptext else ""
                    helptext += _("<b>Default</b>: ") + _(self.tooltip_default)
                if value != default and option_key not in self.raw_config:
                    helptext = helptext + "\n\n" if helptext else ""
                    helptext += _(
                        "<i>(Italic indicates that this option is "
                        "modified in a lower configuration level.)</i>"
                    )
                if helptext:
                    self.wrapper.props.has_tooltip = True
                    self.wrapper.connect("query-tooltip", self.on_query_tooltip, helptext)

                hbox = Gtk.Box(visible=True)
                option_container = hbox
                hbox.set_margin_left(18)
                hbox.pack_end(placeholder, False, False, 5)
                # Grey out option if condition unmet
                if "condition" in option and not option["condition"]:
                    hbox.set_sensitive(False)

                hbox.pack_start(self.wrapper, True, True, 0)

                if "warning" in option:
                    option_container = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, visible=True)
                    option_container.pack_start(hbox, False, False, 0)
                    warning = ConfigBox.WarningBox(option["warning"])
                    warning.set_margin_left(18)
                    warning.set_margin_right(18)
                    warning.set_margin_bottom(6)
                    warning.update_warning(self.config)
                    self.warning_boxes[option_key] = warning
                    option_container.pack_start(warning, False, False, 0)

                # Hide if advanced
                if option.get("advanced"):
                    option_container.get_style_context().add_class("advanced")

                current_vbox.pack_start(option_container, False, False, 0)
            except Exception as ex:
                logger.exception("Failed to generate option widget for '%s': %s", option.get("option"), ex)
        self.show_all()

        show_advanced = settings.read_setting("show_advanced_options") == "True"
        self.set_advanced_visibility(show_advanced)

    def update_warnings(self):
        for box in self.warning_boxes.values():
            box.update_warning(self.config)

    class WarningBox(Gtk.Box):
        def __init__(self, warning):
            self.warning = warning
            super().__init__(spacing=6, visible=False, no_show_all=True)
            warning_image = Gtk.Image(visible=True)
            warning_image.set_from_icon_name("dialog-warning", Gtk.IconSize.DND)
            self.pack_start(warning_image, False, False, 0)
            self.warning_label = Gtk.Label(visible=True, xalign=0)
            self.warning_label.set_line_wrap(True)
            self.pack_start(self.warning_label, False, False, 0)

        def update_warning(self, config):
            if callable(self.warning):
                text = self.warning(config)
            else:
                text = self.warning

            if text:
                self.warning_label.set_markup(str(text))

            self.set_visible(bool(text))

    def set_advanced_visibility(self, value):
        """Sets the visibility of every 'advanced' option and every section that
        contains only 'advanced' options."""

        def update_widgets(widgets):
            visible_count = 0
            for widget in widgets:
                if isinstance(widget, ConfigBox.SectionFrame):
                    frame_visible_count = update_widgets(widget.vbox.get_children())
                    visible_count += frame_visible_count
                    widget.set_visible(frame_visible_count > 0)
                    widget.set_frame_visible(frame_visible_count > 1)
                else:
                    widget_visible = value or not widget.get_style_context().has_class("advanced")
                    widget.set_visible(widget_visible)
                    widget.set_no_show_all(not widget_visible)
                    if widget_visible:
                        visible_count += 1
                        widget.show_all()

            return visible_count

        update_widgets(self.get_children())

    def call_widget_generator(self, option, option_key, value, default):  # noqa: C901
        """Call the right generation method depending on option type."""
        # pylint: disable=too-many-branches
        option_type = option["type"]
        option_size = option.get("size", None)

        if option_key in self.raw_config:
            self.set_style_property("font-weight", "bold", self.wrapper)
        elif value != default:
            self.set_style_property("font-style", "italic", self.wrapper)

        if option_type == "choice":
            self.generate_combobox(option_key, option["choices"], option["label"], value, default)

        elif option_type == "choice_with_entry":
            self.generate_combobox(
                option_key,
                option["choices"],
                option["label"],
                value,
                default,
                has_entry=True,
            )
        elif option_type == "choice_with_search":
            self.generate_searchable_combobox(
                option_key,
                option["choices"],
                option["label"],
                value,
                default,
            )

        elif option_type == "bool":
            self.generate_checkbox(option, value)
            self.tooltip_default = "Enabled" if default else "Disabled"
        elif option_type == "range":
            self.generate_range(option_key, option["min"], option["max"], option["label"], value)
        elif option_type == "string":
            if "label" not in option:
                raise ValueError("Option %s has no label" % option)
            self.generate_entry(option_key, option["label"], value, option_size)
        elif option_type == "directory_chooser":
            self.generate_directory_chooser(option, value)
        elif option_type == "file":
            self.generate_file_chooser(option, value)
        elif option_type == "multiple":
            self.generate_multiple_file_chooser(option_key, option["label"], value)
        elif option_type == "label":
            self.generate_label(option["label"])
        elif option_type == "mapping":
            self.generate_editable_grid(option_key, label=option["label"], value=value)
        else:
            raise ValueError("Unknown widget type %s" % option_type)

    # Label
    def generate_label(self, text):
        """Generate a simple label."""
        label = Label(text)
        label.set_use_markup(True)
        label.set_halign(Gtk.Align.START)
        label.set_valign(Gtk.Align.CENTER)
        self.wrapper.pack_start(label, True, True, 0)

    # Checkbox
    def generate_checkbox(self, option, value=None):
        """Generate a checkbox."""

        label = Label(option["label"])
        self.wrapper.pack_start(label, False, False, 0)

        switch = Gtk.Switch()
        if value is True:
            switch.set_active(value)
        switch.connect("notify::active", self.checkbox_toggle, option["option"])
        switch.set_valign(Gtk.Align.CENTER)
        self.wrapper.pack_start(switch, False, False, 0)
        self.option_widget = switch

    def checkbox_toggle(self, widget, _gparam, option_name):
        """Action for the checkbox's toggled signal."""
        self.option_changed(widget, option_name, widget.get_active())

    # Entry
    def generate_entry(self, option_name, label, value=None, option_size=None):
        """Generate an entry box."""
        label = Label(label)
        self.wrapper.pack_start(label, False, False, 0)

        entry = Gtk.Entry()
        if value:
            entry.set_text(value)
        entry.connect("changed", self.entry_changed, option_name)
        expand = option_size != "small"
        self.wrapper.pack_start(entry, expand, expand, 0)
        self.option_widget = entry

    def entry_changed(self, entry, option_name):
        """Action triggered for entry 'changed' signal."""
        self.option_changed(entry, option_name, entry.get_text())

    def generate_searchable_combobox(self, option_name, choice_func, label, value, default):
        """Generate a searchable combo box"""
        combobox = SearchableCombobox(choice_func, value or default)
        combobox.connect("changed", self.on_searchable_entry_changed, option_name)
        self.wrapper.pack_start(Label(label), False, False, 0)
        self.wrapper.pack_start(combobox, True, True, 0)
        self.option_widget = combobox

    def on_searchable_entry_changed(self, combobox, value, key):
        self.option_changed(combobox, key, value)

    def _populate_combobox_choices(self, liststore, choices, default):
        for choice in choices:
            if isinstance(choice, str):
                choice = (choice, choice)
            if choice[1] == default:
                liststore.append((_("%s (default)") % choice[0], choice[1]))
                self.tooltip_default = choice[0]
            else:
                liststore.append(choice)

    # ComboBox
    def generate_combobox(self, option_name, choices, label, value=None, default=None, has_entry=False):
        """Generate a combobox (drop-down menu)."""
        liststore = Gtk.ListStore(str, str)
        self._populate_combobox_choices(liststore, choices, default)
        # With entry ("choice_with_entry" type)
        if has_entry:
            combobox = Gtk.ComboBox.new_with_model_and_entry(liststore)
            combobox.set_entry_text_column(0)
            if value:
                combobox.get_child().set_text(value)
        # No entry ("choice" type)
        else:
            combobox = Gtk.ComboBox.new_with_model(liststore)
            cell = Gtk.CellRendererText()
            combobox.pack_start(cell, True)
            combobox.add_attribute(cell, "text", 0)
            combobox.set_id_column(1)

            choices = list(v for k, v in choices)
            if value in choices:
                combobox.set_active_id(value)
            else:
                combobox.set_active_id(default)

        combobox.connect("changed", self.on_combobox_change, option_name)
        combobox.connect("scroll-event", self._on_combobox_scroll)
        label = Label(label)
        combobox.set_valign(Gtk.Align.CENTER)
        self.wrapper.pack_start(label, False, False, 0)
        self.wrapper.pack_start(combobox, True, True, 0)
        self.option_widget = combobox

    @staticmethod
    def _on_combobox_scroll(combobox, _event):
        """Prevents users from accidentally changing configuration values
        while scrolling down dialogs.
        """
        combobox.stop_emission_by_name("scroll-event")
        return False

    def on_combobox_change(self, combobox, option):
        """Action triggered on combobox 'changed' signal."""
        list_store = combobox.get_model()
        active = combobox.get_active()
        option_value = None
        if active < 0:
            if combobox.get_has_entry():
                option_value = combobox.get_child().get_text()
        else:
            option_value = list_store[active][1]
        self.option_changed(combobox, option, option_value)

    # Range
    def generate_range(self, option_name, min_val, max_val, label, value=None):
        """Generate a ranged spin button."""
        adjustment = Gtk.Adjustment(float(min_val), float(min_val), float(max_val), 1, 0, 0)
        spin_button = Gtk.SpinButton()
        spin_button.set_adjustment(adjustment)
        if value:
            spin_button.set_value(value)
        spin_button.connect("changed", self.on_spin_button_changed, option_name)
        label = Label(label)
        self.wrapper.pack_start(label, False, False, 0)
        self.wrapper.pack_start(spin_button, True, True, 0)
        self.option_widget = spin_button

    def on_spin_button_changed(self, spin_button, option):
        """Action triggered on spin button 'changed' signal."""
        value = spin_button.get_value_as_int()
        self.option_changed(spin_button, option, value)

    # File chooser
    def generate_file_chooser(self, option, path=None):
        """Generate a file chooser button to select a file."""
        option_name = option["option"]
        label = Label(option["label"])
        default_path = option.get("default_path") or (self.runner.default_path if self.runner else "")
        file_chooser = FileChooserEntry(
            title=_("Select file"),
            action=Gtk.FileChooserAction.OPEN,
            path=path,
            default_path=default_path
        )
        # file_chooser.set_size_request(200, 30)

        if "default_path" in option:
            default_path = self.lutris_config.system_config.get(option["default_path"])
            if default_path and os.path.exists(default_path):
                file_chooser.entry.set_text(default_path)

        if path:
            # If path is relative, complete with game dir
            if not os.path.isabs(path):
                path = os.path.expanduser(path)
                if not os.path.isabs(path):
                    if self.game and self.game.directory:
                        path = os.path.join(self.game.directory, path)
            file_chooser.entry.set_text(path)

        file_chooser.set_valign(Gtk.Align.CENTER)
        self.wrapper.pack_start(label, False, False, 0)
        self.wrapper.pack_start(file_chooser, True, True, 0)
        self.option_widget = file_chooser
        file_chooser.connect("changed", self._on_chooser_file_set, option_name)

    # Directory chooser
    def generate_directory_chooser(self, option, path=None):
        """Generate a file chooser button to select a directory."""
        label = Label(option["label"])
        option_name = option["option"]
        default_path = None
        if not path and self.game and self.game.runner:
            default_path = self.game.runner.working_dir
        directory_chooser = FileChooserEntry(
            title=_("Select folder"), action=Gtk.FileChooserAction.SELECT_FOLDER, path=path, default_path=default_path
        )
        directory_chooser.connect("changed", self._on_chooser_file_set, option_name)
        directory_chooser.set_valign(Gtk.Align.CENTER)
        self.wrapper.pack_start(label, False, False, 0)
        self.wrapper.pack_start(directory_chooser, True, True, 0)
        self.option_widget = directory_chooser

    def _on_chooser_file_set(self, entry, option):
        """Action triggered when the field's content changes."""
        text = entry.get_text()
        if text != entry.get_text():
            entry.set_text(text)
        self.option_changed(entry, option, text)

    # Editable grid
    def generate_editable_grid(self, option_name, label, value=None):
        """Adds an editable grid widget"""
        value = value or {}
        try:
            value = list(value.items())
        except AttributeError:
            logger.error("Invalid value of type %s passed to grid widget: %s", type(value), value)
            value = {}
        label = Label(label)

        grid = EditableGrid(value, columns=["Key", "Value"])
        grid.connect("changed", self._on_grid_changed, option_name)
        self.wrapper.pack_start(label, False, False, 0)
        self.wrapper.pack_start(grid, True, True, 0)
        self.option_widget = grid
        return grid

    def _on_grid_changed(self, grid, option):
        values = dict(grid.get_data())
        self.option_changed(grid, option, values)

    # Multiple file selector
    def generate_multiple_file_chooser(self, option_name, label, value=None):
        """Generate a multiple file selector."""
        vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
        label = Label(label + ":")
        label.set_halign(Gtk.Align.START)
        button = Gtk.Button(_("Add files"))
        button.connect("clicked", self.on_add_files_clicked, option_name, value)
        button.set_margin_left(10)
        vbox.pack_start(label, False, False, 5)
        vbox.pack_end(button, False, False, 0)

        if value:
            if isinstance(value, str):
                self.files = [value]
            else:
                self.files = value
        else:
            self.files = []
        self.files_list_store = Gtk.ListStore(str)
        for filename in self.files:
            self.files_list_store.append([filename])
        cell_renderer = Gtk.CellRendererText()
        files_treeview = Gtk.TreeView(self.files_list_store)
        files_column = Gtk.TreeViewColumn(_("Files"), cell_renderer, text=0)
        files_treeview.append_column(files_column)
        files_treeview.connect("key-press-event", self.on_files_treeview_keypress, option_name)
        treeview_scroll = Gtk.ScrolledWindow()
        treeview_scroll.set_min_content_height(130)
        treeview_scroll.set_margin_left(10)
        treeview_scroll.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
        treeview_scroll.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
        treeview_scroll.add(files_treeview)

        vbox.pack_start(treeview_scroll, True, True, 0)
        self.wrapper.pack_start(vbox, True, True, 0)
        self.option_widget = self.files_list_store

    def on_add_files_clicked(self, _widget, option_name, value):
        """Create and run multi-file chooser dialog."""
        dialog = Gtk.FileChooserNative.new(
            _("Select files"),
            None,
            Gtk.FileChooserAction.OPEN,
            _("_Add"),
            _("_Cancel"),
        )
        dialog.set_select_multiple(True)

        first_file_dir = os.path.dirname(value[0]) if value else None
        dialog.set_current_folder(
            first_file_dir or self.game.directory or self.config.get("game_path") or os.path.expanduser("~")
        )
        response = dialog.run()
        if response == Gtk.ResponseType.ACCEPT:
            self.add_files_to_treeview(dialog, option_name, self.wrapper)
        dialog.destroy()

    def add_files_to_treeview(self, dialog, option, wrapper):
        """Add several files to the configuration"""
        filenames = dialog.get_filenames()
        files = self.config.get(option, [])
        for filename in filenames:
            self.files_list_store.append([filename])
            if filename not in files:
                files.append(filename)
        self.option_changed(wrapper, option, files)

    def on_files_treeview_keypress(self, treeview, event, option):
        """Action triggered when a row is deleted from the filechooser."""
        key = event.keyval
        if key == Gdk.KEY_Delete:
            selection = treeview.get_selection()
            (model, treepaths) = selection.get_selected_rows()
            for treepath in treepaths:
                row_index = int(str(treepath))
                treeiter = model.get_iter(treepath)
                model.remove(treeiter)
                self.raw_config[option].pop(row_index)

    @staticmethod
    def on_query_tooltip(_widget, x, y, keybmode, tooltip, text):  # pylint: disable=unused-argument
        """Prepare a custom tooltip with a fixed width"""
        label = Label(text)
        label.set_use_markup(True)
        label.set_max_width_chars(60)
        hbox = Gtk.Box()
        hbox.pack_start(label, False, False, 0)
        hbox.show_all()
        tooltip.set_custom(hbox)
        return True

    def option_changed(self, widget, option_name, value):
        """Common actions when value changed on a widget"""
        self.raw_config[option_name] = value
        self.config[option_name] = value
        reset_btn = self.reset_buttons.get(option_name)
        wrapper = self.wrappers.get(option_name)

        if reset_btn:
            reset_btn.set_visible(True)

        if wrapper:
            self.set_style_property("font-weight", "bold", wrapper)

        self.update_warnings()

    def on_reset_button_clicked(self, btn, option, _widget, wrapper):
        """Clear option (remove from config, reset option widget)."""
        option_key = option["option"]
        current_value = self.config[option_key]

        btn.set_visible(False)
        self.set_style_property("font-weight", "normal", wrapper)
        self.raw_config.pop(option_key)
        self.lutris_config.update_cascaded_config()

        reset_value = self.config.get(option_key)
        if current_value == reset_value:
            return

        # Destroy and recreate option widget
        self.wrapper = wrapper
        children = wrapper.get_children()
        for child in children:
            child.destroy()
        self.call_widget_generator(option, option_key, reset_value, option.get("default"))
        self.wrapper.show_all()
        self.update_warnings()

    @staticmethod
    def set_style_property(property_, value, wrapper):
        """Add custom style."""
        style_provider = Gtk.CssProvider()
        style_provider.load_from_data("GtkHBox {{{}: {};}}".format(property_, value).encode())
        style_context = wrapper.get_style_context()
        style_context.add_provider(style_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)

    class SectionFrame(Gtk.Frame):
        """A frame that is styled to have particular margins, and can have its frame hidden.
        This leaves the content but removes the margins and borders and all that, so it looks
        like the frame was never there."""

        def __init__(self, section):
            super().__init__(label=section)
            self.section = section
            self.vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
            self.add(self.vbox)
            self.get_style_context().add_class("section-frame")

        def set_frame_visible(self, visible):
            if visible:
                self.show_frame()
            else:
                self.hide_frame()

        def show_frame(self):
            self.get_style_context().remove_class("frame-hidden")

        def hide_frame(self):
            self.get_style_context().add_class("frame-hidden")


class GameBox(ConfigBox):
    config_section = "game"

    def __init__(self, lutris_config, game):
        ConfigBox.__init__(self, game)
        self.lutris_config = lutris_config
        self.runner = game.runner
        if self.runner:
            self.options = self.runner.game_options
        else:
            logger.warning("No runner in game supplied to GameBox")


class RunnerBox(ConfigBox):
    """Configuration box for runner specific options"""

    config_section = "runner"

    def __init__(self, lutris_config, game=None):
        ConfigBox.__init__(self, game)
        self.lutris_config = lutris_config
        try:
            self.runner = import_runner(self.lutris_config.runner_slug)()
        except InvalidRunner:
            self.runner = None
        if self.runner:
            self.options = self.runner.get_runner_options()

        if lutris_config.level == "game":
            self.generate_top_info_box(_(
                "If modified, these options supersede the same options from "
                "the base runner configuration."
            ))


class SystemBox(ConfigBox):
    config_section = "system"

    def __init__(self, lutris_config):
        ConfigBox.__init__(self)
        self.lutris_config = lutris_config
        self.runner = None
        runner_slug = self.lutris_config.runner_slug

        if runner_slug:
            self.options = sysoptions.with_runner_overrides(runner_slug)
        else:
            self.options = sysoptions.system_options

        if lutris_config.game_config_id and runner_slug:
            self.generate_top_info_box(_(
                "If modified, these options supersede the same options from "
                "the base runner configuration, which themselves supersede "
                "the global preferences."
            ))
        elif runner_slug:
            self.generate_top_info_box(_(
                "If modified, these options supersede the same options from "
                "the global preferences."
            ))
