# SPDX-FileCopyrightText: 2022 Chris Heywood
#
# SPDX-License-Identifier: GPL-3.0-or-later

from gettext import gettext as _
import gi

gi.require_version("GtkSource", "5")
from gi.repository import Adw, Gio, GLib, GObject, Gtk, GtkSource, Pango

from iotas.config_manager import ConfigManager, HeaderBarVisibility
from iotas.ui_utils import add_mouse_button_accel, is_likely_mobile_device
from iotas.ui_utils import ComboRowHelper


@Gtk.Template(resource_path="/org/gnome/World/Iotas/ui/preferences_dialog.ui")
class PreferencesDialog(Adw.PreferencesDialog):
    __gtype_name__ = "PreferencesDialog"

    __gsignals__ = {
        "start-onboarding": (GObject.SignalFlags.RUN_FIRST, None, ()),
    }

    CLICKS_FOR_EXTENDED = 6
    SHORTER_TOAST = 2
    LONGER_TOAST = 5

    _index_group: Adw.PreferencesGroup = Gtk.Template.Child()
    _editor_theme_combo: Adw.ComboRow = Gtk.Template.Child()
    _index_category_style_combo: Adw.ComboRow = Gtk.Template.Child()
    _editor_formatting_bar_visibility_combo: Adw.ComboRow = Gtk.Template.Child()
    _editor_header_bar_visibility_combo: Adw.ComboRow = Gtk.Template.Child()
    _editor_detect_syntax: Adw.SwitchRow = Gtk.Template.Child()
    _connect_sync_row: Adw.ActionRow = Gtk.Template.Child()
    _disconnect_sync_row: Adw.ActionRow = Gtk.Template.Child()
    _reset_database_row: Adw.ActionRow = Gtk.Template.Child()
    _debug_page: Adw.PreferencesPage = Gtk.Template.Child()
    _enable_render_view: Adw.SwitchRow = Gtk.Template.Child()
    _markdown_default_to_render: Adw.SwitchRow = Gtk.Template.Child()
    _enable_markdown_maths: Adw.SwitchRow = Gtk.Template.Child()
    _keep_webkit_process: Adw.SwitchRow = Gtk.Template.Child()
    _markdown_use_monospace_font: Adw.SwitchRow = Gtk.Template.Child()
    _markdown_monospace_font_ratio: Adw.SpinRow = Gtk.Template.Child()
    _editor_line_length: Adw.SwitchRow = Gtk.Template.Child()

    def __init__(self) -> None:
        super().__init__()
        self.__app = Gio.Application.get_default()
        self.__config_manager = ConfigManager.get_default()

        self.__dependent_on_markdown_render = [
            self._markdown_default_to_render,
            self._enable_markdown_maths,
            self._markdown_use_monospace_font,
            self._markdown_monospace_font_ratio,
            self._keep_webkit_process,
        ]

        self.__build_actions()

        scheme_manager = GtkSource.StyleSchemeManager.get_default()
        options = {}
        scheme_ids = scheme_manager.get_scheme_ids()

        # Preference and order internal schemes
        def add_preferenced_scheme(scheme_id, name):
            if scheme_id not in scheme_ids:
                return
            options[scheme_id] = name

        # Exposing scheme names for translation here as attempt to translate via scheme XML files
        # wasn't fruitful
        preferenced_schemes = {
            # Translators: Description, a style name
            "iotas-mono": _("Monochrome"),
            # Translators: Description, a style name
            "iotas-alpha-muted": _("Muted Markup"),
            # Translators: Description, a style name
            "iotas-alpha-bold": _("Bold Markup"),
            # Translators: Description, a style name
            "iotas-unstyled": _("Disabled"),
        }

        for scheme_id, name in preferenced_schemes.items():
            add_preferenced_scheme(scheme_id, name)

        # Add other schemes (eg. in user data)
        for scheme_id in scheme_ids:
            if scheme_id in preferenced_schemes.keys():
                continue
            if scheme_id.endswith("-dark") or scheme_id.endswith("-high-contrast"):
                continue
            # Only Iotas-specific schemes, prefixed with "iotas-", are supported
            if scheme_id.startswith("iotas-"):
                scheme = scheme_manager.get_scheme(scheme_id)
                options[scheme_id] = scheme.get_name()

        helper = ComboRowHelper(
            self._editor_theme_combo,
            options,
            self.__config_manager.editor_theme,
        )
        helper.connect("changed", lambda _o, value: self.__apply_editor_theme_change(value))

        options = {
            # Translators: Description, a visual style (for category labels in index)
            "monochrome": _("Monochrome"),
            # Translators: Description, a visual style (for category labels in index)
            "muted": _("Muted"),
            # Translators: Description, a visual style (for category labels in index)
            "blue": _("Blue"),
            # Translators: Description, a visual style (for category labels in index)
            "orange": _("Orange"),
            # Translators: Description, a visual style (for category labels in index)
            "red": _("Red"),
            # Translators: Description, a visual style (for category labels in index)
            "none": _("None"),
        }
        helper = ComboRowHelper(
            self._index_category_style_combo,
            options,
            self.__config_manager.index_category_style,
        )
        helper.connect("changed", lambda _o, value: self.__apply_index_category_style_change(value))

        options = {
            # Translators: Description, for preference option - a description of visibility
            "always-visible": _("Always Visible"),
            # Translators: Description, for preference option - a description of visibility
            "auto-hide": _("Automatically Hide"),
            # Translators: Description, for preference option - a description of visibility
            "auto-hide-fullscreen-only": _("Auto Hide When Fullscreen"),
            # Translators: Description, for preference option - a description of visibility
            "disabled": _("Disabled"),
        }
        helper = ComboRowHelper(
            self._editor_formatting_bar_visibility_combo,
            options,
            str(self.__config_manager.editor_formatting_bar_visibility),
        )
        helper.connect(
            "changed",
            lambda _o, value: self.__on_formatting_visibility_change(value),
        )

        options = {
            # Translators: Description, for preference option - a description of visibility
            "always-visible": _("Always Visible"),
            # Translators: Description, for preference option - a description of visibility
            "auto-hide": _("Automatically Hide"),
            # Translators: Description, for preference option - a description of visibility
            "auto-hide-fullscreen-only": _("Auto Hide When Fullscreen"),
        }
        helper = ComboRowHelper(
            self._editor_header_bar_visibility_combo,
            options,
            str(self.__config_manager.editor_header_bar_visibility),
        )
        helper.connect(
            "changed",
            lambda _o, value: self.__on_header_bar_visibility_change(value),
        )

        self._markdown_monospace_font_ratio.connect(
            "notify::value", lambda _o, _v: self.__apply_font_ratio_change()
        )
        self.__config_manager.connect_changed(
            ConfigManager.MARKDOWN_RENDER_MONOSPACE_FONT_RATIO,
            self.__update_markdown_font_ratio_from_config,
        )
        self.__update_markdown_font_ratio_from_config()

        self._connect_sync_row.set_visible(not self.__config_manager.nextcloud_sync_configured)
        self._disconnect_sync_row.set_visible(self.__config_manager.nextcloud_sync_configured)
        self._reset_database_row.set_visible(not self.__config_manager.nextcloud_sync_configured)
        if not (self.__app.debug_session or self.__app.development_mode):
            self.remove(self._debug_page)

        self.__config_manager.connect_changed(
            ConfigManager.MARKDOWN_RENDER,
            self.__update_disabled_markdown_render_items,
        )
        self.__update_disabled_markdown_render_items()

        self.__init_excessive_preferences_handling()
        self.__toast = None
        self.__setup_temporary_combo_factories()

    def __build_actions(self) -> None:
        action_group = Gio.SimpleActionGroup.new()

        key = ConfigManager.USE_MONOSPACE_FONT
        monospace_font_action = self.__config_manager.create_action(key)
        action_group.add_action(monospace_font_action)

        key = ConfigManager.PERSIST_SIDEBAR
        persist_sidebar_action = self.__config_manager.create_action(key)
        action_group.add_action(persist_sidebar_action)

        key = ConfigManager.SPELLING_ENABLED
        spelling_action = self.__config_manager.create_action(key)
        action_group.add_action(spelling_action)

        key = ConfigManager.MARKDOWN_DETECT_SYNTAX
        syntax_action = self.__config_manager.create_action(key)
        action_group.add_action(syntax_action)

        key = ConfigManager.MARKDOWN_RENDER
        render_action = self.__config_manager.create_action(key)
        action_group.add_action(render_action)

        key = ConfigManager.MARKDOWN_DEFAULT_TO_RENDER
        default_render_action = self.__config_manager.create_action(key)
        action_group.add_action(default_render_action)

        key = ConfigManager.MARKDOWN_TEX_SUPPORT
        tex_action = self.__config_manager.create_action(key)
        action_group.add_action(tex_action)

        key = ConfigManager.MARKDOWN_USE_MONOSPACE_FONT
        markdown_monospace_action = self.__config_manager.create_action(key)
        action_group.add_action(markdown_monospace_action)

        key = ConfigManager.MARKDOWN_KEEP_WEBKIT_PROCESS
        keep_webkit_action = self.__config_manager.create_action(key)
        action_group.add_action(keep_webkit_action)

        key = ConfigManager.LINE_LENGTH
        setting_maximum = self.__config_manager.line_length_max
        init_state = self.__config_manager.line_length < setting_maximum
        action = Gio.SimpleAction.new_stateful(key, None, GLib.Variant("b", init_state))
        action.connect("change-state", self.__on_line_length_state_change)
        action_group.add_action(action)

        self.insert_action_group("settings", action_group)

    def __init_excessive_preferences_handling(self) -> None:
        """An experiment in providing configurability for more technical users while retaining
        simplicity for those less savvy.

        This is an experiment, and may get removed.
        """

        self.__extended_only = [
            self._index_category_style_combo,
            self._enable_render_view,
            self._markdown_default_to_render,
            self._enable_markdown_maths,
            self._keep_webkit_process,
            self._markdown_use_monospace_font,
            self._markdown_monospace_font_ratio,
            self._editor_line_length,
            self._editor_detect_syntax,
        ]
        self.__update_contextual()
        self.__config_manager.connect_changed(
            ConfigManager.SHOW_EXTENDED_PREFERENCES,
            self.__update_contextual,
        )

        self.__config_manager.connect_changed(
            ConfigManager.MARKDOWN_DETECT_SYNTAX,
            self.__update_contextual,
        )

        # Ouch, hacky. Going to hell for this one. Fetch a bunch of widgets to add click listeners
        # to as a method for toggling the visibility of the extended preferences.
        box = self._index_group.get_parent()
        clamp = box.get_parent()
        for widget in box, clamp:
            add_mouse_button_accel(
                widget, self.__on_preferences_background_click, Gtk.PropagationPhase.TARGET
            )

    def __on_preferences_background_click(
        self, gesture: Gtk.GestureClick, n_press: int, _x: float, _y: float
    ) -> None:
        extended = self.__config_manager.show_extended_preferences
        if gesture.get_current_button() != 1:
            return
        if n_press > 1 and n_press < self.CLICKS_FOR_EXTENDED:
            clicks = self.CLICKS_FOR_EXTENDED - n_press
            if extended:
                # Translators: Description, notification, {0} is a number
                text = _("Reducing in {0} presses").format(clicks)
            else:
                # Translators: Description, notification, {0} is a number
                text = _("Extending in {0} presses").format(clicks)
            self.__make_toast(text)
        elif n_press == self.CLICKS_FOR_EXTENDED:
            if extended:
                # Translators: Description, notification
                self.__make_toast(_("Extended hidden"))
            else:
                # Translators: Description, notification
                self.__make_toast(_("Extended shown"))
            self.__config_manager.show_extended_preferences = not extended

    def __on_header_bar_visibility_change(self, value: str) -> None:
        visibility = HeaderBarVisibility(value)
        self.__config_manager.editor_header_bar_visibility = visibility
        self.__discourage_mobile_toolbar_autohiding(visibility)

    def __on_formatting_visibility_change(self, value: str) -> None:
        visibility = HeaderBarVisibility(value)
        self.__config_manager.editor_formatting_bar_visibility = visibility
        self.__discourage_mobile_toolbar_autohiding(visibility)

    def __on_line_length_state_change(self, action: Gio.SimpleAction, state: bool) -> None:
        action.set_state(state)
        setting_maximum = self.__config_manager.line_length_max
        new_value = self.__config_manager.default_line_length if state else setting_maximum
        self.__config_manager.line_length = new_value

    def __discourage_mobile_toolbar_autohiding(self, visibility: HeaderBarVisibility) -> None:
        if not is_likely_mobile_device():
            return

        if visibility in (
            HeaderBarVisibility.AUTO_HIDE,
            HeaderBarVisibility.AUTO_HIDE_FULLSCREEN_ONLY,
        ):
            self.__make_toast(
                # Translators: Description, notification. Needs to be short for toast.
                _("Hiding discouraged on mobile"),
                long=True,
            )

    def __make_toast(self, text: str, long: bool = False) -> None:
        if self.__toast is not None:
            self.__toast.dismiss()
        toast = Adw.Toast.new(text)
        toast.set_priority(Adw.ToastPriority.HIGH)
        toast.set_timeout(self.LONGER_TOAST if long else self.SHORTER_TOAST)
        toast.connect("dismissed", lambda _o: self.__flag_toast_dismissed())
        self.__toast = toast
        self.add_toast(toast)

    def __flag_toast_dismissed(self) -> None:
        self.__toast = None

    @Gtk.Template.Callback()
    def _reset_database(self, _button: Gtk.Button) -> None:
        # Translators: Title
        title = _("Reset Database?")
        # Translators: Description
        body = _("All notes will be deleted. Continue with the reset?")
        dialog = Adw.AlertDialog.new(title, body)
        # Translators: Button
        dialog.add_response("close", _("Cancel"))
        # Translators: Button
        dialog.add_response("reset", _("Reset"))
        dialog.set_response_appearance("reset", Adw.ResponseAppearance.DESTRUCTIVE)

        def handle_response(_o: Adw.AlertDialog, response: str) -> None:
            if response == "reset":
                self.close()
                self.__app.reset_database()

        dialog.connect("response", handle_response)
        dialog.present(self)

    @Gtk.Template.Callback()
    def _connect_nextcloud(self, _button: Gtk.Button) -> None:
        GLib.idle_add(self.emit, "start-onboarding")

    @Gtk.Template.Callback()
    def _disconnect_nextcloud(self, _button: Gtk.Button) -> None:
        # Translators: Title
        title = _("Disconnect Nextcloud?")
        # Translators: Description
        body = _("All notes will be removed. Do you want to sign out?")
        dialog = Adw.AlertDialog.new(title, body)
        # Translators: Button
        dialog.add_response("close", _("Cancel"))
        # Translators: Button
        dialog.add_response("disconnect", _("Disconnect"))
        dialog.set_response_appearance("disconnect", Adw.ResponseAppearance.DESTRUCTIVE)

        def handle_response(_o: Adw.AlertDialog, response: str) -> None:
            if response == "disconnect":
                self.close()
                self.__app.disconnect_nextcloud()

        dialog.connect("response", handle_response)
        dialog.present(self)

    @Gtk.Template.Callback()
    def _reset_prune_threshold(self, _button: Gtk.Button) -> None:
        self.__app.reset_sync_marker()

    def __update_markdown_font_ratio_from_config(self) -> None:
        self._markdown_monospace_font_ratio.set_value(
            self.__config_manager.markdown_render_monospace_font_ratio
        )

    def __update_disabled_markdown_render_items(self) -> None:
        enabled = self.__config_manager.markdown_render_enabled
        for widget in self.__dependent_on_markdown_render:
            widget.set_sensitive(enabled)

    def __update_contextual(self) -> None:
        showing_extended = self.__config_manager.show_extended_preferences
        for obj in self.__extended_only:
            obj.set_visible(showing_extended)

        syntax_enabled = self.__config_manager.markdown_detect_syntax

        # If markdown syntax is disabled always show the preference as it disables others
        if not syntax_enabled:
            self._editor_detect_syntax.set_visible(True)

        self._editor_theme_combo.set_sensitive(syntax_enabled)
        self._editor_formatting_bar_visibility_combo.set_sensitive(syntax_enabled)

    def __apply_editor_theme_change(self, value: str) -> None:
        self.__config_manager.editor_theme = value

    def __apply_font_ratio_change(self) -> None:
        self.__config_manager.markdown_render_monospace_font_ratio = (
            self._markdown_monospace_font_ratio.get_value()
        )

    def __apply_index_category_style_change(self, value: str) -> None:
        self.__config_manager.index_category_style = value

    # Quick fix for this release (due to already being in string freeze) by using a combo row
    # factory to accommodate for long strings not fitting in combo row popovers. This basically
    # copies the stock Adw.ComboRow factory excluding the 20 character width limit.
    def __setup_temporary_combo_factories(self) -> None:
        header_combo_factory = Gtk.SignalListItemFactory()
        header_combo_factory.connect("setup", self.__long_comborow_factory_setup)
        header_combo_factory.connect(
            "bind", self.__long_comborow_factory_bind, self._editor_header_bar_visibility_combo
        )
        header_combo_factory.connect(
            "unbind", self.__long_comborow_factory_unbind, self._editor_header_bar_visibility_combo
        )
        self._editor_header_bar_visibility_combo.set_list_factory(header_combo_factory)

        format_combo_factory = Gtk.SignalListItemFactory()
        format_combo_factory.connect("setup", self.__long_comborow_factory_setup)
        format_combo_factory.connect(
            "bind", self.__long_comborow_factory_bind, self._editor_formatting_bar_visibility_combo
        )
        format_combo_factory.connect(
            "unbind",
            self.__long_comborow_factory_unbind,
            self._editor_formatting_bar_visibility_combo,
        )
        self._editor_formatting_bar_visibility_combo.set_list_factory(format_combo_factory)

    def __long_comborow_factory_setup(
        self, _factory: Gtk.SignalListItemFactory, list_item: Gtk.ListItem
    ) -> None:
        box = Gtk.Box()

        label = Gtk.Label()
        label.set_xalign(0)
        label.set_ellipsize(Pango.EllipsizeMode.END)
        label.set_width_chars(1)
        label.set_valign(Gtk.Align.CENTER)
        box.append(label)

        icon = Gtk.Image.new_from_icon_name("object-select-symbolic")
        icon.set_accessible_role(Gtk.AccessibleRole.PRESENTATION)
        box.append(icon)

        list_item.set_child(box)

    def __long_comborow_factory_bind(
        self, _factory: Gtk.SignalListItemFactory, list_item: Gtk.ListItem, combo: Adw.ComboRow
    ) -> None:
        box = list_item.get_child()
        label = box.get_first_child()
        label.set_text(list_item.get_item().get_string())
        combo.connect("notify::selected-item", self.__selected_item_changed, list_item)
        self.__selected_item_changed(combo, None, list_item)

    def __long_comborow_factory_unbind(
        self, _factory: Gtk.SignalListItemFactory, _list_item: Gtk.ListItem, combo: Adw.ComboRow
    ) -> None:
        combo.disconnect_by_func(self.__selected_item_changed)

    @staticmethod
    def __selected_item_changed(
        combo: Adw.ComboRow, _pspec: GObject.ParamSpec, list_item: Gtk.ListItem
    ) -> None:
        box = list_item.get_child()
        icon = box.get_last_child()
        if combo.get_selected_item() == list_item.get_item():
            icon.set_opacity(1.0)
        else:
            icon.set_opacity(0.0)
