"""
This file contains all of the classes related for a shared server configuration form.
"""

from dataclasses import dataclass
from functools import partial
from pathlib import Path
from time import sleep
from typing import Any, Callable, cast, Dict, Iterable, Optional, Tuple, Type, Union

from gi.repository import GLib, GObject, Gtk, Pango

from . import ConfigurationStore


@dataclass
class ConfigParamDescriptor:
    """
    Describes a parameter that can be used to configure an adapter. The
    :class:`description`, :class:`required` and :class:`default:` should be self-evident
    as to what they do.

    The :class:`helptext` parameter is optional detailed text that will be shown in a
    help bubble corresponding to the field.

    The :class:`type` must be one of the following:

    * The literal type ``str``: corresponds to a freeform text entry field in the UI.
    * The literal type ``bool``: corresponds to a toggle in the UI.
    * The literal type ``int``: corresponds to a numeric input in the UI.
    * The literal string ``"password"``: corresponds to a password entry field in the
      UI.
    * The literal string ``"option"``: corresponds to dropdown in the UI.
    * The literal type ``Path``: corresponds to a file picker in the UI.

    The :class:`advanced` parameter specifies whether the setting should be behind an
    "Advanced" expander.

    The :class:`numeric_bounds` parameter only has an effect if the :class:`type` is
    `int`. It specifies the min and max values that the UI control can have.

    The :class:`numeric_step` parameter only has an effect if the :class:`type` is
    `int`. It specifies the step that will be taken using the "+" and "-" buttons on the
    UI control (if supported).

    The :class:`options` parameter only has an effect if the :class:`type` is
    ``"option"``. It specifies the list of options that will be available in the
    dropdown in the UI.

    The :class:`pathtype` parameter only has an effect if the :class:`type` is
    ``Path``. It can be either ``"file"`` or ``"directory"`` corresponding to a file
    picker and a directory picker, respectively.
    """

    type: Union[Type, str]
    description: str
    required: bool = True
    helptext: Optional[str] = None
    advanced: Optional[bool] = None
    default: Any = None
    numeric_bounds: Optional[Tuple[int, int]] = None
    numeric_step: Optional[int] = None
    options: Optional[Iterable[str]] = None
    pathtype: Optional[str] = None


class ConfigureServerForm(Gtk.Box):
    __gsignals__ = {
        "config-valid-changed": (
            GObject.SignalFlags.RUN_FIRST,
            GObject.TYPE_NONE,
            (bool,),
        ),
    }

    def __init__(
        self,
        config_store: ConfigurationStore,
        config_parameters: Dict[str, ConfigParamDescriptor],
        verify_configuration: Callable[[], Dict[str, Optional[str]]],
        is_networked: bool = True,
    ):
        """
        Inititialize a :class:`ConfigureServerForm` with the given configuration
        parameters.

        :param config_store: The :class:`ConfigurationStore` to use to store
            configuration values for this adapter.
        :param config_parameters: An dictionary where the keys are the name of the
            configuration paramter and the values are the :class:`ConfigParamDescriptor`
            object corresponding to that configuration parameter. The order of the keys
            in the dictionary correspond to the order that the configuration parameters
            will be shown in the UI.
        :param verify_configuration: A function that verifies whether or not the
            current state of the ``config_store`` is valid. The output should be a
            dictionary containing verification errors. The keys of the returned
            dictionary should be the same as the keys passed in via the
            ``config_parameters`` parameter. The values should be strings describing
            why the corresponding value in the ``config_store`` is invalid.

            If the adapter ``is_networked``, and the special ``"__ping__"`` key is
            returned, then the error will be shown below all of the other settings in
            the ping status box.
        :param is_networked: whether or not the adapter is networked.
        """
        Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL)
        self.config_store = config_store
        self.required_config_parameter_keys = set()
        self.verify_configuration = verify_configuration
        self.entries = {}
        self.is_networked = is_networked

        content_grid = Gtk.Grid(
            column_spacing=10,
            row_spacing=5,
            margin_left=10,
            margin_right=10,
        )
        advanced_grid = Gtk.Grid(column_spacing=10, row_spacing=10)

        def create_string_input(is_password: bool, key: str) -> Gtk.Entry:
            entry = Gtk.Entry(
                text=cast(
                    Callable[[str], None],
                    (config_store.get_secret if is_password else config_store.get),
                )(key),
                hexpand=True,
            )
            if is_password:
                entry.set_visibility(False)

            entry.connect(
                "changed",
                lambda e: self._on_config_change(key, e.get_text(), secret=is_password),
            )
            return entry

        def create_bool_input(key: str) -> Gtk.Switch:
            switch = Gtk.Switch(active=config_store.get(key), halign=Gtk.Align.START)
            switch.connect(
                "notify::active",
                lambda s, _: self._on_config_change(key, s.get_active()),
            )
            return switch

        def create_int_input(key: str) -> Gtk.SpinButton:
            raise NotImplementedError()

        def create_option_input(key: str) -> Gtk.ComboBox:
            raise NotImplementedError()

        def create_path_input(key: str) -> Gtk.FileChooser:
            raise NotImplementedError()

        content_grid_i = 0
        advanced_grid_i = 0
        for key, cpd in config_parameters.items():
            if cpd.required:
                self.required_config_parameter_keys.add(key)
            if cpd.default is not None:
                config_store[key] = config_store.get(key, cpd.default)

            label = Gtk.Label(label=cpd.description, halign=Gtk.Align.END)

            input_el_box = Gtk.Box()
            self.entries[key] = cast(
                Callable[[str], Gtk.Widget],
                {
                    str: partial(create_string_input, False),
                    "password": partial(create_string_input, True),
                    bool: create_bool_input,
                    int: create_int_input,
                    "option": create_option_input,
                    Path: create_path_input,
                }[cpd.type],
            )(key)
            input_el_box.add(self.entries[key])

            if cpd.helptext:
                help_icon = Gtk.Image.new_from_icon_name(
                    "help-about",
                    Gtk.IconSize.BUTTON,
                )
                help_icon.get_style_context().add_class("configure-form-help-icon")
                help_icon.set_tooltip_markup(cpd.helptext)
                input_el_box.add(help_icon)

            if not cpd.advanced:
                content_grid.attach(label, 0, content_grid_i, 1, 1)
                content_grid.attach(input_el_box, 1, content_grid_i, 1, 1)
                content_grid_i += 1
            else:
                advanced_grid.attach(label, 0, advanced_grid_i, 1, 1)
                advanced_grid.attach(input_el_box, 1, advanced_grid_i, 1, 1)
                advanced_grid_i += 1

        # Add a button and revealer for the advanced section of the configuration.
        if advanced_grid_i > 0:
            advanced_component = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)

            advanced_expander = Gtk.Revealer()
            advanced_expander_icon = Gtk.Image.new_from_icon_name(
                "go-down-symbolic", Gtk.IconSize.BUTTON
            )
            revealed = False

            def toggle_expander(*args):
                nonlocal revealed
                revealed = not revealed
                advanced_expander.set_reveal_child(revealed)
                icon_dir = "up" if revealed else "down"
                advanced_expander_icon.set_from_icon_name(
                    f"go-{icon_dir}-symbolic", Gtk.IconSize.BUTTON
                )

            advanced_expander_button = Gtk.Button(relief=Gtk.ReliefStyle.NONE)
            advanced_expander_button_box = Gtk.Box(
                orientation=Gtk.Orientation.HORIZONTAL, spacing=10
            )

            advanced_label = Gtk.Label(
                label="<b>Advanced Settings</b>", use_markup=True
            )
            advanced_expander_button_box.add(advanced_label)
            advanced_expander_button_box.add(advanced_expander_icon)

            advanced_expander_button.add(advanced_expander_button_box)
            advanced_expander_button.connect("clicked", toggle_expander)
            advanced_component.add(advanced_expander_button)

            advanced_expander.add(advanced_grid)
            advanced_component.add(advanced_expander)

            content_grid.attach(advanced_component, 0, content_grid_i, 2, 1)
            content_grid_i += 1

        content_grid.attach(
            Gtk.Separator(name="config-verification-separator"), 0, content_grid_i, 2, 1
        )
        content_grid_i += 1

        self.config_verification_box = Gtk.Box(spacing=10)
        content_grid.attach(self.config_verification_box, 0, content_grid_i, 2, 1)

        self.pack_start(content_grid, False, False, 10)
        self._verification_status_ratchet = 0
        self._verify_config(self._verification_status_ratchet)

    had_all_required_keys = False
    verifying_in_progress = False

    def _set_verification_status(
        self, verifying: bool, is_valid: bool = False, error_text: str = None
    ):
        from sublime_music.ui import util

        if verifying:
            if not self.verifying_in_progress:
                for c in self.config_verification_box.get_children():
                    self.config_verification_box.remove(c)
                self.config_verification_box.add(
                    Gtk.Spinner(active=True, name="verify-config-spinner")
                )
                self.config_verification_box.add(
                    Gtk.Label(
                        label="<b>Verifying configuration...</b>", use_markup=True
                    )
                )
            self.verifying_in_progress = True
        else:
            self.verifying_in_progress = False
            for c in self.config_verification_box.get_children():
                self.config_verification_box.remove(c)

            def set_icon_and_label(icon_name: str, label_text: str):
                self.config_verification_box.add(
                    Gtk.Image.new_from_icon_name(icon_name, Gtk.IconSize.DND)
                )
                label = Gtk.Label(
                    label=label_text,
                    use_markup=True,
                    ellipsize=Pango.EllipsizeMode.END,
                )
                label.set_tooltip_markup(label_text)
                self.config_verification_box.add(label)

            if is_valid:
                set_icon_and_label(
                    "config-ok-symbolic", "<b>Configuration is valid</b>"
                )
            elif escaped := util.esc(error_text):
                set_icon_and_label("config-error-symbolic", escaped)

        self.config_verification_box.show_all()

    def _on_config_change(self, key: str, value: Any, secret: bool = False):
        if secret:
            self.config_store.set_secret(key, value)
        else:
            self.config_store[key] = value
        self._verification_status_ratchet += 1
        self._verify_config(self._verification_status_ratchet)

    def _verify_config(self, ratchet: int):
        self.emit("config-valid-changed", False)

        from sublime_music.adapters import Result

        if self.required_config_parameter_keys.issubset(set(self.config_store.keys())):
            if self._verification_status_ratchet != ratchet:
                return

            self._set_verification_status(True)

            has_empty = False
            if self.had_all_required_keys:
                for key in self.required_config_parameter_keys:
                    if self.config_store.get(key) == "":
                        self.entries[key].get_style_context().add_class("invalid")
                        self.entries[key].set_tooltip_markup("This field is required")
                        has_empty = True
                    else:
                        self.entries[key].get_style_context().remove_class("invalid")
                        self.entries[key].set_tooltip_markup(None)

            self.had_all_required_keys = True
            if has_empty:
                self._set_verification_status(
                    False,
                    error_text="<b>There are missing fields</b>\n"
                    "Please fill out all required fields.",
                )
                return

            def on_verify_result(verification_errors: Dict[str, Optional[str]]):
                if self._verification_status_ratchet != ratchet:
                    return

                if len(verification_errors) == 0:
                    self.emit("config-valid-changed", True)
                    for entry in self.entries.values():
                        entry.get_style_context().remove_class("invalid")
                    self._set_verification_status(False, is_valid=True)
                    return

                for key, entry in self.entries.items():
                    if error_text := verification_errors.get(key):
                        entry.get_style_context().add_class("invalid")
                        entry.set_tooltip_markup(error_text)
                    else:
                        entry.get_style_context().remove_class("invalid")
                        entry.set_tooltip_markup(None)

                self._set_verification_status(
                    False, error_text=verification_errors.get("__ping__")
                )

            def verify_with_delay() -> Dict[str, Optional[str]]:
                sleep(0.75)
                if self._verification_status_ratchet != ratchet:
                    return {}

                return self.verify_configuration()

            errors_result: Result[Dict[str, Optional[str]]] = Result(verify_with_delay)
            errors_result.add_done_callback(
                lambda f: GLib.idle_add(on_verify_result, f.result())
            )
