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

from __future__ import annotations

from gi.repository import Adw, Gio, GLib, GObject

from enum import StrEnum
import logging
from typing import Callable, Optional


class HeaderBarVisibility(StrEnum):
    """Top or bottom bar visibility."""

    ALWAYS_VISIBLE = "always-visible"
    AUTO_HIDE = "auto-hide"
    AUTO_HIDE_FULLSCREEN_ONLY = "auto-hide-fullscreen-only"
    DISABLED = "disabled"


class ConfigManager(GObject.Object):

    STYLE = "style-variant"
    WINDOW_SIZE = "window-size"
    FIRST_START = "first-start"
    FONT_SIZE = "font-size"
    LINE_LENGTH = "line-length"
    USE_MONOSPACE_FONT = "use-monospace-font"
    EDITOR_THEME = "editor-theme"
    EDITOR_FORMATTING_BAR_VISIBILTY = "editor-formatting-bar-visibility"
    EDITOR_HEADER_BAR_VISIBILTY = "editor-header-bar-visibility"
    BACKUP_NOTE_EXTENSION = "backup-note-extension"
    PERSIST_SIDEBAR = "persist-sidebar"
    INDEX_CATEGORY_STYLE = "index-category-style"

    NEXTCLOUD_ENDPOINT = "nextcloud-endpoint"
    NEXTCLOUD_USERNAME = "nextcloud-username"
    NEXTCLOUD_PRUNE_THRESHOLD = "nextcloud-prune-threshold"
    SYNC_INTERVAL = "sync-interval"
    NETWORK_TIMEOUT = "network-timeout"

    SPELLING_ENABLED = "spelling-enabled"
    SPELLING_LANGUAGE = "spelling-language"

    SHOW_STARTUP_SECRET_SERVICE_FAILURE = "show-startup-secret-service-failure"
    SHOW_SYNCING_DEBUG_NOTIFICATION = "show-syncing-debug-notification"

    MARKDOWN_RENDER = "markdown-render-enabled"
    MARKDOWN_DETECT_SYNTAX = "markdown-syntax-detection-enabled"
    MARKDOWN_KEEP_WEBKIT_PROCESS = "markdown-keep-webkit-process"
    MARKDOWN_TEX_SUPPORT = "markdown-tex-support"
    MARKDOWN_USE_MONOSPACE_FONT = "markdown-use-monospace-font"
    MARKDOWN_RENDER_MONOSPACE_FONT_RATIO = "markdown-render-proportional-to-monospace-font-ratio"
    MARKDOWN_DEFAULT_TO_RENDER = "markdown-default-to-render"

    LAST_LAUNCHED_VERSION = "last-launched-version"
    LAST_EXPORT_DIRECTORY = "last-export-directory"
    EXTRA_EXPORT_FORMATS = "extra-export-formats"
    SHOW_EXTENDED_PREFERENCES = "show-extended-preferences"

    # Retaining to for old migrations ###################################################
    MARKDOWN_SYNTAX_HIGHLIGHTING = "markdown-syntax-highlighting-enabled"
    HIDE_EDITOR_HEADERBAR = "auto-hide-editor-headerbar"
    HIDE_HEADERBAR_WHEN_FULLSCREEN = "hide-editor-headerbar-when-fullscreen"
    #####################################################################################

    _instance = None
    _app_id = None
    settings: Gio.Settings

    def __init__(self, settings: Gio.Settings) -> None:
        super().__init__()
        self.settings = settings

    def connect_changed(self, property_name: str, callback: Callable[[], None]) -> None:
        """Connect callback to changed signal on property.

        The callback is called without the setting value.

        :param str property_name: Property to connect
        :param Callable[[], None] callback: Callback
        """
        self.settings.connect(f"changed::{property_name}", lambda _k, _v: callback())

    def create_action(self, key: str) -> Gio.Action:
        """Create action on Gio.Settings object for setting.

        :param str key: Key identifier
        :return: The action
        :rtype: Gio.Action
        """
        return self.settings.create_action(key)

    def bind(
        self, key: str, object: GObject.Object, property: str, flags: Gio.SettingsBindFlags
    ) -> None:
        """Bind setting to property.

        :param str key: Setting key
        :param GObject.Object object: Destination object to bind to
        :param str property: Destination property name
        :param Gio.SettingsBindFlags: Binding flags
        """
        self.settings.bind(key, object, property, flags)

    def get_window_size(self) -> GLib.Variant:
        """Get window size.

        :return: The size
        :rtype: GLib.Variant
        """
        return self.settings.get_value(self.WINDOW_SIZE)

    def set_window_size(self, width: int, height: int) -> None:
        """Set window size.

        :param int width: Width
        :param int height: Height
        """
        g_variant = GLib.Variant("ai", (width, height))
        self.settings.set_value(self.WINDOW_SIZE, g_variant)

    @GObject.Property(type=bool, default=True)
    def first_start(self) -> bool:
        """Get whether doing first startup.

        :return: First startup
        :rtype: bool
        """
        return self.settings.get_value(self.FIRST_START)

    @first_start.setter
    def set_first_start(self, value: bool) -> None:
        """Set whether first start.

        :param bool value: New value
        """
        self.settings.set_boolean(self.FIRST_START, value)

    @GObject.Property(type=bool, default=True)
    def use_monospace_font(self) -> bool:
        """Get whether to use a monospace font.

        :return: Using monospace font
        :rtype: bool
        """
        return self.settings.get_value(self.USE_MONOSPACE_FONT)

    @use_monospace_font.setter
    def set_use_monospace_font(self, value: bool) -> None:
        """Set whether to use a monospace font.

        :param bool value: New value
        """
        self.settings.set_boolean(self.USE_MONOSPACE_FONT, value)

    @GObject.Property(type=int)
    def font_size(self) -> int:
        """Get font size.

        :return: Size
        :rtype: int
        """
        return self.settings.get_int(self.FONT_SIZE)

    @font_size.setter
    def set_font_size(self, value: int) -> None:
        """Set font size.

        :param int value: New value
        """
        self.settings.set_int(self.FONT_SIZE, value)

    @GObject.Property(type=int)
    def default_font_size(self) -> int:
        """Get default font size.

        :return: Default size
        :rtype: int
        """
        return self.settings.get_default_value(self.FONT_SIZE).get_int32()

    @GObject.Property(type=int)
    def line_length(self) -> int:
        """Get line length.

        :return: Size in pixels
        :rtype: int
        """
        return self.settings.get_int(self.LINE_LENGTH)

    @line_length.setter
    def set_line_length(self, value: int) -> None:
        """Set line length.

        :param int value: New value
        """
        self.settings.set_int(self.LINE_LENGTH, value)

    @GObject.Property(type=int)
    def default_line_length(self) -> int:
        """Get default line length.

        :return: Default length
        :rtype: int
        """
        return self.settings.get_default_value(self.LINE_LENGTH).get_int32()

    @GObject.Property(type=int)
    def line_length_max(self) -> int:
        """Get line length maximum.

        :return: Size in pixels
        :rtype: int
        """
        return (
            self.settings.get_property("settings-schema")
            .get_key(self.LINE_LENGTH)
            .get_range()[1][-1]
        )

    @GObject.Property(type=bool, default=True)
    def spelling_enabled(self) -> bool:
        """Get whether spelling enabled.

        :return: Spelling enabled
        :rtype: bool
        """
        return self.settings.get_value(self.SPELLING_ENABLED)

    @spelling_enabled.setter
    def set_spelling_enabled(self, value: bool) -> None:
        """Set spelling enabled.

        :param bool value: New value
        """
        self.settings.set_boolean(self.SPELLING_ENABLED, value)

    @GObject.Property(type=str)
    def spelling_language(self) -> Optional[str]:
        """Get spelling language.

        :return: Language tag or None if empty
        :rtype: str
        """
        lang = self.settings.get_string(self.SPELLING_LANGUAGE)
        if lang.strip() == "":
            lang = None
        return lang

    @spelling_language.setter
    def set_spelling_language(self, value: str) -> None:
        """Set spelling language.

        :param str value: New value
        """
        self.settings.set_string(self.SPELLING_LANGUAGE, value)

    @GObject.Property(type=str)
    def style(self) -> str:
        """Get style.

        :return: Style
        :rtype: str
        """
        return self.settings.get_string(self.STYLE)

    @style.setter
    def set_style(self, value: str) -> None:
        """Set style.

        :param str value: New value
        """
        self.settings.set_string(self.STYLE, value)

    @GObject.Property(type=int)
    def sync_interval(self) -> int:
        """Get sync interval.

        :return: Interval
        :rtype: int
        """
        return self.settings.get_int(self.SYNC_INTERVAL)

    @sync_interval.setter
    def set_sync_interval(self, value: int) -> None:
        """Set sync interval.

        :param int value: New value
        """
        self.settings.set_int(self.SYNC_INTERVAL, value)

    @GObject.Property(type=str)
    def index_category_style(self) -> str:
        """Get the index category label style.

        :return: The style
        :rtype: str
        """
        return self.settings.get_string(self.INDEX_CATEGORY_STYLE)

    @index_category_style.setter
    def set_index_category_style(self, value: str) -> None:
        """Set the index category style.

        :param str value: New value
        """
        self.settings.set_string(self.INDEX_CATEGORY_STYLE, value)

    @GObject.Property(type=bool, default=True)
    def pin_sidebar(self) -> bool:
        """Get whether to pin the index sidebar (in large windows).

        :return: Whether to hide
        :rtype: bool
        """
        return self.settings.get_value(self.PERSIST_SIDEBAR)

    @pin_sidebar.setter
    def set_pin_sidebar(self, value: bool) -> None:
        """Set whether to pin the index sidebar (in large windows).

        :param bool value: New value
        """
        self.settings.set_boolean(self.PERSIST_SIDEBAR, value)

    @GObject.Property(type=str)
    def editor_theme(self) -> str:
        """Get editor theme.

        :return: Theme
        :rtype: str
        """
        return self.settings.get_string(self.EDITOR_THEME)

    @editor_theme.setter
    def set_editor_theme(self, value: str) -> None:
        """Set editor theme.

        :param str value: New value
        """
        self.settings.set_string(self.EDITOR_THEME, value)

    @property
    def editor_formatting_bar_visibility(self) -> Optional[HeaderBarVisibility]:
        """Get the formatting bar visibility.

        :return: Visibility, or None
        :rtype: HeaderBarVisibility, optional
        """
        setting_value = self.settings.get_string(self.EDITOR_FORMATTING_BAR_VISIBILTY)
        try:
            obj = HeaderBarVisibility(setting_value)
        except ValueError as e:
            logging.warning(f"Couldn't parse formatting bar visibility from '{setting_value}': {e}")
            return None
        else:
            return obj

    @editor_formatting_bar_visibility.setter
    def editor_formatting_bar_visibility(self, value: HeaderBarVisibility) -> None:
        """Set the formatting bar visibility.

        :param HeaderBarVisibility value: New value
        """
        self.settings.set_string(self.EDITOR_FORMATTING_BAR_VISIBILTY, str(value))

    @GObject.Property(type=bool, default=True)
    def editor_formatting_bar_visible_for_window_state(self) -> bool:
        """Get whether the formatting bar is configured visible for the window maximised state."

        :return: Whether visible
        :rtype: bool
        """
        setting_value = self.settings.get_string(self.EDITOR_FORMATTING_BAR_VISIBILTY)
        try:
            obj = HeaderBarVisibility(setting_value)
        except ValueError as e:
            logging.warning(f"Couldn't parse formatting bar visibility from '{setting_value}': {e}")
            return False
        else:
            visible = False
            if self.markdown_detect_syntax:
                if obj == HeaderBarVisibility.ALWAYS_VISIBLE:
                    visible = True
                elif obj == HeaderBarVisibility.AUTO_HIDE_FULLSCREEN_ONLY:
                    app = Gio.Application.get_default()
                    if not app:
                        logging.error("Couldn't get app?")
                        return False
                    window = app.get_active_window()
                    visible = not window.is_fullscreen()
            return visible

    @property
    def editor_header_bar_visibility(self) -> Optional[HeaderBarVisibility]:
        """Get the header bar visibility.

        :return: The visibility setting
        :rtype: HeaderBarVisibility
        """
        setting_value = self.settings.get_string(self.EDITOR_HEADER_BAR_VISIBILTY)
        try:
            obj = HeaderBarVisibility(setting_value)
        except ValueError as e:
            logging.warning(f"Couldn't parse header bar visibility from '{setting_value}': {e}")
            return None
        else:
            return obj

    @editor_header_bar_visibility.setter
    def editor_header_bar_visibility(self, value: HeaderBarVisibility) -> None:
        """Set the header bar visibility.

        :param HeaderBarVisibility value: New value
        """
        self.settings.set_string(self.EDITOR_HEADER_BAR_VISIBILTY, str(value))

    @GObject.Property(type=bool, default=True)
    def editor_header_bar_visible_for_window_state(self) -> bool:
        """Get whether the header bar is configured visible for the window maximised state."

        :return: Whether visible
        :rtype: bool
        """
        setting_value = self.settings.get_string(self.EDITOR_HEADER_BAR_VISIBILTY)
        try:
            obj = HeaderBarVisibility(setting_value)
        except ValueError as e:
            logging.warning(f"Couldn't parse header bar visibility from '{setting_value}': {e}")
            return False
        else:
            visible = False
            if obj == HeaderBarVisibility.ALWAYS_VISIBLE:
                visible = True
            elif obj == HeaderBarVisibility.AUTO_HIDE_FULLSCREEN_ONLY:
                app = Gio.Application.get_default()
                if not app:
                    logging.error("Couldn't get app?")
                    return False
                window = app.get_active_window()
                visible = not window.is_fullscreen()
            return visible

    @GObject.Property(type=bool, default=True)
    def markdown_render_enabled(self) -> bool:
        """Get markdown render is enabled.

        :return: Markdown render enabled
        :rtype: bool
        """
        return self.settings.get_value(self.MARKDOWN_RENDER)

    @markdown_render_enabled.setter
    def set_markdown_render_enabled(self, value: bool) -> None:
        """Set markdown render enabled.

        :param bool value: New value
        """
        self.settings.set_boolean(self.MARKDOWN_RENDER, value)

    @GObject.Property(type=bool, default=True)
    def markdown_detect_syntax(self) -> bool:
        """Get markdown syntax detection is enabled.

        :return: Enabled
        :rtype: bool
        """
        return self.settings.get_value(self.MARKDOWN_DETECT_SYNTAX)

    @markdown_detect_syntax.setter
    def set_markdown_detect_syntax(self, value: bool) -> None:
        """Set markdown syntax detection is enabled.

        :param bool value: New value
        """
        self.settings.set_boolean(self.MARKDOWN_DETECT_SYNTAX, value)

    @GObject.Property(type=bool, default=True)
    def markdown_keep_webkit_process(self) -> bool:
        """Get markdown WebKit process being retained.

        :return: WebKit process being retained
        :rtype: bool
        """
        return self.settings.get_value(self.MARKDOWN_KEEP_WEBKIT_PROCESS)

    @markdown_keep_webkit_process.setter
    def set_markdown_keep_webkit_process(self, value: bool) -> None:
        """Set markdown WebKit process being retained.

        :param bool value: New value
        """
        self.settings.set_boolean(self.MARKDOWN_KEEP_WEBKIT_PROCESS, value)

    @GObject.Property(type=bool, default=True)
    def markdown_tex_support(self) -> bool:
        """Get whether markdown TeX rendering is supported.

        :return: Markdown TeX supported
        :rtype: bool
        """
        return self.settings.get_value(self.MARKDOWN_TEX_SUPPORT)

    @markdown_tex_support.setter
    def set_markdown_tex_support(self, value: bool) -> None:
        """Set whether markdown TeX rendering is supported.

        :param bool value: New value
        """
        self.settings.set_boolean(self.MARKDOWN_TEX_SUPPORT, value)

    @GObject.Property(type=bool, default=True)
    def markdown_use_monospace_font(self) -> bool:
        """Get whether to use a monospace font for the markdown render.

        :return: Using monospace font
        :rtype: bool
        """
        return self.settings.get_value(self.MARKDOWN_USE_MONOSPACE_FONT)

    @markdown_use_monospace_font.setter
    def set_markdown_use_monospace_font(self, value: bool) -> None:
        """Set whether to use a monospace font for the markdown render.

        :param bool value: New value
        """
        self.settings.set_boolean(self.MARKDOWN_USE_MONOSPACE_FONT, value)

    @GObject.Property(type=float)
    def markdown_render_monospace_font_ratio(self) -> float:
        """Get the adjustment in size from proportional to fixed width font.

        :return: Ratio
        :rtype: float
        """
        return self.settings.get_double(self.MARKDOWN_RENDER_MONOSPACE_FONT_RATIO)

    @markdown_render_monospace_font_ratio.setter
    def set_markdown_render_monospace_font_ratio(self, value: float) -> None:
        """Set the adjustment in size from proportional to fixed width font.

        :param bool value: New value
        """
        self.settings.set_double(self.MARKDOWN_RENDER_MONOSPACE_FONT_RATIO, value)

    @GObject.Property(type=bool, default=True)
    def markdown_default_to_render(self) -> bool:
        """Get whether to render the markdown when opening the note.

        :return: Defaulting to render
        :rtype: bool
        """
        return self.settings.get_value(self.MARKDOWN_DEFAULT_TO_RENDER)

    @markdown_default_to_render.setter
    def set_markdown_default_to_render(self, value: bool) -> None:
        """Set whether to render the markdown when opening the note.

        :param bool value: New value
        """
        self.settings.set_boolean(self.MARKDOWN_DEFAULT_TO_RENDER, value)

    @GObject.Property(type=str)
    def nextcloud_endpoint(self) -> str:
        """Get Nextcloud endpoint.

        :return: Endpoint
        :rtype: str
        """
        return self.settings.get_string(self.NEXTCLOUD_ENDPOINT)

    @nextcloud_endpoint.setter
    def set_nextcloud_endpoint(self, value: str) -> None:
        """Set Nextcloud endpoint.

        :param str value: New value
        """
        self.settings.set_string(self.NEXTCLOUD_ENDPOINT, value)

    @GObject.Property(type=str)
    def nextcloud_username(self) -> str:
        """Get Nextcloud username.

        :return: Username
        :rtype: str
        """
        return self.settings.get_string(self.NEXTCLOUD_USERNAME)

    @nextcloud_username.setter
    def set_nextcloud_username(self, value: str) -> None:
        """Set Nextcloud username.

        :param str value: New value
        """
        self.settings.set_string(self.NEXTCLOUD_USERNAME, value)

    @GObject.Property(type=bool, default=False)
    def nextcloud_sync_configured(self) -> bool:
        """Get whether sync with Nextcloud is configured.

        This does not mean authentication has been successful this session.

        :return: Whether configured
        :rtype: bool
        """
        sync_username = self.nextcloud_username
        sync_endpoint = self.nextcloud_endpoint
        return sync_username != "" and sync_endpoint != ""

    @GObject.Property(type=int)
    def nextcloud_prune_threshold(self) -> int:
        """Get Nextcloud prune threshold.

        :return: Threshold
        :rtype: int
        """
        return self.settings.get_int(self.NEXTCLOUD_PRUNE_THRESHOLD)

    @nextcloud_prune_threshold.setter
    def set_nextcloud_prune_threshold(self, value: int) -> None:
        """Set Nextcloud prune threshold.

        :param int value: New value
        """
        self.settings.set_int(self.NEXTCLOUD_PRUNE_THRESHOLD, value)

    @GObject.Property(type=bool, default=True)
    def show_startup_secret_service_failure(self) -> bool:
        """Get to show Secret Service failure at startup.

        :return: Whether to show
        :rtype: bool
        """
        return self.settings.get_value(self.SHOW_STARTUP_SECRET_SERVICE_FAILURE)

    @GObject.Property(type=bool, default=True)
    def show_syncing_debug_notification(self) -> bool:
        """Get whether to show sync debug notifications.

        :return: Whether to show
        :rtype: bool
        """
        return self.settings.get_value(self.SHOW_SYNCING_DEBUG_NOTIFICATION)

    @GObject.Property(type=str)
    def backup_note_extension(self) -> str:
        """Get file extension for backed up notes.

        :return: Extension
        :rtype: str
        """
        return self.settings.get_string(self.BACKUP_NOTE_EXTENSION)

    @backup_note_extension.setter
    def set_backup_note_extension(self, value: str) -> None:
        """Set file extension for backed up notes.

        :param str value: New value
        """
        self.settings.set_string(self.BACKUP_NOTE_EXTENSION, value)

    @GObject.Property(type=str)
    def last_launched_version(self) -> str:
        """Get the last version which was run.

        :return: Version
        :rtype: str
        """
        return self.settings.get_string(self.LAST_LAUNCHED_VERSION)

    @last_launched_version.setter
    def set_last_launched_version(self, value: str) -> None:
        """Set the last version which was run.

        :param str value: New value
        """
        self.settings.set_string(self.LAST_LAUNCHED_VERSION, value)

    @GObject.Property(type=str)
    def last_export_directory(self) -> str:
        """Get the last export directory.

        :return: Directory
        :rtype: str
        """
        return self.settings.get_string(self.LAST_EXPORT_DIRECTORY)

    @last_export_directory.setter
    def set_last_export_directory(self, value: str) -> None:
        """Set the last export directory.

        :param str value: New value
        """
        self.settings.set_string(self.LAST_EXPORT_DIRECTORY, value)

    @GObject.Property(type=str)
    def extra_export_formats(self) -> str:
        """Get any extra export formats.

        :return: Raw formats string
        :rtype: str
        """
        return self.settings.get_string(self.EXTRA_EXPORT_FORMATS)

    @extra_export_formats.setter
    def set_extra_export_formats(self, value: str) -> None:
        """Set extra export formats.

        :param str value: New value
        """
        self.settings.set_string(self.EXTRA_EXPORT_FORMATS, value)

    @GObject.Property(type=bool, default=True)
    def show_extended_preferences(self) -> bool:
        """Get whether to show a great many preferences.

        :return: Whether to show
        :rtype: bool
        """
        return self.settings.get_value(self.SHOW_EXTENDED_PREFERENCES)

    @show_extended_preferences.setter
    def set_show_extended_preferences(self, value: bool) -> None:
        """Set whether to show a great many preferences.

        :param bool value: New value
        """
        self.settings.set_boolean(self.SHOW_EXTENDED_PREFERENCES, value)

    @GObject.Property(type=float)
    def network_timeout(self) -> float:
        """Get the network timeout.

        :return: Timeout in seconds
        :rtype: float
        """
        return self.settings.get_double(self.NETWORK_TIMEOUT)

    @network_timeout.setter
    def set_network_timeout(self, value: float) -> None:
        """Set the network timeout.

        :param int value: New value
        """
        self.settings.set_double(self.NETWORK_TIMEOUT, value)

    # Piped through the configuration manager for now as a result of the HTML generator using it
    # directly as configuration.
    @GObject.Property(type=bool, default=True)
    def high_contrast(self) -> bool:
        """Get whether high contrast output should be used for screen rendering.

        :return: Whether visible
        :rtype: bool
        """
        return Adw.StyleManager.get_default().get_high_contrast()

    # TODO remove ~early 2025 #####################################################

    def get_markdown_syntax_hightlighting_enabled(self) -> bool:
        """Get markdown syntax highlighting is enabled.

        :return: Highlighting enabled
        :rtype: bool
        """
        return self.settings.get_value(self.MARKDOWN_SYNTAX_HIGHLIGHTING)

    def get_hide_editor_headerbar(self) -> bool:
        """Get whether to hide the editor headerbar.

        :return: Whether to hide
        :rtype: bool
        """
        return self.settings.get_value(self.HIDE_EDITOR_HEADERBAR)

    def get_hide_editor_headerbar_when_fullscreen(self) -> bool:
        """Get whether to hide the editor headerbar when fullscreen.

        :return: Whether to hide
        :rtype: bool
        """
        return self.settings.get_value(self.HIDE_HEADERBAR_WHEN_FULLSCREEN)

    ###############################################################################

    @staticmethod
    def set_app_id(app_id: str) -> None:
        if not ConfigManager._instance:
            ConfigManager._app_id = app_id

    @staticmethod
    def get_default() -> ConfigManager:
        if not ConfigManager._instance:
            settings = Gio.Settings.new(ConfigManager._app_id)
            ConfigManager._instance = ConfigManager(settings)
        return ConfigManager._instance
